Skip to content

Commit

Permalink
Fix event data that is not JSON-encodable (#602)
Browse files Browse the repository at this point in the history
Closes #521.
  • Loading branch information
whatyouhide authored Sep 3, 2023
1 parent 601d330 commit 2ceee54
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 4 deletions.
46 changes: 46 additions & 0 deletions lib/sentry/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,16 @@ defmodule Sentry.Client do

@spec render_event(Event.t()) :: map()
def render_event(%Event{} = event) do
json_library = Config.json_library()

event
|> Map.from_struct()
|> update_if_present(:message, &String.slice(&1, 0, @max_message_length))
|> update_if_present(:breadcrumbs, fn bcs -> Enum.map(bcs, &Map.from_struct/1) end)
|> update_if_present(:sdk, &Map.from_struct/1)
|> update_if_present(:extra, &sanitize_non_jsonable_values(&1, json_library))
|> update_if_present(:user, &sanitize_non_jsonable_values(&1, json_library))
|> update_if_present(:tags, &sanitize_non_jsonable_values(&1, json_library))
|> update_if_present(:exception, &[render_exception(&1)])
|> Map.drop([:__source__, :__original_exception__])
end
Expand All @@ -121,6 +126,47 @@ defmodule Sentry.Client do
end)
end

defp sanitize_non_jsonable_values(map, json_library) do
# We update the existing map instead of building a new one from scratch
# due to performance reasons. See the docs for :maps.map/2.
Enum.reduce(map, map, fn {key, value}, acc ->
case sanitize_non_jsonable_value(value, json_library) do
:unchanged -> acc
{:changed, value} -> Map.put(acc, key, value)
end
end)
end

# For performance, skip all the keys that we know for sure are JSON encodable.
defp sanitize_non_jsonable_value(value, _json_library)
when is_binary(value) or is_number(value) or is_boolean(value) or is_nil(value) do
:unchanged
end

defp sanitize_non_jsonable_value(value, json_library) when is_list(value) do
mapped =
Enum.map(value, fn value ->
case sanitize_non_jsonable_value(value, json_library) do
:unchanged -> value
{:changed, value} -> value
end
end)

{:changed, mapped}
end

defp sanitize_non_jsonable_value(value, json_library)
when is_map(value) and not is_struct(value) do
{:changed, sanitize_non_jsonable_values(value, json_library)}
end

defp sanitize_non_jsonable_value(value, json_library) do
case json_library.encode(value) do
{:ok, _encoded} -> :unchanged
{:error, _reason} -> {:changed, inspect(value)}
end
end

defp update_if_present(map, key, fun) do
case Map.pop(map, key) do
{nil, _} -> map
Expand Down
2 changes: 1 addition & 1 deletion test/logger_backend_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ defmodule Sentry.LoggerBackendTest do
{:ok, _plug_pid} = Plug.Cowboy.http(Sentry.ExamplePlugApplication, [], port: 8003)

:hackney.get("http://127.0.0.1:8003/error_route", [], "", [])
assert_receive {^ref, _event}
assert_receive {^ref, _event}, 1000
after
:ok = Plug.Cowboy.shutdown(Sentry.ExamplePlugApplication.HTTP)
Logger.configure_backend(Sentry.LoggerBackend, excluded_domains: [:cowboy])
Expand Down
44 changes: 41 additions & 3 deletions test/sentry/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,47 @@ defmodule Sentry.ClientTest do

describe "render_event/1" do
test "transforms structs into maps" do
event = Sentry.Event.transform_exception(%RuntimeError{message: "foo"}, user: %{id: 1})
event = Event.transform_exception(%RuntimeError{message: "foo"}, user: %{id: 1})

assert %{
user: %{id: 1},
exception: [%{type: "RuntimeError", value: "foo"}],
sdk: %{name: "sentry-elixir"}
} = Client.render_event(event)
end

test "truncates the message to a max length" do
max_length = 8_192
event = Event.create_event(message: String.duplicate("a", max_length + 1))
assert Client.render_event(event).message == String.duplicate("a", max_length)
end

test "safely inspects terms that cannot be converted to JSON" do
event =
Event.create_event(
extra: %{
valid: "yes",
self: self(),
keyword: [key: "value"],
nested: %{self: self()}
},
user: %{id: "valid-ID", email: {"user", "@example.com"}},
tags: %{valid: "yes", tokens: MapSet.new([1])}
)

rendered = Client.render_event(event)

assert rendered.extra.valid == "yes"
assert rendered.extra.self == inspect(self())
assert rendered.extra.keyword == [~s({:key, "value"})]
assert rendered.extra.nested.self == inspect(self())

assert rendered.user.id == "valid-ID"
assert rendered.user.email == ~s({"user", "@example.com"})

assert rendered.tags.valid == "yes"
assert rendered.tags.tokens == inspect(MapSet.new([1]))
end
end

describe "send_event/2" do
Expand Down Expand Up @@ -154,8 +187,13 @@ defmodule Sentry.ClientTest do
end

test "logs an error when unable to encode JSON" do
event =
Event.create_event(message: "Something went wrong", extra: %{metadata: [keyword: "list"]})
defmodule BadJSONClient do
def encode(_term), do: {:error, :im_just_bad}
end

modify_env(:sentry, json_library: BadJSONClient)

event = Event.create_event(message: "Something went wrong")

assert capture_log(fn ->
Client.send_event(event, result: :sync)
Expand Down

0 comments on commit 2ceee54

Please sign in to comment.