From c77a1beb6289b596e5f68d27438b8b0f1b224b47 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Wed, 26 Feb 2025 18:37:31 +0100 Subject: [PATCH 1/5] Ensure compatibility with Jason and JSON --- lib/error_tracker/schemas/occurrence.ex | 30 ++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/error_tracker/schemas/occurrence.ex b/lib/error_tracker/schemas/occurrence.ex index 1c4288e..1ed3667 100644 --- a/lib/error_tracker/schemas/occurrence.ex +++ b/lib/error_tracker/schemas/occurrence.ex @@ -53,19 +53,23 @@ defmodule ErrorTracker.Occurrence do :sqlite -> Application.get_env(:ecto_sqlite3, :json_library, Jason) end) - case json_encoder.encode_to_iodata(context) do - {:ok, _} -> - put_change(changeset, :context, context) - - {:error, _} -> - Logger.warning( - "[ErrorTracker] Context has been ignored: it is not serializable to JSON." - ) - - put_change(changeset, :context, %{ - error: "Context not stored because it contains information not serializable to JSON." - }) - end + validated_context = + try do + _iodata = json_encoder.encode_to_iodata!(context) + context + rescue + _e in Protocol.UndefinedError -> + Logger.warning( + "[ErrorTracker] Context has been ignored: it is not serializable to JSON." + ) + + %{ + error: + "Context not stored because it contains information not serializable to JSON." + } + end + + put_change(changeset, :context, validated_context) else changeset end From 0df2d9bdb6b45baa6e970696b2587fd28f522fae Mon Sep 17 00:00:00 2001 From: crbelaus Date: Wed, 26 Feb 2025 18:41:23 +0100 Subject: [PATCH 2/5] Use JSON if available and mark Jason as an optional dep Elixir 1.18+ comes with the JSON module out of the box so there is no need for Jason anymore. This commit marks Jason as an optional dependency and upates the code to use JSON if available and Jason for older Elixir versions. --- lib/error_tracker.ex | 20 ++++++++++++++------ lib/error_tracker/schemas/occurrence.ex | 10 ++++++---- lib/error_tracker/web/live/show.html.heex | 4 +++- mix.exs | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 327060b..a2b27e1 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -223,14 +223,13 @@ defmodule ErrorTracker do ## Content serialization - The content stored on the context should be serializable using the JSON library - used by the application (usually `Jason`), so it is rather recommended to use - primitive types (strings, numbers, booleans...). + The content stored on the context should be serializable using the JSON library used by the + application (usually `JSON` for Elixir 1.18+ and `Jason` for older versions), so it is + recommended to use primitive types (strings, numbers, booleans...). If you still need to pass more complex data types to your context, please test - that they can be encoded to JSON or storing the errors will fail. In the case - of `Jason` that may require defining an Encoder for that data type if not - included by default. + that they can be encoded to JSON or storing the errors will fail. You may need to define a + custom encoder for that data type if not included by default. """ @spec set_context(context()) :: context() def set_context(params) when is_map(params) do @@ -384,4 +383,13 @@ defmodule ErrorTracker do Telemetry.new_occurrence(occurrence, muted) occurrence end + + @doc false + def __default_json_encoder__ do + # Elixir 1.18+ includes the JSON module. On older versions we should fall back to Jason (which + # is listed as an optional dependency). + if Version.match?(System.version(), ">= 1.18.0"), + do: JSON, + else: Jason + end end diff --git a/lib/error_tracker/schemas/occurrence.ex b/lib/error_tracker/schemas/occurrence.ex index 1ed3667..f579799 100644 --- a/lib/error_tracker/schemas/occurrence.ex +++ b/lib/error_tracker/schemas/occurrence.ex @@ -46,16 +46,18 @@ defmodule ErrorTracker.Occurrence do if changeset.valid? do context = get_field(changeset, :context, %{}) - json_encoder = + db_json_encoder = ErrorTracker.Repo.with_adapter(fn - :postgres -> Application.get_env(:postgrex, :json_library, Jason) - :mysql -> Application.get_env(:myxql, :json_library, Jason) - :sqlite -> Application.get_env(:ecto_sqlite3, :json_library, Jason) + :postgres -> Application.get_env(:postgrex, :json_library) + :mysql -> Application.get_env(:myxql, :json_library) + :sqlite -> Application.get_env(:ecto_sqlite3, :json_library) end) validated_context = try do + json_encoder = db_json_encoder || ErrorTracker.__default_json_encoder__() _iodata = json_encoder.encode_to_iodata!(context) + context rescue _e in Protocol.UndefinedError -> diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index ec045a8..c0c23a5 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -79,7 +79,9 @@ <.section title="Context"> -
<%= Jason.encode!(@occurrence.context, pretty: true) %>
+
+        <%= ErrorTracker.__default_json_encoder__().encode_to_iodata!(@occurrence.context) %>
+      
diff --git a/mix.exs b/mix.exs index 59238b0..cf33a3f 100644 --- a/mix.exs +++ b/mix.exs @@ -86,10 +86,10 @@ defmodule ErrorTracker.MixProject do [ {:ecto_sql, "~> 3.13"}, {:ecto, "~> 3.13"}, - {:jason, "~> 1.1"}, {:phoenix_live_view, "~> 1.0"}, {:phoenix_ecto, "~> 4.6"}, {:plug, "~> 1.10"}, + {:jason, "~> 1.1", optional: true}, {:postgrex, ">= 0.0.0", optional: true}, {:myxql, ">= 0.0.0", optional: true}, {:ecto_sqlite3, ">= 0.0.0", optional: true}, From 1646ec58eb19dbf28b8afbdd63982c56f9d28116 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Wed, 26 Feb 2025 19:15:59 +0100 Subject: [PATCH 3/5] WIP: pretty print context in client --- assets/js/app.js | 26 +++++++++++++++++++++++ lib/error_tracker/web/live/show.html.heex | 6 +++++- priv/static/app.js | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index aeef705..6e06925 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -5,9 +5,35 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute(" let livePath = document.querySelector("meta[name='live-path']").getAttribute("content"); let liveTransport = document .querySelector("meta[name='live-transport']") .getAttribute("content"); +const Hooks = { + JsonPrettyPrint: { + mounted() { + this.formatJson(); + }, + updated() { + this.formatJson(); + }, + formatJson() { + try { + // Get the raw JSON content + const rawJson = this.el.textContent.trim(); + // Parse and stringify with indentation + const formattedJson = JSON.stringify(JSON.parse(rawJson), null, 2); + // Update the element content + this.el.textContent = formattedJson; + } catch (error) { + console.error("Error formatting JSON:", error); + // Keep the original content if there's an error + } + } + } +}; + let liveSocket = new LiveView.LiveSocket(livePath, Phoenix.Socket, { transport: liveTransport === "longpoll" ? Phoenix.LongPoll : WebSocket, params: { _csrf_token: csrfToken }, + hooks: Hooks + }); // Show progress bar on live navigation and form submits diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index c0c23a5..0e02552 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -79,7 +79,11 @@ <.section title="Context"> -
+      
         <%= ErrorTracker.__default_json_encoder__().encode_to_iodata!(@occurrence.context) %>
       
diff --git a/priv/static/app.js b/priv/static/app.js index 9b539a6..f41d947 100644 --- a/priv/static/app.js +++ b/priv/static/app.js @@ -1 +1 @@ -var C=Object.create;var{defineProperty:m,getPrototypeOf:x,getOwnPropertyNames:E}=Object;var w=Object.prototype.hasOwnProperty;var F=(t,i,u)=>{u=t!=null?C(x(t)):{};const n=i||!t||!t.__esModule?m(u,"default",{value:t,enumerable:!0}):u;for(let s of E(t))if(!w.call(n,s))m(n,s,{get:()=>t[s],enumerable:!0});return n};var I=(t,i)=>()=>(i||t((i={exports:{}}).exports,i),i.exports);var y=I((b,g)=>{(function(t,i){function u(){n.width=t.innerWidth,n.height=5*r.barThickness;var e=n.getContext("2d");e.shadowBlur=r.shadowBlur,e.shadowColor=r.shadowColor;var o,a=e.createLinearGradient(0,0,n.width,0);for(o in r.barColors)a.addColorStop(o,r.barColors[o]);e.lineWidth=r.barThickness,e.beginPath(),e.moveTo(0,r.barThickness/2),e.lineTo(Math.ceil(s*n.width),r.barThickness/2),e.strokeStyle=a,e.stroke()}var n,s,c,d=null,p=null,h=null,r={autoRun:!0,barThickness:3,barColors:{0:"rgba(26, 188, 156, .9)",".25":"rgba(52, 152, 219, .9)",".50":"rgba(241, 196, 15, .9)",".75":"rgba(230, 126, 34, .9)","1.0":"rgba(211, 84, 0, .9)"},shadowBlur:10,shadowColor:"rgba(0, 0, 0, .6)",className:null},l={config:function(e){for(var o in e)r.hasOwnProperty(o)&&(r[o]=e[o])},show:function(e){var o,a;c||(e?h=h||setTimeout(()=>l.show(),e):(c=!0,p!==null&&t.cancelAnimationFrame(p),n||((a=(n=i.createElement("canvas")).style).position="fixed",a.top=a.left=a.right=a.margin=a.padding=0,a.zIndex=100001,a.display="none",r.className&&n.classList.add(r.className),o="resize",e=u,(a=t).addEventListener?a.addEventListener(o,e,!1):a.attachEvent?a.attachEvent("on"+o,e):a["on"+o]=e),n.parentElement||i.body.appendChild(n),n.style.opacity=1,n.style.display="block",l.progress(0),r.autoRun&&function v(){d=t.requestAnimationFrame(v),l.progress("+"+0.05*Math.pow(1-Math.sqrt(s),2))}()))},progress:function(e){return e===void 0||(typeof e=="string"&&(e=(0<=e.indexOf("+")||0<=e.indexOf("-")?s:0)+parseFloat(e)),s=1f.default.show(300));window.addEventListener("phx:page-loading-stop",(t)=>f.default.hide());T.connect();window.liveSocket=T; +var C=Object.create;var{defineProperty:b,getPrototypeOf:x,getOwnPropertyNames:E}=Object;var F=Object.prototype.hasOwnProperty;var I=(n,s,u)=>{u=n!=null?C(x(n)):{};const t=s||!n||!n.__esModule?b(u,"default",{value:n,enumerable:!0}):u;for(let o of E(n))if(!F.call(t,o))b(t,o,{get:()=>n[o],enumerable:!0});return t};var w=(n,s)=>()=>(s||n((s={exports:{}}).exports,s),s.exports);var y=w((m,g)=>{(function(n,s){function u(){t.width=n.innerWidth,t.height=5*i.barThickness;var e=t.getContext("2d");e.shadowBlur=i.shadowBlur,e.shadowColor=i.shadowColor;var r,a=e.createLinearGradient(0,0,t.width,0);for(r in i.barColors)a.addColorStop(r,i.barColors[r]);e.lineWidth=i.barThickness,e.beginPath(),e.moveTo(0,i.barThickness/2),e.lineTo(Math.ceil(o*t.width),i.barThickness/2),e.strokeStyle=a,e.stroke()}var t,o,c,d=null,p=null,h=null,i={autoRun:!0,barThickness:3,barColors:{0:"rgba(26, 188, 156, .9)",".25":"rgba(52, 152, 219, .9)",".50":"rgba(241, 196, 15, .9)",".75":"rgba(230, 126, 34, .9)","1.0":"rgba(211, 84, 0, .9)"},shadowBlur:10,shadowColor:"rgba(0, 0, 0, .6)",className:null},l={config:function(e){for(var r in e)i.hasOwnProperty(r)&&(i[r]=e[r])},show:function(e){var r,a;c||(e?h=h||setTimeout(()=>l.show(),e):(c=!0,p!==null&&n.cancelAnimationFrame(p),t||((a=(t=s.createElement("canvas")).style).position="fixed",a.top=a.left=a.right=a.margin=a.padding=0,a.zIndex=100001,a.display="none",i.className&&t.classList.add(i.className),r="resize",e=u,(a=n).addEventListener?a.addEventListener(r,e,!1):a.attachEvent?a.attachEvent("on"+r,e):a["on"+r]=e),t.parentElement||s.body.appendChild(t),t.style.opacity=1,t.style.display="block",l.progress(0),i.autoRun&&function v(){d=n.requestAnimationFrame(v),l.progress("+"+0.05*Math.pow(1-Math.sqrt(o),2))}()))},progress:function(e){return e===void 0||(typeof e=="string"&&(e=(0<=e.indexOf("+")||0<=e.indexOf("-")?o:0)+parseFloat(e)),o=1f.default.show(300));window.addEventListener("phx:page-loading-stop",(n)=>f.default.hide());T.connect();window.liveSocket=T; From 63c01a63cef761ec2d9960636cf2b6b7bf5a177c Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 28 Feb 2026 12:18:52 +0100 Subject: [PATCH 4/5] Avoid compile-time warnings --- lib/error_tracker.ex | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index a2b27e1..b05b699 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -384,12 +384,23 @@ defmodule ErrorTracker do occurrence end + @default_json_encoder (cond do + Code.ensure_loaded?(JSON) -> + JSON + + Code.ensure_loaded?(Jason) -> + Jason + + true -> + raise """ + No JSON encoder found. Please add Jason to your dependencies: + + {:jason, "~> 1.1"} + + Or upgrade to Elixir 1.18+. + """ + end) + @doc false - def __default_json_encoder__ do - # Elixir 1.18+ includes the JSON module. On older versions we should fall back to Jason (which - # is listed as an optional dependency). - if Version.match?(System.version(), ">= 1.18.0"), - do: JSON, - else: Jason - end + def __default_json_encoder__, do: @default_json_encoder end From 86b26c664c7247351c658ce7a815f06f8de45639 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 28 Feb 2026 12:21:08 +0100 Subject: [PATCH 5/5] Better error handling --- lib/error_tracker/schemas/occurrence.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/schemas/occurrence.ex b/lib/error_tracker/schemas/occurrence.ex index f579799..b57b91d 100644 --- a/lib/error_tracker/schemas/occurrence.ex +++ b/lib/error_tracker/schemas/occurrence.ex @@ -60,7 +60,7 @@ defmodule ErrorTracker.Occurrence do context rescue - _e in Protocol.UndefinedError -> + _e -> Logger.warning( "[ErrorTracker] Context has been ignored: it is not serializable to JSON." )