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"}
]
diff --git a/README.md b/README.md
index 4dbd358f..765396c3 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 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
@@ -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..3e9f12cc 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 error.__struct__ == JSON.DecodeError
+ else
+ assert error.__struct__ == Jason.DecodeError
+ 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..0df21667 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, 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