From 4454612de0521408a6044ef928863ed3c074c7c0 Mon Sep 17 00:00:00 2001 From: Jonathan Moraes Date: Mon, 30 Dec 2024 16:32:52 +0100 Subject: [PATCH 1/7] Allow the usage of JSON for Elixir 1.18+ --- config/config.exs | 10 ++++- lib/sentry/client.ex | 12 ++---- lib/sentry/config.ex | 15 +++---- lib/sentry/envelope.ex | 39 ++++++------------- lib/sentry/transport.ex | 9 ++--- test/envelope_test.exs | 28 ++++++------- test/mix/sentry.package_source_code_test.exs | 4 +- test/plug_capture_test.exs | 4 +- test/sentry/client_test.exs | 14 +++---- test/sentry/config_test.exs | 2 +- test/sentry/transport_test.exs | 10 +++-- test/support/example_plug_application.ex | 4 +- test/support/test_error_view.ex | 5 ++- test/support/test_helpers.ex | 9 +++-- .../phoenix_app/test/support/test_helpers.ex | 6 +-- 15 files changed, 82 insertions(+), 89 deletions(-) diff --git a/config/config.exs b/config/config.exs index 1184ea49..579c4402 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,8 +1,16 @@ import Config +json_library = + if Version.compare(System.version(), "1.18.0") == :lt do + Jason + else + JSON + end + if config_env() == :test do config :sentry, environment_name: :test, + json_library: json_library, tags: %{}, enable_source_code_context: true, root_source_code_paths: [File.cwd!()], @@ -15,4 +23,4 @@ if config_env() == :test do config :logger, backends: [] end -config :phoenix, :json_library, Jason +config :phoenix, :json_library, json_library diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index 87c124fc..f21c3c51 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -294,14 +294,10 @@ defmodule Sentry.Client do end defp sanitize_non_jsonable_value(value, json_library) do - try do - json_library.encode(value) - catch - _type, _reason -> {:changed, inspect(value)} - else - {:ok, _encoded} -> :unchanged - {:error, _reason} -> {:changed, inspect(value)} - end + json_library.encode!(value) + :unchanged + rescue + _error -> {:changed, inspect(value)} end defp update_if_present(map, key, fun) do diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index a2407512..61f8b3b2 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -152,7 +152,7 @@ defmodule Sentry.Config do type_doc: "`t:module/0`", doc: """ A module that implements the "standard" Elixir JSON behaviour, that is, exports the - `encode/1` and `decode/1` functions. If you use the default, make sure to add + `encode!/1` and `decode!/1` functions. If you use the default, make sure to add [`:jason`](https://hex.pm/packages/jason) as a dependency of your application. """ ], @@ -695,19 +695,14 @@ defmodule Sentry.Config do def __validate_json_library__(mod) when is_atom(mod) do try do - with {:ok, %{}} <- mod.decode("{}"), - {:ok, "{}"} <- mod.encode(%{}) do - {:ok, mod} - else - _ -> - {:error, - "configured :json_library #{inspect(mod)} does not implement decode/1 and encode/1"} - end + %{} = mod.decode!("{}") + "{}" = mod.encode!(%{}) + {:ok, mod} rescue UndefinedFunctionError -> {:error, """ - configured :json_library #{inspect(mod)} is not available or does not implement decode/1 and encode/1. + configured :json_library #{inspect(mod)} is not available or does not implement decode!/1 and encode!/1. Do you need to add #{inspect(mod)} to your mix.exs? """} end diff --git a/lib/sentry/envelope.ex b/lib/sentry/envelope.ex index 649f6e85..d28c1e58 100644 --- a/lib/sentry/envelope.ex +++ b/lib/sentry/envelope.ex @@ -76,19 +76,14 @@ defmodule Sentry.Envelope do items_iodata = Enum.map(envelope.items, &item_to_binary(json_library, &1)) {:ok, IO.iodata_to_binary([headers_iodata, items_iodata])} - catch - {:error, _reason} = error -> error + rescue + error -> {:error, error} end defp item_to_binary(json_library, %Event{} = event) do - case event |> Sentry.Client.render_event() |> json_library.encode() do - {:ok, encoded_event} -> - header = ~s({"type":"event","length":#{byte_size(encoded_event)}}) - [header, ?\n, encoded_event, ?\n] - - {:error, _reason} = error -> - throw(error) - end + encoded_event = event |> Sentry.Client.render_event() |> json_library.encode!() + header = ~s({"type":"event","length":#{byte_size(encoded_event)}}) + [header, ?\n, encoded_event, ?\n] end defp item_to_binary(json_library, %Attachment{} = attachment) do @@ -100,30 +95,20 @@ defmodule Sentry.Envelope do into: header, do: {Atom.to_string(key), value} - {:ok, header_iodata} = json_library.encode(header) + header_iodata = json_library.encode!(header) [header_iodata, ?\n, attachment.data, ?\n] end defp item_to_binary(json_library, %CheckIn{} = check_in) do - case check_in |> CheckIn.to_map() |> json_library.encode() do - {:ok, encoded_check_in} -> - header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}}) - [header, ?\n, encoded_check_in, ?\n] - - {:error, _reason} = error -> - throw(error) - end + encoded_check_in = check_in |> CheckIn.to_map() |> json_library.encode!() + header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}}) + [header, ?\n, encoded_check_in, ?\n] end defp item_to_binary(json_library, %ClientReport{} = client_report) do - case client_report |> Map.from_struct() |> json_library.encode() do - {:ok, encoded_client_report} -> - header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}}) - [header, ?\n, encoded_client_report, ?\n] - - {:error, _reason} = error -> - throw(error) - end + encoded_client_report = client_report |> Map.from_struct() |> json_library.encode!() + header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}}) + [header, ?\n, encoded_client_report, ?\n] end end diff --git a/lib/sentry/transport.ex b/lib/sentry/transport.ex index a353b2b8..4c94387d 100644 --- a/lib/sentry/transport.ex +++ b/lib/sentry/transport.ex @@ -93,11 +93,10 @@ defmodule Sentry.Transport do end defp request(client, endpoint, headers, body) do - with {:ok, 200, _headers, body} <- - client_post_and_validate_return_value(client, endpoint, headers, body), - {:ok, json} <- Config.json_library().decode(body) do - {:ok, Map.get(json, "id")} - else + case client_post_and_validate_return_value(client, endpoint, headers, body) do + {:ok, 200, _headers, body} -> + {:ok, body |> Config.json_library().decode!() |> Map.get("id")} + {:ok, 429, headers, _body} -> delay_ms = with timeout when is_binary(timeout) <- diff --git a/test/envelope_test.exs b/test/envelope_test.exs index dd887d3d..702e3eee 100644 --- a/test/envelope_test.exs +++ b/test/envelope_test.exs @@ -15,10 +15,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert Jason.decode!(id_line) == %{"event_id" => event.event_id} - assert %{"type" => "event", "length" => _} = Jason.decode!(header_line) + assert json_library().decode!(id_line) == %{"event_id" => event.event_id} + assert %{"type" => "event", "length" => _} = json_library().decode!(header_line) - assert {:ok, decoded_event} = Jason.decode(event_line) + assert {:ok, decoded_event} = json_library().decode(event_line) assert decoded_event["event_id"] == event.event_id assert decoded_event["breadcrumbs"] == [] assert decoded_event["environment"] == "test" @@ -65,29 +65,29 @@ defmodule Sentry.EnvelopeTest do "..." ] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) + assert %{"event_id" => _} = json_library().decode!(id_line) - assert Jason.decode!(attachment1_header) == %{ + assert json_library().decode!(attachment1_header) == %{ "type" => "attachment", "length" => 3, "filename" => "example.dat" } - assert Jason.decode!(attachment2_header) == %{ + assert json_library().decode!(attachment2_header) == %{ "type" => "attachment", "length" => 6, "filename" => "example.txt", "content_type" => "text/plain" } - assert Jason.decode!(attachment3_header) == %{ + assert json_library().decode!(attachment3_header) == %{ "type" => "attachment", "length" => 2, "filename" => "example.json", "content_type" => "application/json" } - assert Jason.decode!(attachment4_header) == %{ + assert json_library().decode!(attachment4_header) == %{ "type" => "attachment", "length" => 3, "filename" => "dump", @@ -105,10 +105,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) - assert %{"type" => "check_in", "length" => _} = Jason.decode!(header_line) + assert %{"event_id" => _} = json_library().decode!(id_line) + assert %{"type" => "check_in", "length" => _} = json_library().decode!(header_line) - assert {:ok, decoded_check_in} = Jason.decode(event_line) + assert {:ok, decoded_check_in} = json_library().decode(event_line) assert decoded_check_in["check_in_id"] == check_in_id assert decoded_check_in["monitor_slug"] == "test" assert decoded_check_in["status"] == "ok" @@ -128,10 +128,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) - assert %{"type" => "client_report", "length" => _} = Jason.decode!(header_line) + assert %{"event_id" => _} = json_library().decode!(id_line) + assert %{"type" => "client_report", "length" => _} = json_library().decode!(header_line) - assert {:ok, decoded_client_report} = Jason.decode(event_line) + assert {:ok, decoded_client_report} = json_library().decode(event_line) assert decoded_client_report["timestamp"] == client_report.timestamp assert decoded_client_report["discarded_events"] == [ diff --git a/test/mix/sentry.package_source_code_test.exs b/test/mix/sentry.package_source_code_test.exs index f60474b9..bf7e722b 100644 --- a/test/mix/sentry.package_source_code_test.exs +++ b/test/mix/sentry.package_source_code_test.exs @@ -66,8 +66,8 @@ defmodule Mix.Tasks.Sentry.PackageSourceCodeTest do # "loadpaths" and "compile" to the dependencies of this Mix task. test "supports custom configured :json_library" do defmodule Sentry.ExampleJSON do - defdelegate encode(term), to: Jason - defdelegate decode(term), to: Jason + defdelegate encode!(term), to: json_library() + defdelegate decode!(term), to: json_library() end put_test_config(json_library: Sentry.ExampleJSON) diff --git a/test/plug_capture_test.exs b/test/plug_capture_test.exs index 226fc2cf..8480483f 100644 --- a/test/plug_capture_test.exs +++ b/test/plug_capture_test.exs @@ -29,7 +29,7 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_library() plug Sentry.PlugContext plug PhoenixRouter end @@ -45,7 +45,7 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_library() plug Sentry.PlugContext plug PhoenixRouter end diff --git a/test/sentry/client_test.exs b/test/sentry/client_test.exs index 8e11ab73..fa46997e 100644 --- a/test/sentry/client_test.exs +++ b/test/sentry/client_test.exs @@ -67,10 +67,10 @@ defmodule Sentry.ClientTest do test "works if the JSON library crashes" do defmodule RaisingJSONClient do - def encode(:crash), do: raise("Oops") - def encode(term), do: Jason.encode(term) + def encode!(:crash), do: raise("Oops") + def encode!(term), do: json_library().encode!(term) - def decode(term), do: Jason.decode(term) + def decode!(term), do: json_library().decode!(term) end put_test_config(json_library: RaisingJSONClient) @@ -336,10 +336,10 @@ defmodule Sentry.ClientTest do test "logs an error when unable to encode JSON" do defmodule BadJSONClient do - def encode(term) when term == %{}, do: {:ok, "{}"} - def encode(_term), do: {:error, :im_just_bad} + def encode!(term) when term == %{}, do: "{}" + def encode!(_term), do: raise("im_just_bad") - def decode(term), do: Jason.decode(term) + def decode!(term), do: json_library().decode!(term) end put_test_config(json_library: BadJSONClient) @@ -347,7 +347,7 @@ defmodule Sentry.ClientTest do assert capture_log(fn -> Client.send_event(event, result: :sync) - end) =~ "the Sentry SDK could not encode the event to JSON: :im_just_bad" + end) =~ "the Sentry SDK could not encode the event to JSON: im_just_bad" end test "uses the async sender pool when :result is :none", %{bypass: bypass} do diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 3637c712..04e64f06 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -158,7 +158,7 @@ defmodule Sentry.ConfigTest do end test ":json_library" do - assert Config.validate!(json_library: Jason)[:json_library] == Jason + assert Config.validate!(json_library: JSON)[:json_library] == JSON # Default assert Config.validate!([])[:json_library] == Jason diff --git a/test/sentry/transport_test.exs b/test/sentry/transport_test.exs index 0b7d7e85..10a64507 100644 --- a/test/sentry/transport_test.exs +++ b/test/sentry/transport_test.exs @@ -158,10 +158,10 @@ defmodule Sentry.TransportTest do envelope = Envelope.from_event(Event.create_event(message: "Hello")) defmodule CrashingJSONLibrary do - defdelegate encode(term), to: Jason + defdelegate encode!(term), to: json_library() - def decode("{}"), do: {:ok, %{}} - def decode(_body), do: raise("I'm a really bad JSON library") + def decode!("{}"), do: %{} + def decode!(_body), do: raise("I'm a really bad JSON library") end Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn -> @@ -190,7 +190,9 @@ defmodule Sentry.TransportTest do Plug.Conn.resp(conn, 200, ~s) end) - assert {:request_failure, %Jason.DecodeError{}} = + exception = Module.concat(json_library(), DecodeError) + + assert {:error, %^exception{}, _stacktrace} = error(fn -> Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [0]) end) diff --git a/test/support/example_plug_application.ex b/test/support/example_plug_application.ex index ee6e88c5..082ffe1a 100644 --- a/test/support/example_plug_application.ex +++ b/test/support/example_plug_application.ex @@ -5,6 +5,8 @@ defmodule Sentry.ExamplePlugApplication do import ExUnit.Assertions + alias Sentry.Config + plug Plug.Parsers, parsers: [:multipart, :urlencoded] plug Sentry.PlugContext plug :match @@ -50,7 +52,7 @@ defmodule Sentry.ExamplePlugApplication do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Jason.encode!() + |> Config.json_library().encode!() """ diff --git a/test/support/test_error_view.ex b/test/support/test_error_view.ex index 29dcd7e4..473c6511 100644 --- a/test/support/test_error_view.ex +++ b/test/support/test_error_view.ex @@ -1,14 +1,17 @@ defmodule Sentry.ErrorView do use Phoenix.Component + import Phoenix.HTML, only: [raw: 1] + alias Sentry.Config + def render(_, _) do case Sentry.get_last_event_id_and_source() do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Jason.encode!() + |> Config.json_library().encode!() assigns = %{opts: opts} diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index eca6a617..3d45703a 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -3,6 +3,9 @@ defmodule Sentry.TestHelpers do alias Sentry.Config + @spec json_library :: module() + def json_library, do: Config.json_library() + @spec put_test_config(keyword()) :: :ok def put_test_config(config) when is_list(config) do all_original_config = all_config() @@ -46,7 +49,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) + %{"event_id" => _} = json_library().decode!(id_line) decode_envelope_items(rest) end @@ -55,8 +58,8 @@ defmodule Sentry.TestHelpers do |> Enum.chunk_every(2) |> Enum.flat_map(fn [header, item] -> - {:ok, header} = Config.json_library().decode(header) - {:ok, item} = Config.json_library().decode(item) + header = json_library().decode!(header) + item = json_library().decode!(item) [{header, item}] [""] -> diff --git a/test_integrations/phoenix_app/test/support/test_helpers.ex b/test_integrations/phoenix_app/test/support/test_helpers.ex index eca6a617..7cdfede7 100644 --- a/test_integrations/phoenix_app/test/support/test_helpers.ex +++ b/test_integrations/phoenix_app/test/support/test_helpers.ex @@ -46,7 +46,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) + %{"event_id" => _} = Config.json_library().decode!(id_line) decode_envelope_items(rest) end @@ -55,8 +55,8 @@ defmodule Sentry.TestHelpers do |> Enum.chunk_every(2) |> Enum.flat_map(fn [header, item] -> - {:ok, header} = Config.json_library().decode(header) - {:ok, item} = Config.json_library().decode(item) + header = Config.json_library().decode!(header) + item = Config.json_library().decode!(item) [{header, item}] [""] -> From 217cbdfd24989656de79c9ae4caa063c24c2642a Mon Sep 17 00:00:00 2001 From: Jonathan Moraes Date: Thu, 2 Jan 2025 09:11:02 +0100 Subject: [PATCH 2/7] Revert "Allow the usage of JSON for Elixir 1.18+" This reverts commit 4454612de0521408a6044ef928863ed3c074c7c0. --- config/config.exs | 10 +---- lib/sentry/client.ex | 12 ++++-- lib/sentry/config.ex | 15 ++++--- lib/sentry/envelope.ex | 39 +++++++++++++------ lib/sentry/transport.ex | 9 +++-- test/envelope_test.exs | 28 ++++++------- test/mix/sentry.package_source_code_test.exs | 4 +- test/plug_capture_test.exs | 4 +- test/sentry/client_test.exs | 14 +++---- test/sentry/config_test.exs | 2 +- test/sentry/transport_test.exs | 10 ++--- test/support/example_plug_application.ex | 4 +- test/support/test_error_view.ex | 5 +-- test/support/test_helpers.ex | 9 ++--- .../phoenix_app/test/support/test_helpers.ex | 6 +-- 15 files changed, 89 insertions(+), 82 deletions(-) diff --git a/config/config.exs b/config/config.exs index 579c4402..1184ea49 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,16 +1,8 @@ import Config -json_library = - if Version.compare(System.version(), "1.18.0") == :lt do - Jason - else - JSON - end - if config_env() == :test do config :sentry, environment_name: :test, - json_library: json_library, tags: %{}, enable_source_code_context: true, root_source_code_paths: [File.cwd!()], @@ -23,4 +15,4 @@ if config_env() == :test do config :logger, backends: [] end -config :phoenix, :json_library, json_library +config :phoenix, :json_library, Jason diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index f21c3c51..87c124fc 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -294,10 +294,14 @@ defmodule Sentry.Client do end defp sanitize_non_jsonable_value(value, json_library) do - json_library.encode!(value) - :unchanged - rescue - _error -> {:changed, inspect(value)} + try do + json_library.encode(value) + catch + _type, _reason -> {:changed, inspect(value)} + else + {:ok, _encoded} -> :unchanged + {:error, _reason} -> {:changed, inspect(value)} + end end defp update_if_present(map, key, fun) do diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 61f8b3b2..a2407512 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -152,7 +152,7 @@ defmodule Sentry.Config do type_doc: "`t:module/0`", doc: """ A module that implements the "standard" Elixir JSON behaviour, that is, exports the - `encode!/1` and `decode!/1` functions. If you use the default, make sure to add + `encode/1` and `decode/1` functions. If you use the default, make sure to add [`:jason`](https://hex.pm/packages/jason) as a dependency of your application. """ ], @@ -695,14 +695,19 @@ defmodule Sentry.Config do def __validate_json_library__(mod) when is_atom(mod) do try do - %{} = mod.decode!("{}") - "{}" = mod.encode!(%{}) - {:ok, mod} + with {:ok, %{}} <- mod.decode("{}"), + {:ok, "{}"} <- mod.encode(%{}) do + {:ok, mod} + else + _ -> + {:error, + "configured :json_library #{inspect(mod)} does not implement decode/1 and encode/1"} + end rescue UndefinedFunctionError -> {:error, """ - configured :json_library #{inspect(mod)} is not available or does not implement decode!/1 and encode!/1. + configured :json_library #{inspect(mod)} is not available or does not implement decode/1 and encode/1. Do you need to add #{inspect(mod)} to your mix.exs? """} end diff --git a/lib/sentry/envelope.ex b/lib/sentry/envelope.ex index d28c1e58..649f6e85 100644 --- a/lib/sentry/envelope.ex +++ b/lib/sentry/envelope.ex @@ -76,14 +76,19 @@ defmodule Sentry.Envelope do items_iodata = Enum.map(envelope.items, &item_to_binary(json_library, &1)) {:ok, IO.iodata_to_binary([headers_iodata, items_iodata])} - rescue - error -> {:error, error} + catch + {:error, _reason} = error -> error end defp item_to_binary(json_library, %Event{} = event) do - encoded_event = event |> Sentry.Client.render_event() |> json_library.encode!() - header = ~s({"type":"event","length":#{byte_size(encoded_event)}}) - [header, ?\n, encoded_event, ?\n] + case event |> Sentry.Client.render_event() |> json_library.encode() do + {:ok, encoded_event} -> + header = ~s({"type":"event","length":#{byte_size(encoded_event)}}) + [header, ?\n, encoded_event, ?\n] + + {:error, _reason} = error -> + throw(error) + end end defp item_to_binary(json_library, %Attachment{} = attachment) do @@ -95,20 +100,30 @@ defmodule Sentry.Envelope do into: header, do: {Atom.to_string(key), value} - header_iodata = json_library.encode!(header) + {:ok, header_iodata} = json_library.encode(header) [header_iodata, ?\n, attachment.data, ?\n] end defp item_to_binary(json_library, %CheckIn{} = check_in) do - encoded_check_in = check_in |> CheckIn.to_map() |> json_library.encode!() - header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}}) - [header, ?\n, encoded_check_in, ?\n] + case check_in |> CheckIn.to_map() |> json_library.encode() do + {:ok, encoded_check_in} -> + header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}}) + [header, ?\n, encoded_check_in, ?\n] + + {:error, _reason} = error -> + throw(error) + end end defp item_to_binary(json_library, %ClientReport{} = client_report) do - encoded_client_report = client_report |> Map.from_struct() |> json_library.encode!() - header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}}) - [header, ?\n, encoded_client_report, ?\n] + case client_report |> Map.from_struct() |> json_library.encode() do + {:ok, encoded_client_report} -> + header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}}) + [header, ?\n, encoded_client_report, ?\n] + + {:error, _reason} = error -> + throw(error) + end end end diff --git a/lib/sentry/transport.ex b/lib/sentry/transport.ex index 4c94387d..a353b2b8 100644 --- a/lib/sentry/transport.ex +++ b/lib/sentry/transport.ex @@ -93,10 +93,11 @@ defmodule Sentry.Transport do end defp request(client, endpoint, headers, body) do - case client_post_and_validate_return_value(client, endpoint, headers, body) do - {:ok, 200, _headers, body} -> - {:ok, body |> Config.json_library().decode!() |> Map.get("id")} - + with {:ok, 200, _headers, body} <- + client_post_and_validate_return_value(client, endpoint, headers, body), + {:ok, json} <- Config.json_library().decode(body) do + {:ok, Map.get(json, "id")} + else {:ok, 429, headers, _body} -> delay_ms = with timeout when is_binary(timeout) <- diff --git a/test/envelope_test.exs b/test/envelope_test.exs index 702e3eee..dd887d3d 100644 --- a/test/envelope_test.exs +++ b/test/envelope_test.exs @@ -15,10 +15,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert json_library().decode!(id_line) == %{"event_id" => event.event_id} - assert %{"type" => "event", "length" => _} = json_library().decode!(header_line) + assert Jason.decode!(id_line) == %{"event_id" => event.event_id} + assert %{"type" => "event", "length" => _} = Jason.decode!(header_line) - assert {:ok, decoded_event} = json_library().decode(event_line) + assert {:ok, decoded_event} = Jason.decode(event_line) assert decoded_event["event_id"] == event.event_id assert decoded_event["breadcrumbs"] == [] assert decoded_event["environment"] == "test" @@ -65,29 +65,29 @@ defmodule Sentry.EnvelopeTest do "..." ] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = json_library().decode!(id_line) + assert %{"event_id" => _} = Jason.decode!(id_line) - assert json_library().decode!(attachment1_header) == %{ + assert Jason.decode!(attachment1_header) == %{ "type" => "attachment", "length" => 3, "filename" => "example.dat" } - assert json_library().decode!(attachment2_header) == %{ + assert Jason.decode!(attachment2_header) == %{ "type" => "attachment", "length" => 6, "filename" => "example.txt", "content_type" => "text/plain" } - assert json_library().decode!(attachment3_header) == %{ + assert Jason.decode!(attachment3_header) == %{ "type" => "attachment", "length" => 2, "filename" => "example.json", "content_type" => "application/json" } - assert json_library().decode!(attachment4_header) == %{ + assert Jason.decode!(attachment4_header) == %{ "type" => "attachment", "length" => 3, "filename" => "dump", @@ -105,10 +105,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = json_library().decode!(id_line) - assert %{"type" => "check_in", "length" => _} = json_library().decode!(header_line) + assert %{"event_id" => _} = Jason.decode!(id_line) + assert %{"type" => "check_in", "length" => _} = Jason.decode!(header_line) - assert {:ok, decoded_check_in} = json_library().decode(event_line) + assert {:ok, decoded_check_in} = Jason.decode(event_line) assert decoded_check_in["check_in_id"] == check_in_id assert decoded_check_in["monitor_slug"] == "test" assert decoded_check_in["status"] == "ok" @@ -128,10 +128,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = json_library().decode!(id_line) - assert %{"type" => "client_report", "length" => _} = json_library().decode!(header_line) + assert %{"event_id" => _} = Jason.decode!(id_line) + assert %{"type" => "client_report", "length" => _} = Jason.decode!(header_line) - assert {:ok, decoded_client_report} = json_library().decode(event_line) + assert {:ok, decoded_client_report} = Jason.decode(event_line) assert decoded_client_report["timestamp"] == client_report.timestamp assert decoded_client_report["discarded_events"] == [ diff --git a/test/mix/sentry.package_source_code_test.exs b/test/mix/sentry.package_source_code_test.exs index bf7e722b..f60474b9 100644 --- a/test/mix/sentry.package_source_code_test.exs +++ b/test/mix/sentry.package_source_code_test.exs @@ -66,8 +66,8 @@ defmodule Mix.Tasks.Sentry.PackageSourceCodeTest do # "loadpaths" and "compile" to the dependencies of this Mix task. test "supports custom configured :json_library" do defmodule Sentry.ExampleJSON do - defdelegate encode!(term), to: json_library() - defdelegate decode!(term), to: json_library() + defdelegate encode(term), to: Jason + defdelegate decode(term), to: Jason end put_test_config(json_library: Sentry.ExampleJSON) diff --git a/test/plug_capture_test.exs b/test/plug_capture_test.exs index 8480483f..226fc2cf 100644 --- a/test/plug_capture_test.exs +++ b/test/plug_capture_test.exs @@ -29,7 +29,7 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_library() + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason plug Sentry.PlugContext plug PhoenixRouter end @@ -45,7 +45,7 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_library() + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason plug Sentry.PlugContext plug PhoenixRouter end diff --git a/test/sentry/client_test.exs b/test/sentry/client_test.exs index fa46997e..8e11ab73 100644 --- a/test/sentry/client_test.exs +++ b/test/sentry/client_test.exs @@ -67,10 +67,10 @@ defmodule Sentry.ClientTest do test "works if the JSON library crashes" do defmodule RaisingJSONClient do - def encode!(:crash), do: raise("Oops") - def encode!(term), do: json_library().encode!(term) + def encode(:crash), do: raise("Oops") + def encode(term), do: Jason.encode(term) - def decode!(term), do: json_library().decode!(term) + def decode(term), do: Jason.decode(term) end put_test_config(json_library: RaisingJSONClient) @@ -336,10 +336,10 @@ defmodule Sentry.ClientTest do test "logs an error when unable to encode JSON" do defmodule BadJSONClient do - def encode!(term) when term == %{}, do: "{}" - def encode!(_term), do: raise("im_just_bad") + def encode(term) when term == %{}, do: {:ok, "{}"} + def encode(_term), do: {:error, :im_just_bad} - def decode!(term), do: json_library().decode!(term) + def decode(term), do: Jason.decode(term) end put_test_config(json_library: BadJSONClient) @@ -347,7 +347,7 @@ defmodule Sentry.ClientTest do assert capture_log(fn -> Client.send_event(event, result: :sync) - end) =~ "the Sentry SDK could not encode the event to JSON: im_just_bad" + end) =~ "the Sentry SDK could not encode the event to JSON: :im_just_bad" end test "uses the async sender pool when :result is :none", %{bypass: bypass} do diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 04e64f06..3637c712 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -158,7 +158,7 @@ defmodule Sentry.ConfigTest do end test ":json_library" do - assert Config.validate!(json_library: JSON)[:json_library] == JSON + assert Config.validate!(json_library: Jason)[:json_library] == Jason # Default assert Config.validate!([])[:json_library] == Jason diff --git a/test/sentry/transport_test.exs b/test/sentry/transport_test.exs index 10a64507..0b7d7e85 100644 --- a/test/sentry/transport_test.exs +++ b/test/sentry/transport_test.exs @@ -158,10 +158,10 @@ defmodule Sentry.TransportTest do envelope = Envelope.from_event(Event.create_event(message: "Hello")) defmodule CrashingJSONLibrary do - defdelegate encode!(term), to: json_library() + defdelegate encode(term), to: Jason - def decode!("{}"), do: %{} - def decode!(_body), do: raise("I'm a really bad JSON library") + def decode("{}"), do: {:ok, %{}} + def decode(_body), do: raise("I'm a really bad JSON library") end Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn -> @@ -190,9 +190,7 @@ defmodule Sentry.TransportTest do Plug.Conn.resp(conn, 200, ~s) end) - exception = Module.concat(json_library(), DecodeError) - - assert {:error, %^exception{}, _stacktrace} = + assert {:request_failure, %Jason.DecodeError{}} = error(fn -> Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [0]) end) diff --git a/test/support/example_plug_application.ex b/test/support/example_plug_application.ex index 082ffe1a..ee6e88c5 100644 --- a/test/support/example_plug_application.ex +++ b/test/support/example_plug_application.ex @@ -5,8 +5,6 @@ defmodule Sentry.ExamplePlugApplication do import ExUnit.Assertions - alias Sentry.Config - plug Plug.Parsers, parsers: [:multipart, :urlencoded] plug Sentry.PlugContext plug :match @@ -52,7 +50,7 @@ defmodule Sentry.ExamplePlugApplication do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Config.json_library().encode!() + |> Jason.encode!() """ diff --git a/test/support/test_error_view.ex b/test/support/test_error_view.ex index 473c6511..29dcd7e4 100644 --- a/test/support/test_error_view.ex +++ b/test/support/test_error_view.ex @@ -1,17 +1,14 @@ defmodule Sentry.ErrorView do use Phoenix.Component - import Phoenix.HTML, only: [raw: 1] - alias Sentry.Config - def render(_, _) do case Sentry.get_last_event_id_and_source() do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Config.json_library().encode!() + |> Jason.encode!() assigns = %{opts: opts} diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index 3d45703a..eca6a617 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -3,9 +3,6 @@ defmodule Sentry.TestHelpers do alias Sentry.Config - @spec json_library :: module() - def json_library, do: Config.json_library() - @spec put_test_config(keyword()) :: :ok def put_test_config(config) when is_list(config) do all_original_config = all_config() @@ -49,7 +46,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - %{"event_id" => _} = json_library().decode!(id_line) + {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) decode_envelope_items(rest) end @@ -58,8 +55,8 @@ defmodule Sentry.TestHelpers do |> Enum.chunk_every(2) |> Enum.flat_map(fn [header, item] -> - header = json_library().decode!(header) - item = json_library().decode!(item) + {:ok, header} = Config.json_library().decode(header) + {:ok, item} = Config.json_library().decode(item) [{header, item}] [""] -> diff --git a/test_integrations/phoenix_app/test/support/test_helpers.ex b/test_integrations/phoenix_app/test/support/test_helpers.ex index 7cdfede7..eca6a617 100644 --- a/test_integrations/phoenix_app/test/support/test_helpers.ex +++ b/test_integrations/phoenix_app/test/support/test_helpers.ex @@ -46,7 +46,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - %{"event_id" => _} = Config.json_library().decode!(id_line) + {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) decode_envelope_items(rest) end @@ -55,8 +55,8 @@ defmodule Sentry.TestHelpers do |> Enum.chunk_every(2) |> Enum.flat_map(fn [header, item] -> - header = Config.json_library().decode!(header) - item = Config.json_library().decode!(item) + {:ok, header} = Config.json_library().decode(header) + {:ok, item} = Config.json_library().decode(item) [{header, item}] [""] -> From cf6285d7c4a5c4ebac6686e46cc687b6236a0736 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Fri, 3 Jan 2025 10:45:47 +0100 Subject: [PATCH 3/7] Fixes --- README.md | 19 +++++++++-- config/config.exs | 2 +- lib/sentry/client.ex | 2 +- lib/sentry/config.ex | 14 ++++++-- lib/sentry/envelope.ex | 8 ++--- lib/sentry/json.ex | 33 +++++++++++++++++++ lib/sentry/transport.ex | 2 +- pages/setup-with-plug-and-phoenix.md | 2 +- test/envelope_test.exs | 28 ++++++++-------- test/plug_capture_test.exs | 8 +++-- test/sentry/config_test.exs | 6 +++- test/sentry/json_test.exs | 32 ++++++++++++++++++ test/sentry/transport_test.exs | 8 ++++- test/support/example_plug_application.ex | 4 ++- test/support/test_error_view.ex | 4 ++- test/support/test_helpers.ex | 22 ++++++++----- .../phoenix_app/config/config.exs | 3 +- .../phoenix_app/test/support/test_helpers.ex | 22 ++++++++----- 18 files changed, 167 insertions(+), 52 deletions(-) create mode 100644 lib/sentry/json.ex create mode 100644 test/sentry/json_test.exs diff --git a/README.md b/README.md index 4dbd358f..000c5259 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ This is the official Sentry SDK for [Sentry]. ### Install -To use Sentry in your project, add it as a dependency in your `mix.exs` file. Sentry does not install a JSON library nor HTTP client by itself. Sentry will default to trying to use [Jason] for JSON serialization and [Hackney] for HTTP requests, but can be configured to use other ones. To use the default ones, do: +To use Sentry in your project, add it as a dependency in your `mix.exs` file. + +Sentry does not install a JSON library nor a HTTP client by itself. Sentry will default to the [built-in `JSON`](https://hexdocs.pm/elixir/JSON.html) for JSON and [Hackney] for HTTP requests, but can be configured to use other ones. To use the default ones, do: ```elixir defp deps do @@ -26,12 +28,24 @@ defp deps do # ... {:sentry, "~> 10.0"}, - {:jason, "~> 1.4"}, {:hackney, "~> 1.19"} ] end ``` +> [!WARNING] +> If you're using an Elixir version before 1.18, the Sentry SDK will default to [Jason] as the JSON library. However, you **must** add it to your dependencies: +> +> ```elixir +> defp deps do +> [ +> # ... +> {:sentry, "~> 10.0"}, +> {:jason, "~> 1.4"} +> ] +> end +> ``` + ### Configuration Sentry has a range of configuration options, but most applications will have a configuration that looks like the following: @@ -130,7 +144,6 @@ Thanks to everyone who has contributed to this project so far. - ## Getting Help/Support If you need help setting up or configuring the Elixir SDK (or anything else in the Sentry universe) please head over to the [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr). There is a ton of great people in our Discord community ready to help you! diff --git a/config/config.exs b/config/config.exs index 1184ea49..d63cc9d2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -15,4 +15,4 @@ if config_env() == :test do config :logger, backends: [] end -config :phoenix, :json_library, Jason +config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason) diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index 87c124fc..54c7d6b8 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -295,7 +295,7 @@ defmodule Sentry.Client do defp sanitize_non_jsonable_value(value, json_library) do try do - json_library.encode(value) + Sentry.JSON.encode(value, json_library) catch _type, _reason -> {:changed, inspect(value)} else diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index a2407512..d57830c7 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -146,14 +146,20 @@ defmodule Sentry.Config do environment variable is set, it will be used as the default value. """ ], + # TODO: deprecate this once we require Elixir 1.18+, when we can force users to use + # the JSON module. json_library: [ type: {:custom, __MODULE__, :__validate_json_library__, []}, - default: Jason, type_doc: "`t:module/0`", + default: if(Code.ensure_loaded?(JSON), do: JSON, else: Jason), doc: """ A module that implements the "standard" Elixir JSON behaviour, that is, exports the - `encode/1` and `decode/1` functions. If you use the default, make sure to add - [`:jason`](https://hex.pm/packages/jason) as a dependency of your application. + `encode/1` and `decode/1` functions. + + Defaults to `Jason` if the `JSON` kernel module is not available (it was introduced + in Elixir 1.18.0). If you use the default configuration with Elixir version lower than + 1.18, this option will default to `Jason`, but you will have to add + [`:jason`](https://hexa.pm/packages/jason) as a dependency of your application. """ ], send_client_reports: [ @@ -693,6 +699,8 @@ defmodule Sentry.Config do {:error, "nil is not a valid value for the :json_library option"} end + def __validate_json_library__(JSON), do: {:ok, JSON} + def __validate_json_library__(mod) when is_atom(mod) do try do with {:ok, %{}} <- mod.decode("{}"), diff --git a/lib/sentry/envelope.ex b/lib/sentry/envelope.ex index 649f6e85..8f44cb20 100644 --- a/lib/sentry/envelope.ex +++ b/lib/sentry/envelope.ex @@ -81,7 +81,7 @@ defmodule Sentry.Envelope do end defp item_to_binary(json_library, %Event{} = event) do - case event |> Sentry.Client.render_event() |> json_library.encode() do + case event |> Sentry.Client.render_event() |> Sentry.JSON.encode(json_library) do {:ok, encoded_event} -> header = ~s({"type":"event","length":#{byte_size(encoded_event)}}) [header, ?\n, encoded_event, ?\n] @@ -100,13 +100,13 @@ defmodule Sentry.Envelope do into: header, do: {Atom.to_string(key), value} - {:ok, header_iodata} = json_library.encode(header) + {:ok, header_iodata} = Sentry.JSON.encode(header, json_library) [header_iodata, ?\n, attachment.data, ?\n] end defp item_to_binary(json_library, %CheckIn{} = check_in) do - case check_in |> CheckIn.to_map() |> json_library.encode() do + case check_in |> CheckIn.to_map() |> Sentry.JSON.encode(json_library) do {:ok, encoded_check_in} -> header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}}) [header, ?\n, encoded_check_in, ?\n] @@ -117,7 +117,7 @@ defmodule Sentry.Envelope do end defp item_to_binary(json_library, %ClientReport{} = client_report) do - case client_report |> Map.from_struct() |> json_library.encode() do + case client_report |> Map.from_struct() |> Sentry.JSON.encode(json_library) do {:ok, encoded_client_report} -> header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}}) [header, ?\n, encoded_client_report, ?\n] diff --git a/lib/sentry/json.ex b/lib/sentry/json.ex new file mode 100644 index 00000000..b5378504 --- /dev/null +++ b/lib/sentry/json.ex @@ -0,0 +1,33 @@ +defmodule Sentry.JSON do + @moduledoc false + + @spec encode(term(), module()) :: {:ok, String.t()} | {:error, term()} + def encode(data, json_library) + + if Code.ensure_loaded?(JSON) do + def encode(data, JSON) do + {:ok, JSON.encode!(data)} + rescue + error -> {:error, error} + end + end + + def encode(data, json_library) do + json_library.encode(data) + end + + @spec decode(binary(), module()) :: {:ok, term()} | {:error, term()} + def decode(binary, json_library) + + if Code.ensure_loaded?(JSON) do + def decode(binary, JSON) do + {:ok, JSON.decode!(binary)} + rescue + error -> {:error, error} + end + end + + def decode(binary, json_library) do + json_library.decode(binary) + end +end diff --git a/lib/sentry/transport.ex b/lib/sentry/transport.ex index a353b2b8..e6899ff9 100644 --- a/lib/sentry/transport.ex +++ b/lib/sentry/transport.ex @@ -95,7 +95,7 @@ defmodule Sentry.Transport do defp request(client, endpoint, headers, body) do with {:ok, 200, _headers, body} <- client_post_and_validate_return_value(client, endpoint, headers, body), - {:ok, json} <- Config.json_library().decode(body) do + {:ok, json} <- Sentry.JSON.decode(body, Config.json_library()) do {:ok, Map.get(json, "id")} else {:ok, 429, headers, _body} -> diff --git a/pages/setup-with-plug-and-phoenix.md b/pages/setup-with-plug-and-phoenix.md index 00271923..2221fc88 100644 --- a/pages/setup-with-plug-and-phoenix.md +++ b/pages/setup-with-plug-and-phoenix.md @@ -51,7 +51,7 @@ defmodule MyAppWeb.ErrorView do def render("500.html", _assigns) do case Sentry.get_last_event_id_and_source() do {event_id, :plug} when is_binary(event_id) -> - opts = Jason.encode!(%{eventId: event_id}) + opts = JSON.encode!(%{eventId: event_id}) ~E""" diff --git a/test/envelope_test.exs b/test/envelope_test.exs index dd887d3d..d0707266 100644 --- a/test/envelope_test.exs +++ b/test/envelope_test.exs @@ -15,10 +15,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert Jason.decode!(id_line) == %{"event_id" => event.event_id} - assert %{"type" => "event", "length" => _} = Jason.decode!(header_line) + assert decode!(id_line) == %{"event_id" => event.event_id} + assert %{"type" => "event", "length" => _} = decode!(header_line) - assert {:ok, decoded_event} = Jason.decode(event_line) + decoded_event = decode!(event_line) assert decoded_event["event_id"] == event.event_id assert decoded_event["breadcrumbs"] == [] assert decoded_event["environment"] == "test" @@ -65,29 +65,29 @@ defmodule Sentry.EnvelopeTest do "..." ] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) + assert %{"event_id" => _} = decode!(id_line) - assert Jason.decode!(attachment1_header) == %{ + assert decode!(attachment1_header) == %{ "type" => "attachment", "length" => 3, "filename" => "example.dat" } - assert Jason.decode!(attachment2_header) == %{ + assert decode!(attachment2_header) == %{ "type" => "attachment", "length" => 6, "filename" => "example.txt", "content_type" => "text/plain" } - assert Jason.decode!(attachment3_header) == %{ + assert decode!(attachment3_header) == %{ "type" => "attachment", "length" => 2, "filename" => "example.json", "content_type" => "application/json" } - assert Jason.decode!(attachment4_header) == %{ + assert decode!(attachment4_header) == %{ "type" => "attachment", "length" => 3, "filename" => "dump", @@ -105,10 +105,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) - assert %{"type" => "check_in", "length" => _} = Jason.decode!(header_line) + assert %{"event_id" => _} = decode!(id_line) + assert %{"type" => "check_in", "length" => _} = decode!(header_line) - assert {:ok, decoded_check_in} = Jason.decode(event_line) + decoded_check_in = decode!(event_line) assert decoded_check_in["check_in_id"] == check_in_id assert decoded_check_in["monitor_slug"] == "test" assert decoded_check_in["status"] == "ok" @@ -128,10 +128,10 @@ defmodule Sentry.EnvelopeTest do assert {:ok, encoded} = Envelope.to_binary(envelope) assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true) - assert %{"event_id" => _} = Jason.decode!(id_line) - assert %{"type" => "client_report", "length" => _} = Jason.decode!(header_line) + assert %{"event_id" => _} = decode!(id_line) + assert %{"type" => "client_report", "length" => _} = decode!(header_line) - assert {:ok, decoded_client_report} = Jason.decode(event_line) + decoded_client_report = decode!(event_line) assert decoded_client_report["timestamp"] == client_report.timestamp assert decoded_client_report["discarded_events"] == [ diff --git a/test/plug_capture_test.exs b/test/plug_capture_test.exs index 226fc2cf..c9e1e22e 100644 --- a/test/plug_capture_test.exs +++ b/test/plug_capture_test.exs @@ -29,7 +29,9 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason + json_mod = if Code.ensure_loaded?(JSON), do: JSON, else: Jason + + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_mod plug Sentry.PlugContext plug PhoenixRouter end @@ -45,7 +47,9 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Endpoint, otp_app: :sentry use Plug.Debugger, otp_app: :sentry - plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason + json_mod = if Code.ensure_loaded?(JSON), do: JSON, else: Jason + + plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_mod plug Sentry.PlugContext plug PhoenixRouter end diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 3637c712..07ad8992 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -161,7 +161,11 @@ defmodule Sentry.ConfigTest do assert Config.validate!(json_library: Jason)[:json_library] == Jason # Default - assert Config.validate!([])[:json_library] == Jason + if Version.match?(System.version(), "~> 1.18") do + assert Config.validate!([])[:json_library] == JSON + else + assert Config.validate!([])[:json_library] == Jason + end assert_raise ArgumentError, ~r/invalid value for :json_library option/, fn -> Config.validate!(json_library: Atom) diff --git a/test/sentry/json_test.exs b/test/sentry/json_test.exs new file mode 100644 index 00000000..48461c65 --- /dev/null +++ b/test/sentry/json_test.exs @@ -0,0 +1,32 @@ +defmodule Sentry.JSONTest do + use ExUnit.Case, async: true + + json_modules = + if Code.ensure_loaded?(JSON) do + [JSON, Jason] + else + [Jason] + end + + for json_mod <- json_modules do + describe "decode/2 with #{inspect(json_mod)}" do + test "decodes empty object to empty map" do + assert Sentry.JSON.decode("{}", unquote(json_mod)) == {:ok, %{}} + end + + test "returns {:error, reason} if binary is not a JSON" do + assert {:error, _reason} = Sentry.JSON.decode("not JSON", unquote(json_mod)) + end + end + + describe "encode/2 with #{inspect(json_mod)}" do + test "encodes empty map to empty object" do + assert Sentry.JSON.encode(%{}, unquote(json_mod)) == {:ok, "{}"} + end + + test "returns {:error, reason} if data cannot be parsed to JSON" do + assert {:error, _reason} = Sentry.JSON.encode({:ok, "will fail"}, unquote(json_mod)) + end + end + end +end diff --git a/test/sentry/transport_test.exs b/test/sentry/transport_test.exs index 0b7d7e85..ec3989ed 100644 --- a/test/sentry/transport_test.exs +++ b/test/sentry/transport_test.exs @@ -190,11 +190,17 @@ defmodule Sentry.TransportTest do Plug.Conn.resp(conn, 200, ~s) end) - assert {:request_failure, %Jason.DecodeError{}} = + assert {:request_failure, error} = error(fn -> Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [0]) end) + if Version.match?(System.version(), "~> 1.18") do + assert %JSON.DecodeError{} = error + else + assert %Jason.DecodeError{} = error + end + assert_received {:request, ^ref} assert_received {:request, ^ref} end diff --git a/test/support/example_plug_application.ex b/test/support/example_plug_application.ex index ee6e88c5..0fc5d637 100644 --- a/test/support/example_plug_application.ex +++ b/test/support/example_plug_application.ex @@ -5,6 +5,8 @@ defmodule Sentry.ExamplePlugApplication do import ExUnit.Assertions + alias Sentry.TestHelpers + plug Plug.Parsers, parsers: [:multipart, :urlencoded] plug Sentry.PlugContext plug :match @@ -50,7 +52,7 @@ defmodule Sentry.ExamplePlugApplication do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Jason.encode!() + |> TestHelpers.encode!() """ diff --git a/test/support/test_error_view.ex b/test/support/test_error_view.ex index 29dcd7e4..fb8fb6e1 100644 --- a/test/support/test_error_view.ex +++ b/test/support/test_error_view.ex @@ -3,12 +3,14 @@ defmodule Sentry.ErrorView do import Phoenix.HTML, only: [raw: 1] + alias Sentry.TestHelpers + def render(_, _) do case Sentry.get_last_event_id_and_source() do {event_id, :plug} -> opts = %{title: "Testing", eventId: event_id} - |> Jason.encode!() + |> TestHelpers.encode!() assigns = %{opts: opts} diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index eca6a617..f3203593 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -1,7 +1,17 @@ defmodule Sentry.TestHelpers do import ExUnit.Assertions - alias Sentry.Config + @spec decode!(String.t()) :: term() + def decode!(binary) do + assert {:ok, data} = Sentry.JSON.decode(binary, Sentry.Config.json_library()) + data + end + + @spec decode!(term()) :: String.t() + def encode!(data) do + assert {:ok, binary} = Sentry.JSON.encode(data, Sentry.Config.json_library()) + binary + end @spec put_test_config(keyword()) :: :ok def put_test_config(config) when is_list(config) do @@ -46,7 +56,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) + %{"event_id" => _} = decode!(id_line) decode_envelope_items(rest) end @@ -54,13 +64,9 @@ defmodule Sentry.TestHelpers do items |> Enum.chunk_every(2) |> Enum.flat_map(fn - [header, item] -> - {:ok, header} = Config.json_library().decode(header) - {:ok, item} = Config.json_library().decode(item) - [{header, item}] + [header, item] ->[{decode!(header), decode!(item)}] - [""] -> - [] + [""] ->[] end) end end diff --git a/test_integrations/phoenix_app/config/config.exs b/test_integrations/phoenix_app/config/config.exs index 1f7d4a0d..a0ce0afe 100644 --- a/test_integrations/phoenix_app/config/config.exs +++ b/test_integrations/phoenix_app/config/config.exs @@ -57,8 +57,7 @@ config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason +config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason) # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/test_integrations/phoenix_app/test/support/test_helpers.ex b/test_integrations/phoenix_app/test/support/test_helpers.ex index eca6a617..365d1a62 100644 --- a/test_integrations/phoenix_app/test/support/test_helpers.ex +++ b/test_integrations/phoenix_app/test/support/test_helpers.ex @@ -1,7 +1,17 @@ defmodule Sentry.TestHelpers do import ExUnit.Assertions - alias Sentry.Config + @spec decode!(String.t()) :: term() + def decode!(binary) do + assert {:ok, data} = Sentry.JSON.decode(binary) + data + end + + @spec decode!(term()) :: String.t() + def encode!(data) do + assert {:ok, binary} = Sentry.JSON.encode(data) + binary + end @spec put_test_config(keyword()) :: :ok def put_test_config(config) when is_list(config) do @@ -46,7 +56,7 @@ defmodule Sentry.TestHelpers do @spec decode_envelope!(binary()) :: [{header :: map(), item :: map()}] def decode_envelope!(binary) do [id_line | rest] = String.split(binary, "\n") - {:ok, %{"event_id" => _}} = Config.json_library().decode(id_line) + %{"event_id" => _} = decode!(id_line) decode_envelope_items(rest) end @@ -54,13 +64,9 @@ defmodule Sentry.TestHelpers do items |> Enum.chunk_every(2) |> Enum.flat_map(fn - [header, item] -> - {:ok, header} = Config.json_library().decode(header) - {:ok, item} = Config.json_library().decode(item) - [{header, item}] + [header, item] -> [{decode!(header), decode!(item)}] - [""] -> - [] + [""] -> [] end) end end From 96948e6a14c124e9549b6e0dbf5ae26386c1216d Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Fri, 3 Jan 2025 10:49:35 +0100 Subject: [PATCH 4/7] FIXUP --- test/sentry/transport_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sentry/transport_test.exs b/test/sentry/transport_test.exs index ec3989ed..3e9f12cc 100644 --- a/test/sentry/transport_test.exs +++ b/test/sentry/transport_test.exs @@ -196,9 +196,9 @@ defmodule Sentry.TransportTest do end) if Version.match?(System.version(), "~> 1.18") do - assert %JSON.DecodeError{} = error + assert error.__struct__ == JSON.DecodeError else - assert %Jason.DecodeError{} = error + assert error.__struct__ == Jason.DecodeError end assert_received {:request, ^ref} From 2bb49286bba5c848029932d08769296d2739a0a6 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Fri, 3 Jan 2025 10:55:26 +0100 Subject: [PATCH 5/7] FIXUP --- test_integrations/phoenix_app/test/support/test_helpers.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_integrations/phoenix_app/test/support/test_helpers.ex b/test_integrations/phoenix_app/test/support/test_helpers.ex index 365d1a62..0df21667 100644 --- a/test_integrations/phoenix_app/test/support/test_helpers.ex +++ b/test_integrations/phoenix_app/test/support/test_helpers.ex @@ -3,13 +3,13 @@ defmodule Sentry.TestHelpers do @spec decode!(String.t()) :: term() def decode!(binary) do - assert {:ok, data} = Sentry.JSON.decode(binary) + assert {:ok, data} = Sentry.JSON.decode(binary, Sentry.Config.json_library()) data end @spec decode!(term()) :: String.t() def encode!(data) do - assert {:ok, binary} = Sentry.JSON.encode(data) + assert {:ok, binary} = Sentry.JSON.encode(data, Sentry.Config.json_library()) binary end From 02c1dc2bb3be5c47a61fe071948da0a24b28608e Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Fri, 3 Jan 2025 11:07:25 +0100 Subject: [PATCH 6/7] Dialyzer --- .dialyzer_ignore.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index ac700b22..53745216 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,3 +1,4 @@ [ - {"test/support/example_plug_application.ex"} + {"test/support/example_plug_application.ex"}, + {"test/support/test_helpers.ex"} ] From ca3102f674de874c3d08e4b096929490debba09c Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 7 Jan 2025 11:24:47 +0100 Subject: [PATCH 7/7] Update README.md Co-authored-by: Peter Solnica --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 000c5259..765396c3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This is the official Sentry SDK for [Sentry]. To use Sentry in your project, add it as a dependency in your `mix.exs` file. -Sentry does not install a JSON library nor a HTTP client by itself. Sentry will default to the [built-in `JSON`](https://hexdocs.pm/elixir/JSON.html) for JSON and [Hackney] for HTTP requests, but can be configured to use other ones. To use the default ones, do: +Sentry does not install a JSON library nor an HTTP client by itself. Sentry will default to the [built-in `JSON`](https://hexdocs.pm/elixir/JSON.html) for JSON and [Hackney] for HTTP requests, but can be configured to use other ones. To use the default ones, do: ```elixir defp deps do