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.ex b/lib/error_tracker.ex index 327060b..b05b699 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,24 @@ defmodule ErrorTracker do Telemetry.new_occurrence(occurrence, muted) 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: @default_json_encoder end diff --git a/lib/error_tracker/schemas/occurrence.ex b/lib/error_tracker/schemas/occurrence.ex index 1c4288e..b57b91d 100644 --- a/lib/error_tracker/schemas/occurrence.ex +++ b/lib/error_tracker/schemas/occurrence.ex @@ -46,26 +46,32 @@ 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) - 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 + json_encoder = db_json_encoder || ErrorTracker.__default_json_encoder__() + _iodata = json_encoder.encode_to_iodata!(context) + + context + rescue + _e -> + 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 diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index ec045a8..0e02552 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -79,7 +79,13 @@ <.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}, 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;