Skip to content

Commit

Permalink
Safely convert payloads to strings (#110)
Browse files Browse the repository at this point in the history
For non-error tuples `Exception.normalize/3` returns the original
payload, which may be anything. Some payloads such as tuples don't
implement the `String.Chars` protocol. So calling `to_string/1` will
fail. In such cases we fall back to `inspect/1`.

I made a few updates to the `live.exs` script to reproduce this problem.
You can run `elixir live.exs` and click the button called «GenServer
timeout». This will cause an error that should be recorded by the error
tracker.

If you revert the changes that I made and try this same button you will
se that the error payload cannot be converted to string and detaches the
ErrorTracker telemetry handler.

Closes #109
  • Loading branch information
crbelaus authored Nov 26, 2024
1 parent cc6aacb commit 00f8271
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 9 deletions.
14 changes: 9 additions & 5 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,18 @@ defmodule ErrorTracker do

defp normalize_exception({kind, ex}, stacktrace) do
case Exception.normalize(kind, ex, stacktrace) do
%struct{} = ex ->
{to_string(struct), Exception.message(ex)}

other ->
{to_string(kind), to_string(other)}
%struct{} = ex -> {to_string(struct), Exception.message(ex)}
payload -> {to_string(kind), safe_to_string(payload)}
end
end

defp safe_to_string(term) do
to_string(term)
rescue
Protocol.UndefinedError ->
inspect(term)
end

defp exception_breadcrumbs(exception) do
case exception do
{_kind, exception} -> exception_breadcrumbs(exception)
Expand Down
72 changes: 68 additions & 4 deletions live.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
Mix.install([
{:phoenix_playground,
github: "phoenix-playground/phoenix_playground",
ref: "ee6da0fc3b141f78b9f967ce71a4fb015c6764a6"},
{:phoenix_playground, "~> 0.1.7"},
{:postgrex, "~> 0.19.3"},
{:error_tracker, path: "."}
])

Expand All @@ -13,6 +12,7 @@ end
Application.put_env(:error_tracker, :repo, ErrorTrackerDev.Repo)
Application.put_env(:error_tracker, :application, :error_tracker_dev)
Application.put_env(:error_tracker, :prefix, "private")
Application.put_env(:error_tracker, :otp_app, :error_tracker_dev)

Application.put_env(:error_tracker, ErrorTrackerDev.Repo,
url: "ecto://postgres:[email protected]/error_tracker_dev"
Expand All @@ -26,6 +26,29 @@ defmodule Migration0 do
def down, do: ErrorTracker.Migration.down(prefix: "private")
end

defmodule ErrorTrackerDev.TimeoutGenServer do
use GenServer

# Client

def start_link(_) do
GenServer.start_link(__MODULE__, %{})
end

# Server (callbacks)

@impl true
def init(initial_state) do
{:ok, initial_state}
end

@impl true
def handle_call(:timeout, _from, state) do
:timer.sleep(5000)
{:reply, state, state}
end
end

defmodule DemoLive do
use Phoenix.LiveView

Expand All @@ -47,6 +70,7 @@ defmodule DemoLive do
<button phx-click="inc">+</button>
<button phx-click="dec">-</button>
<button phx-click="error">Crash on handle_event</button>
<button phx-click="genserver-timeout">GenServer timeout</button>
<.link href="/?crash=mount">Crash on mount</.link>
<.link patch="/?crash=handle_params">Crash on handle_params</.link>
Expand All @@ -69,6 +93,11 @@ defmodule DemoLive do
raise "Crash on handle_event"
end

def handle_event("genserver-timeout", _params, socket) do
GenServer.call(TimeoutGenServer, :timeout, 2000)
{:noreply, socket}
end

def handle_params(params, _uri, socket) do
if params["crash"] == "handle_params" do
raise "Crash on handle_params"
Expand All @@ -78,7 +107,42 @@ defmodule DemoLive do
end
end

PhoenixPlayground.start(live: DemoLive, child_specs: [ErrorTrackerDev.Repo])
defmodule DemoRouter do
use Phoenix.Router
use ErrorTracker.Web, :router

import Phoenix.LiveView.Router

pipeline :browser do
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
end

scope "/" do
pipe_through :browser
live "/", DemoLive
error_tracker_dashboard "/errors"
end
end

defmodule DemoEndpoint do
use Phoenix.Endpoint, otp_app: :phoenix_playground
plug Plug.Logger
socket "/live", Phoenix.LiveView.Socket
plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2
plug DemoRouter
end

PhoenixPlayground.start(
endpoint: DemoEndpoint,
child_specs: [
{ErrorTrackerDev.Repo, []},
{ErrorTrackerDev.TimeoutGenServer, [name: TimeoutGenServer]}
]
)

# Create the database if it does not exist and run migrations if needed
_ = Ecto.Adapters.Postgres.storage_up(ErrorTrackerDev.Repo.config())
Expand Down

0 comments on commit 00f8271

Please sign in to comment.