When my Phoenix SaaS app starts to see real traffic, I want to see crashes without tailing the logs. A database-backed dashboard that groups errors by fingerprint and shows me their last occurrence is enough for most of the work, and it does not require a third-party account.
In this tutorial I install the error_tracker feature with Live SaaS Kit,
raise an error on purpose from a controller, and find it on the error
dashboard. The starting point is a project with admin already installed,
because that is where the error dashboard link lands.
Step 1 - Install the error_tracker feature
First step is to run the installer.
mix saaskit.feature.install error_tracker
mix deps.get
mix ecto.migrate
The feature has no hard dependencies and a recommended_features = ["admin", "oban"]
list. With admin installed, the dashboard mounts under the protected
admin pipes; with oban installed, errors raised inside workers are
captured too.
The package added to mix.exs:
# mix.exs
{:error_tracker, "~> 0.9.0"}
The migration delegates to the library, which is the same pattern Oban uses.
# priv/repo/migrations/TIMESTAMP_add_error_tracker.exs
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration
def up, do: ErrorTracker.Migration.up()
def down, do: ErrorTracker.Migration.down()
end
Step 2 - Look at the config
The installer injects a config block in config/config.exs.
# config/config.exs
config :error_tracker,
otp_app: :my_app,
repo: MyApp.Repo,
enabled: Mix.env() != :test,
plugins: [ErrorTracker.Plugins.Pruner]
Note that enabled: Mix.env() != :test keeps the tracker off in the
test environment. So error assertions in tests do not race against the
tracker writing rows to the test database.
The Pruner plugin removes resolved errors on a schedule, which keeps
the table from growing forever in a long-running app.
Step 3 - Look at the router changes
The router gets two small additions. First, the macro import.
# lib/my_app_web/router.ex
use ErrorTracker.Web, :router
Then the dashboard mount. Because admin is installed, it lands under
the admin pipes.
# lib/my_app_web/router.ex
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
# other admin routes
error_tracker_dashboard "/errors"
end
A link is also injected into the admin developer page.
<.link navigate={~p"/admin/errors"} class="btn btn-info">
ErrorTracker
</.link>
If admin is not installed, the dashboard mounts at /errors in the
development scope instead, and I am responsible for putting it behind
authentication myself.
Step 4 - Raise an error on purpose
The feature does not generate a sample endpoint, so I add a deliberate raise to confirm the tracker is wired up. I put it in the controller that already exists.
# lib/my_app_web/controllers/page_controller.ex
def boom(_conn, _params) do
raise "boom"
end
And a route to reach it.
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
pipe_through :browser
get "/boom", PageController, :boom
end
With the server running, I visit http://localhost:4000/boom and let
Phoenix render its error page.
mix phx.server
Step 5 - Find the error on the dashboard
Now that I have the deliberate crash, I sign in as a superuser and open
/admin/errors. The dashboard shows one unresolved error grouped by its
fingerprint, with the kind (RuntimeError), the reason ("boom"), the
source function, and the last occurrence timestamp. Clicking through
opens the recent occurrences, each with the request context that was
attached at the time.
If I hit /boom a second time, the count goes up but the fingerprint
stays the same. So the dashboard groups recurring errors instead of
showing the same crash 50 times.
I remove the deliberate route and controller action before moving on. The tracker keeps the historical row, which I can resolve from the dashboard or leave for the Pruner.
Final Result
I now have a database-backed error dashboard at /admin/errors that
captures unhandled exceptions from controllers, LiveViews, and Oban
workers, and groups them by fingerprint. A natural next step is to add a
healthcheck endpoint so an external uptime probe can confirm the app is
up — that is covered in the next tutorial in the series.