Insights overview
Insights lets you observe what your Elixir application does in production.
Honeybadger records common Elixir activity automatically, including Phoenix requests, Ecto queries, LiveView lifecycle events, Oban jobs, Absinthe operations, and Finch and Tesla HTTP calls. From there, you can add context and custom events from your own code, then use BadgerQL to ask questions across the whole event stream. Any field you send is queryable as soon as it arrives, with no schema to define ahead of time.
Start with automatic instrumentation
Section titled “Start with automatic instrumentation”Set insights_enabled: true in your config and the package starts recording
events as soon as your app boots.
Add a built-in dashboard
Section titled “Add a built-in dashboard”Automatic events power built-in dashboards.
Add application context
Section titled “Add application context”Context adds fields to the current process. Once set, every event emitted from that process carries them.
Say the app is A/B testing a new checkout flow against the control. Each checkout request sets context like this:
Honeybadger.event_context(%{ checkout_variant: checkout_variant})The checkout_variant field is now on every Ecto event for that request.
You can group by it like any other field.
filter event_type::str == "MyApp.Repo.query" and isNotNull(checkout_variant::str)| stats count() as queries, avg(query_time::float) as avg_us by checkout_variant::str| sort queries desc| queries | avg_us | checkout_variant |
|---|---|---|
| 26815 | 287 | new |
| 11873 | 261 | control |
(MyApp.Repo.query is the telemetry prefix from your Ecto repo
configuration. Substitute your own.)
The new variant ran more than twice as many queries with similar per-query time. Keep in mind that adding another A/B variant will extend any of the examples here without the need to change anything on the Honeybadger side.
Go deeper: check for possible N+1 queries
The package attaches a request_id to every event from the same request.
To turn total Ecto work into queries per request, group events by
request_id first to get a per-request count, then aggregate by variant.
filter event_type::str == "MyApp.Repo.query" and isNotNull(checkout_variant::str)| stats count() as queries by request_id::str, checkout_variant::str| stats count() as request_count, avg(queries) as avg_q, percentile(95, queries) as p95_q by checkout_variant| sort p95_q desc| only toHumanString(request_count) as requests, toHumanString(avg_q) as avg_queries, toHumanString(p95_q) as p95_queries, checkout_variant| requests | avg_queries | p95_queries | checkout_variant |
|---|---|---|---|
| 631 | 42.18 | 97 | new |
| 638 | 18.61 | 31 | control |
The new variant runs more queries per request, and the p95 is much higher than control. That pattern often points at an N+1.
Record application events
Section titled “Record application events”Custom events record activity the framework cannot see at all. Phoenix knows a checkout request ran. Only your app knows whether the payment authorized:
Honeybadger.event("payment.authorized", %{ payment_provider: payment.provider, amount: checkout.total, currency: checkout.currency, authorization_id: payment.authorization_id})This query breaks down the amounts collected by variant and provider:
filter event_type::str == "payment.authorized"| stats count() as authorizations, sum(amount::float) as authorized_amount by checkout_variant::str, payment_provider::str| sort authorized_amount desc| authorizations | authorized_amount | checkout_variant | payment_provider |
|---|---|---|---|
| 413 | 34108.00 | new | stripe |
| 218 | 18722.00 | new | paypal |
| 418 | 32167.00 | control | stripe |
| 220 | 13639.00 | control | paypal |
Go deeper: more insights, same instrumentation
filter event_type::str == "payment.authorized" or controller::str == "MyAppWeb.CheckoutsController"| stats count(event_type::str == "payment.authorized") as auth_events by request_id::str, checkout_variant::str| stats count() as auths, count(auth_events > 0) as checkouts, checkouts / auths as conv_rate by checkout_variant::str| only conv_rate, checkout_variant| conv_rate | checkout_variant |
|---|---|
| 0.92 | new |
| 0.86 | control |
filter event_type::str == "payment.authorized"| stats sum(amount::float) as total by payment_provider::str, checkout_variant::str| sort total desc| only toHumanString(total) as revenue, payment_provider, checkout_variant| revenue | payment_provider | checkout_variant |
|---|---|---|
| 34,108 | stripe | new |
| 32,167 | stripe | control |
| 18,722 | paypal | new |
| 13,639 | paypal | control |
filter event_type::str == "phoenix.endpoint.stop" and controller::str == "MyAppWeb.CheckoutsController"| stats avg(duration::float) as avg_us by checkout_variant::str| only toHumanString(avg_us, "microseconds") as avg, checkout_variant| avg | checkout_variant |
|---|---|
| 38ms | new |
| 15ms | control |
filter event_type::str == "payment.authorized" or controller::str == "MyAppWeb.CheckoutsController"| stats count(event_type::str == "payment.authorized") as auth_events, min(@ts) as request_ts by request_id::str, checkout_variant::str| stats count(auth_events > 0) / count() as conv_rate by checkout_variant::str, bin(1h, request_ts) as hour| sort hour asc| conv_rate | checkout_variant | hour |
|---|---|---|
| 0.93 | new | 2026-06-26 14:00:00 |
| 0.86 | control | 2026-06-26 14:00:00 |
| 0.92 | new | 2026-06-26 15:00:00 |
| 0.86 | control | 2026-06-26 15:00:00 |
| 0.91 | new | 2026-06-26 16:00:00 |
| 0.87 | control | 2026-06-26 16:00:00 |
The new variant holds a consistent lead over control across the rollout window.