diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 8d1f196..dec4d8c 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -118,7 +118,7 @@ defmodule ErrorTracker do {kind, reason} = normalize_exception(exception, stacktrace) {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace) {:ok, error} = Error.new(kind, reason, stacktrace) - context = Map.merge(get_context(), given_context) + context = get_context() |> Map.merge(given_context) |> filter_context_data() if enabled?() && !ignored?(error, context) do {_error, occurrence} = upsert_error!(error, stacktrace, context, reason) @@ -209,6 +209,16 @@ defmodule ErrorTracker do ignorer && ignorer.ignore?(error, context) end + defp filter_context_data(context) do + filter_mod = Application.get_env(:error_tracker, :filter) + + if filter_mod do + filter_mod.sanitize(context) + else + context + end + end + defp normalize_exception(%struct{} = ex, _stacktrace) when is_exception(ex) do {to_string(struct), Exception.message(ex)} end diff --git a/lib/error_tracker/filter.ex b/lib/error_tracker/filter.ex new file mode 100644 index 0000000..69f1ac7 --- /dev/null +++ b/lib/error_tracker/filter.ex @@ -0,0 +1,32 @@ +defmodule ErrorTracker.Filter do + @moduledoc """ + Behaviour for sanitizing & modifying the error context before it's saved. + + defmodule MyApp.ErrorFilter do + @behaviour ErrorTracker.Filter + + @impl true + def sanitize(context) do + context # Modify the context object (add or remove fields as much as you need.) + end + end + + Once implemented, include it in the ErrorTracker configuration: + + config :error_tracker, filter: MyApp.Filter + + With this configuration in place, the ErrorTracker will call `MyApp.Filter.sanitize/1` to get a context before + saving error occurrence. + + > #### A note on performance {: .warning} + > + > Keep in mind that the `sanitize/1` will be called in the context of the ErrorTracker itself. + > Slow code will have a significant impact in the ErrorTracker performance. Buggy code can bring + > the ErrorTracker process down. + """ + + @doc """ + This function will be given an error context to inspect/modify before it's saved. + """ + @callback sanitize(context :: map()) :: map() +end diff --git a/test/error_tracker/filter_test.exs b/test/error_tracker/filter_test.exs new file mode 100644 index 0000000..2231155 --- /dev/null +++ b/test/error_tracker/filter_test.exs @@ -0,0 +1,63 @@ +defmodule ErrorTracker.FilterTest do + use ErrorTracker.Test.Case + + setup context do + if filter = context[:filter] do + previous_setting = Application.get_env(:error_tracker, :filter) + Application.put_env(:error_tracker, :filter, filter) + # Ensure that the application env is restored after each test + on_exit(fn -> Application.put_env(:error_tracker, :filter, previous_setting) end) + end + + [] + end + + @sensitive_ctx %{ + "request" => %{ + "headers" => %{ + "accept" => "application/json, text/plain, */*", + "authorization" => "Bearer 12341234" + } + } + } + + test "without an filter, context objects are saved as they are." do + assert %ErrorTracker.Occurrence{context: ctx} = + report_error(fn -> raise "BOOM" end, @sensitive_ctx) + + assert ctx == @sensitive_ctx + end + + @tag filter: ErrorTracker.FilterTest.AuthHeaderHider + test "user defined filter should be used to sanitize the context before it's saved." do + assert %ErrorTracker.Occurrence{context: ctx} = + report_error(fn -> raise "BOOM" end, @sensitive_ctx) + + assert ctx != @sensitive_ctx + + cleaned_header_value = + ctx |> Map.get("request") |> Map.get("headers") |> Map.get("authorization") + + assert cleaned_header_value == "REMOVED" + end +end + +defmodule ErrorTracker.FilterTest.AuthHeaderHider do + @behaviour ErrorTracker.Filter + + def sanitize(context) do + context + |> Enum.map(fn + {"authorization", _} -> + {"authorization", "REMOVED"} + + o -> + o + end) + |> Enum.map(fn + {key, val} when is_map(val) -> {key, sanitize(val)} + o -> o + end) + |> Map.new() + end +end