Skip to content

Commit 4eb7f61

Browse files
authored
feat: Support native JSON module (#135)
Since Elixir 1.18 there is a native `JSON` module, so new projects don't need `Jason` and many don't have it. This pull request updates the error tracker so: - Jason is an optional dependency as the error tracker can work without it - We try to use the user-defined JSON library (just like we did before) but if none is defined we default to the native `JSON` module for Elixir 1.18 and newer. On older versions we still default to `Jason` just like we did before. - We use the `encode_to_iodata!` function which is the most performant and provided by both libraries. - The `JSON` module can't pretty print like `Jason` does. Instead we are now pretty printing in the client using `JSON.stringify` which is widely available across all browsers. There should not be any user-facing changes. Users will still see the JSON context formatted just like they did before but now the ErrorTracker works well on Elixir 1.18 and newer projects that don't include Jason. We also don't force it for such projects as a native alternative exists.
1 parent 428b650 commit 4eb7f61

File tree

6 files changed

+83
-26
lines changed

6 files changed

+83
-26
lines changed

assets/js/app.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,35 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("
55
let livePath = document.querySelector("meta[name='live-path']").getAttribute("content");
66
let liveTransport = document .querySelector("meta[name='live-transport']") .getAttribute("content");
77

8+
const Hooks = {
9+
JsonPrettyPrint: {
10+
mounted() {
11+
this.formatJson();
12+
},
13+
updated() {
14+
this.formatJson();
15+
},
16+
formatJson() {
17+
try {
18+
// Get the raw JSON content
19+
const rawJson = this.el.textContent.trim();
20+
// Parse and stringify with indentation
21+
const formattedJson = JSON.stringify(JSON.parse(rawJson), null, 2);
22+
// Update the element content
23+
this.el.textContent = formattedJson;
24+
} catch (error) {
25+
console.error("Error formatting JSON:", error);
26+
// Keep the original content if there's an error
27+
}
28+
}
29+
}
30+
};
31+
832
let liveSocket = new LiveView.LiveSocket(livePath, Phoenix.Socket, {
933
transport: liveTransport === "longpoll" ? Phoenix.LongPoll : WebSocket,
1034
params: { _csrf_token: csrfToken },
35+
hooks: Hooks
36+
1137
});
1238

1339
// Show progress bar on live navigation and form submits

lib/error_tracker.ex

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,13 @@ defmodule ErrorTracker do
223223
224224
## Content serialization
225225
226-
The content stored on the context should be serializable using the JSON library
227-
used by the application (usually `Jason`), so it is rather recommended to use
228-
primitive types (strings, numbers, booleans...).
226+
The content stored on the context should be serializable using the JSON library used by the
227+
application (usually `JSON` for Elixir 1.18+ and `Jason` for older versions), so it is
228+
recommended to use primitive types (strings, numbers, booleans...).
229229
230230
If you still need to pass more complex data types to your context, please test
231-
that they can be encoded to JSON or storing the errors will fail. In the case
232-
of `Jason` that may require defining an Encoder for that data type if not
233-
included by default.
231+
that they can be encoded to JSON or storing the errors will fail. You may need to define a
232+
custom encoder for that data type if not included by default.
234233
"""
235234
@spec set_context(context()) :: context()
236235
def set_context(params) when is_map(params) do
@@ -384,4 +383,24 @@ defmodule ErrorTracker do
384383
Telemetry.new_occurrence(occurrence, muted)
385384
occurrence
386385
end
386+
387+
@default_json_encoder (cond do
388+
Code.ensure_loaded?(JSON) ->
389+
JSON
390+
391+
Code.ensure_loaded?(Jason) ->
392+
Jason
393+
394+
true ->
395+
raise """
396+
No JSON encoder found. Please add Jason to your dependencies:
397+
398+
{:jason, "~> 1.1"}
399+
400+
Or upgrade to Elixir 1.18+.
401+
"""
402+
end)
403+
404+
@doc false
405+
def __default_json_encoder__, do: @default_json_encoder
387406
end

lib/error_tracker/schemas/occurrence.ex

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,32 @@ defmodule ErrorTracker.Occurrence do
4646
if changeset.valid? do
4747
context = get_field(changeset, :context, %{})
4848

49-
json_encoder =
49+
db_json_encoder =
5050
ErrorTracker.Repo.with_adapter(fn
51-
:postgres -> Application.get_env(:postgrex, :json_library, Jason)
52-
:mysql -> Application.get_env(:myxql, :json_library, Jason)
53-
:sqlite -> Application.get_env(:ecto_sqlite3, :json_library, Jason)
51+
:postgres -> Application.get_env(:postgrex, :json_library)
52+
:mysql -> Application.get_env(:myxql, :json_library)
53+
:sqlite -> Application.get_env(:ecto_sqlite3, :json_library)
5454
end)
5555

56-
case json_encoder.encode_to_iodata(context) do
57-
{:ok, _} ->
58-
put_change(changeset, :context, context)
59-
60-
{:error, _} ->
61-
Logger.warning(
62-
"[ErrorTracker] Context has been ignored: it is not serializable to JSON."
63-
)
64-
65-
put_change(changeset, :context, %{
66-
error: "Context not stored because it contains information not serializable to JSON."
67-
})
68-
end
56+
validated_context =
57+
try do
58+
json_encoder = db_json_encoder || ErrorTracker.__default_json_encoder__()
59+
_iodata = json_encoder.encode_to_iodata!(context)
60+
61+
context
62+
rescue
63+
_e ->
64+
Logger.warning(
65+
"[ErrorTracker] Context has been ignored: it is not serializable to JSON."
66+
)
67+
68+
%{
69+
error:
70+
"Context not stored because it contains information not serializable to JSON."
71+
}
72+
end
73+
74+
put_change(changeset, :context, validated_context)
6975
else
7076
changeset
7177
end

lib/error_tracker/web/live/show.html.heex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@
7979
</.section>
8080

8181
<.section title="Context">
82-
<pre class="overflow-auto text-sm p-4 rounded-lg bg-gray-300/10 border border-gray-900"><%= Jason.encode!(@occurrence.context, pretty: true) %></pre>
82+
<pre
83+
id="context"
84+
class="overflow-auto text-sm p-4 rounded-lg bg-gray-300/10 border border-gray-900"
85+
phx-hook="JsonPrettyPrint"
86+
>
87+
<%= ErrorTracker.__default_json_encoder__().encode_to_iodata!(@occurrence.context) %>
88+
</pre>
8389
</.section>
8490
</div>
8591

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ defmodule ErrorTracker.MixProject do
8686
[
8787
{:ecto_sql, "~> 3.13"},
8888
{:ecto, "~> 3.13"},
89-
{:jason, "~> 1.1"},
9089
{:phoenix_live_view, "~> 1.0"},
9190
{:phoenix_ecto, "~> 4.6"},
9291
{:plug, "~> 1.10"},
92+
{:jason, "~> 1.1", optional: true},
9393
{:postgrex, ">= 0.0.0", optional: true},
9494
{:myxql, ">= 0.0.0", optional: true},
9595
{:ecto_sqlite3, ">= 0.0.0", optional: true},

priv/static/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)