Skip to content

Commit

Permalink
Sanitize the context when tracking errors (#94)
Browse files Browse the repository at this point in the history
Adds a `ErrorTracker.Filter` behavior that users can supply, it has a
single function `sanitize/1` that takes an error context and returns a
new error context which will be saved in the DB.

Closes #88
  • Loading branch information
MSE99 authored Oct 12, 2024
1 parent a15639a commit fd552ad
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 1 deletion.
12 changes: 11 additions & 1 deletion lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions lib/error_tracker/filter.ex
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions test/error_tracker/filter_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit fd552ad

Please sign in to comment.