Skip to content

Commit f375551

Browse files
authored
Allow the usage of JSON for Elixir 1.18+ (#845)
Co-Authored-By: [email protected]
1 parent 722e316 commit f375551

19 files changed

+169
-53
lines changed

.dialyzer_ignore.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[
2-
{"test/support/example_plug_application.ex"}
2+
{"test/support/example_plug_application.ex"},
3+
{"test/support/test_helpers.ex"}
34
]

README.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,34 @@ This is the official Sentry SDK for [Sentry].
1818

1919
### Install
2020

21-
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:
21+
To use Sentry in your project, add it as a dependency in your `mix.exs` file.
22+
23+
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:
2224

2325
```elixir
2426
defp deps do
2527
[
2628
# ...
2729

2830
{:sentry, "~> 10.0"},
29-
{:jason, "~> 1.4"},
3031
{:hackney, "~> 1.19"}
3132
]
3233
end
3334
```
3435

36+
> [!WARNING]
37+
> 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:
38+
>
39+
> ```elixir
40+
> defp deps do
41+
> [
42+
> # ...
43+
> {:sentry, "~> 10.0"},
44+
> {:jason, "~> 1.4"}
45+
> ]
46+
> end
47+
> ```
48+
3549
### Configuration
3650
3751
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.
130144
<img src="https://contributors-img.web.app/image?repo=getsentry/sentry-elixir" />
131145
</a>
132146

133-
134147
## Getting Help/Support
135148

136149
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!

config/config.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ if config_env() == :test do
1515
config :logger, backends: []
1616
end
1717

18-
config :phoenix, :json_library, Jason
18+
config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason)

lib/sentry/client.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ defmodule Sentry.Client do
295295

296296
defp sanitize_non_jsonable_value(value, json_library) do
297297
try do
298-
json_library.encode(value)
298+
Sentry.JSON.encode(value, json_library)
299299
catch
300300
_type, _reason -> {:changed, inspect(value)}
301301
else

lib/sentry/config.ex

+11-3
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,20 @@ defmodule Sentry.Config do
146146
environment variable is set, it will be used as the default value.
147147
"""
148148
],
149+
# TODO: deprecate this once we require Elixir 1.18+, when we can force users to use
150+
# the JSON module.
149151
json_library: [
150152
type: {:custom, __MODULE__, :__validate_json_library__, []},
151-
default: Jason,
152153
type_doc: "`t:module/0`",
154+
default: if(Code.ensure_loaded?(JSON), do: JSON, else: Jason),
153155
doc: """
154156
A module that implements the "standard" Elixir JSON behaviour, that is, exports the
155-
`encode/1` and `decode/1` functions. If you use the default, make sure to add
156-
[`:jason`](https://hex.pm/packages/jason) as a dependency of your application.
157+
`encode/1` and `decode/1` functions.
158+
159+
Defaults to `Jason` if the `JSON` kernel module is not available (it was introduced
160+
in Elixir 1.18.0). If you use the default configuration with Elixir version lower than
161+
1.18, this option will default to `Jason`, but you will have to add
162+
[`:jason`](https://hexa.pm/packages/jason) as a dependency of your application.
157163
"""
158164
],
159165
send_client_reports: [
@@ -693,6 +699,8 @@ defmodule Sentry.Config do
693699
{:error, "nil is not a valid value for the :json_library option"}
694700
end
695701

702+
def __validate_json_library__(JSON), do: {:ok, JSON}
703+
696704
def __validate_json_library__(mod) when is_atom(mod) do
697705
try do
698706
with {:ok, %{}} <- mod.decode("{}"),

lib/sentry/envelope.ex

+4-4
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ defmodule Sentry.Envelope do
8181
end
8282

8383
defp item_to_binary(json_library, %Event{} = event) do
84-
case event |> Sentry.Client.render_event() |> json_library.encode() do
84+
case event |> Sentry.Client.render_event() |> Sentry.JSON.encode(json_library) do
8585
{:ok, encoded_event} ->
8686
header = ~s({"type":"event","length":#{byte_size(encoded_event)}})
8787
[header, ?\n, encoded_event, ?\n]
@@ -100,13 +100,13 @@ defmodule Sentry.Envelope do
100100
into: header,
101101
do: {Atom.to_string(key), value}
102102

103-
{:ok, header_iodata} = json_library.encode(header)
103+
{:ok, header_iodata} = Sentry.JSON.encode(header, json_library)
104104

105105
[header_iodata, ?\n, attachment.data, ?\n]
106106
end
107107

108108
defp item_to_binary(json_library, %CheckIn{} = check_in) do
109-
case check_in |> CheckIn.to_map() |> json_library.encode() do
109+
case check_in |> CheckIn.to_map() |> Sentry.JSON.encode(json_library) do
110110
{:ok, encoded_check_in} ->
111111
header = ~s({"type":"check_in","length":#{byte_size(encoded_check_in)}})
112112
[header, ?\n, encoded_check_in, ?\n]
@@ -117,7 +117,7 @@ defmodule Sentry.Envelope do
117117
end
118118

119119
defp item_to_binary(json_library, %ClientReport{} = client_report) do
120-
case client_report |> Map.from_struct() |> json_library.encode() do
120+
case client_report |> Map.from_struct() |> Sentry.JSON.encode(json_library) do
121121
{:ok, encoded_client_report} ->
122122
header = ~s({"type":"client_report","length":#{byte_size(encoded_client_report)}})
123123
[header, ?\n, encoded_client_report, ?\n]

lib/sentry/json.ex

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule Sentry.JSON do
2+
@moduledoc false
3+
4+
@spec encode(term(), module()) :: {:ok, String.t()} | {:error, term()}
5+
def encode(data, json_library)
6+
7+
if Code.ensure_loaded?(JSON) do
8+
def encode(data, JSON) do
9+
{:ok, JSON.encode!(data)}
10+
rescue
11+
error -> {:error, error}
12+
end
13+
end
14+
15+
def encode(data, json_library) do
16+
json_library.encode(data)
17+
end
18+
19+
@spec decode(binary(), module()) :: {:ok, term()} | {:error, term()}
20+
def decode(binary, json_library)
21+
22+
if Code.ensure_loaded?(JSON) do
23+
def decode(binary, JSON) do
24+
{:ok, JSON.decode!(binary)}
25+
rescue
26+
error -> {:error, error}
27+
end
28+
end
29+
30+
def decode(binary, json_library) do
31+
json_library.decode(binary)
32+
end
33+
end

lib/sentry/transport.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ defmodule Sentry.Transport do
9595
defp request(client, endpoint, headers, body) do
9696
with {:ok, 200, _headers, body} <-
9797
client_post_and_validate_return_value(client, endpoint, headers, body),
98-
{:ok, json} <- Config.json_library().decode(body) do
98+
{:ok, json} <- Sentry.JSON.decode(body, Config.json_library()) do
9999
{:ok, Map.get(json, "id")}
100100
else
101101
{:ok, 429, headers, _body} ->

pages/setup-with-plug-and-phoenix.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ defmodule MyAppWeb.ErrorView do
5151
def render("500.html", _assigns) do
5252
case Sentry.get_last_event_id_and_source() do
5353
{event_id, :plug} when is_binary(event_id) ->
54-
opts = Jason.encode!(%{eventId: event_id})
54+
opts = JSON.encode!(%{eventId: event_id})
5555

5656
~E"""
5757
<script src="https://browser.sentry-cdn.com/5.9.1/bundle.min.js" integrity="sha384-/x1aHz0nKRd6zVUazsV6CbQvjJvr6zQL2CHbQZf3yoLkezyEtZUpqUNnOLW9Nt3v" crossorigin="anonymous"></script>

test/envelope_test.exs

+14-14
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ defmodule Sentry.EnvelopeTest do
1515
assert {:ok, encoded} = Envelope.to_binary(envelope)
1616

1717
assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true)
18-
assert Jason.decode!(id_line) == %{"event_id" => event.event_id}
19-
assert %{"type" => "event", "length" => _} = Jason.decode!(header_line)
18+
assert decode!(id_line) == %{"event_id" => event.event_id}
19+
assert %{"type" => "event", "length" => _} = decode!(header_line)
2020

21-
assert {:ok, decoded_event} = Jason.decode(event_line)
21+
decoded_event = decode!(event_line)
2222
assert decoded_event["event_id"] == event.event_id
2323
assert decoded_event["breadcrumbs"] == []
2424
assert decoded_event["environment"] == "test"
@@ -65,29 +65,29 @@ defmodule Sentry.EnvelopeTest do
6565
"..."
6666
] = String.split(encoded, "\n", trim: true)
6767

68-
assert %{"event_id" => _} = Jason.decode!(id_line)
68+
assert %{"event_id" => _} = decode!(id_line)
6969

70-
assert Jason.decode!(attachment1_header) == %{
70+
assert decode!(attachment1_header) == %{
7171
"type" => "attachment",
7272
"length" => 3,
7373
"filename" => "example.dat"
7474
}
7575

76-
assert Jason.decode!(attachment2_header) == %{
76+
assert decode!(attachment2_header) == %{
7777
"type" => "attachment",
7878
"length" => 6,
7979
"filename" => "example.txt",
8080
"content_type" => "text/plain"
8181
}
8282

83-
assert Jason.decode!(attachment3_header) == %{
83+
assert decode!(attachment3_header) == %{
8484
"type" => "attachment",
8585
"length" => 2,
8686
"filename" => "example.json",
8787
"content_type" => "application/json"
8888
}
8989

90-
assert Jason.decode!(attachment4_header) == %{
90+
assert decode!(attachment4_header) == %{
9191
"type" => "attachment",
9292
"length" => 3,
9393
"filename" => "dump",
@@ -105,10 +105,10 @@ defmodule Sentry.EnvelopeTest do
105105
assert {:ok, encoded} = Envelope.to_binary(envelope)
106106

107107
assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true)
108-
assert %{"event_id" => _} = Jason.decode!(id_line)
109-
assert %{"type" => "check_in", "length" => _} = Jason.decode!(header_line)
108+
assert %{"event_id" => _} = decode!(id_line)
109+
assert %{"type" => "check_in", "length" => _} = decode!(header_line)
110110

111-
assert {:ok, decoded_check_in} = Jason.decode(event_line)
111+
decoded_check_in = decode!(event_line)
112112
assert decoded_check_in["check_in_id"] == check_in_id
113113
assert decoded_check_in["monitor_slug"] == "test"
114114
assert decoded_check_in["status"] == "ok"
@@ -128,10 +128,10 @@ defmodule Sentry.EnvelopeTest do
128128
assert {:ok, encoded} = Envelope.to_binary(envelope)
129129

130130
assert [id_line, header_line, event_line] = String.split(encoded, "\n", trim: true)
131-
assert %{"event_id" => _} = Jason.decode!(id_line)
132-
assert %{"type" => "client_report", "length" => _} = Jason.decode!(header_line)
131+
assert %{"event_id" => _} = decode!(id_line)
132+
assert %{"type" => "client_report", "length" => _} = decode!(header_line)
133133

134-
assert {:ok, decoded_client_report} = Jason.decode(event_line)
134+
decoded_client_report = decode!(event_line)
135135
assert decoded_client_report["timestamp"] == client_report.timestamp
136136

137137
assert decoded_client_report["discarded_events"] == [

test/plug_capture_test.exs

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ defmodule Sentry.PlugCaptureTest do
2929
use Phoenix.Endpoint, otp_app: :sentry
3030
use Plug.Debugger, otp_app: :sentry
3131

32-
plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason
32+
json_mod = if Code.ensure_loaded?(JSON), do: JSON, else: Jason
33+
34+
plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_mod
3335
plug Sentry.PlugContext
3436
plug PhoenixRouter
3537
end
@@ -45,7 +47,9 @@ defmodule Sentry.PlugCaptureTest do
4547
use Phoenix.Endpoint, otp_app: :sentry
4648
use Plug.Debugger, otp_app: :sentry
4749

48-
plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: Jason
50+
json_mod = if Code.ensure_loaded?(JSON), do: JSON, else: Jason
51+
52+
plug Plug.Parsers, parsers: [:json], pass: ["*/*"], json_decoder: json_mod
4953
plug Sentry.PlugContext
5054
plug PhoenixRouter
5155
end

test/sentry/config_test.exs

+5-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ defmodule Sentry.ConfigTest do
161161
assert Config.validate!(json_library: Jason)[:json_library] == Jason
162162

163163
# Default
164-
assert Config.validate!([])[:json_library] == Jason
164+
if Version.match?(System.version(), "~> 1.18") do
165+
assert Config.validate!([])[:json_library] == JSON
166+
else
167+
assert Config.validate!([])[:json_library] == Jason
168+
end
165169

166170
assert_raise ArgumentError, ~r/invalid value for :json_library option/, fn ->
167171
Config.validate!(json_library: Atom)

test/sentry/json_test.exs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule Sentry.JSONTest do
2+
use ExUnit.Case, async: true
3+
4+
json_modules =
5+
if Code.ensure_loaded?(JSON) do
6+
[JSON, Jason]
7+
else
8+
[Jason]
9+
end
10+
11+
for json_mod <- json_modules do
12+
describe "decode/2 with #{inspect(json_mod)}" do
13+
test "decodes empty object to empty map" do
14+
assert Sentry.JSON.decode("{}", unquote(json_mod)) == {:ok, %{}}
15+
end
16+
17+
test "returns {:error, reason} if binary is not a JSON" do
18+
assert {:error, _reason} = Sentry.JSON.decode("not JSON", unquote(json_mod))
19+
end
20+
end
21+
22+
describe "encode/2 with #{inspect(json_mod)}" do
23+
test "encodes empty map to empty object" do
24+
assert Sentry.JSON.encode(%{}, unquote(json_mod)) == {:ok, "{}"}
25+
end
26+
27+
test "returns {:error, reason} if data cannot be parsed to JSON" do
28+
assert {:error, _reason} = Sentry.JSON.encode({:ok, "will fail"}, unquote(json_mod))
29+
end
30+
end
31+
end
32+
end

test/sentry/transport_test.exs

+7-1
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,17 @@ defmodule Sentry.TransportTest do
190190
Plug.Conn.resp(conn, 200, ~s<invalid JSON>)
191191
end)
192192

193-
assert {:request_failure, %Jason.DecodeError{}} =
193+
assert {:request_failure, error} =
194194
error(fn ->
195195
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [0])
196196
end)
197197

198+
if Version.match?(System.version(), "~> 1.18") do
199+
assert error.__struct__ == JSON.DecodeError
200+
else
201+
assert error.__struct__ == Jason.DecodeError
202+
end
203+
198204
assert_received {:request, ^ref}
199205
assert_received {:request, ^ref}
200206
end

test/support/example_plug_application.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Sentry.ExamplePlugApplication do
55

66
import ExUnit.Assertions
77

8+
alias Sentry.TestHelpers
9+
810
plug Plug.Parsers, parsers: [:multipart, :urlencoded]
911
plug Sentry.PlugContext
1012
plug :match
@@ -50,7 +52,7 @@ defmodule Sentry.ExamplePlugApplication do
5052
{event_id, :plug} ->
5153
opts =
5254
%{title: "Testing", eventId: event_id}
53-
|> Jason.encode!()
55+
|> TestHelpers.encode!()
5456

5557
"""
5658
<script src="https://browser.sentry-cdn.com/5.9.1/bundle.min.js" integrity="sha384-/x1aHz0nKRd6zVUazsV6CbQvjJvr6zQL2CHbQZf3yoLkezyEtZUpqUNnOLW9Nt3v" crossorigin="anonymous"></script>

test/support/test_error_view.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ defmodule Sentry.ErrorView do
33

44
import Phoenix.HTML, only: [raw: 1]
55

6+
alias Sentry.TestHelpers
7+
68
def render(_, _) do
79
case Sentry.get_last_event_id_and_source() do
810
{event_id, :plug} ->
911
opts =
1012
%{title: "Testing", eventId: event_id}
11-
|> Jason.encode!()
13+
|> TestHelpers.encode!()
1214

1315
assigns = %{opts: opts}
1416

0 commit comments

Comments
 (0)