Before an upgrade, an Octane conversion, or the first strangler route, we ask the same question: when something breaks in production, how do you find out? In most mature Ember apps the honest answer is "a customer tells us." There is usually an error tracker wired up, installed years ago, quietly reporting Script error. and undefined is not an object against a single 4 MB vendor.js with no line numbers. That is not telemetry. It is noise with a monthly invoice.
This matters more than it looks. Every incremental modernization plan depends on being able to ship a change and see whether it hurt anyone. If your error signal is unreadable, each upgrade step has to be defended by manual QA instead of by evidence, and the work slows to a crawl. Fixing reporting is usually a two-to-four day job. We do it first.
Start by reading what you already have
Open the tracker before touching code. Sort by event volume and look at the top twenty groups. In an aging app they tend to sort into four piles:
- Browser noise. Extension injections,
ResizeObserver loop completed with undelivered notifications, failed requests from ad blockers. Real, but not yours. - Network and aborted transitions.
TransitionAbortedand fetch failures from users navigating away mid-request. Ember raisesTransitionAbortedas part of normal operation; if it is in your top ten, your reporting is wrong, not your app. - Dead code paths. Errors from a feature retired in 2019 that still has a route stub.
- The actual bugs, usually buried below all of the above.
Write down the proportions. That ratio is the baseline you will report against, and it is also a useful line in an assessment: a team seeing 94% noise has effectively been running blind, whatever the dashboard says.
Source maps are the whole game
A stack trace without source maps tells you nothing you can act on. Getting them right in an Ember app has two halves.
First, produce them. A classic ember-cli build emits source maps in development but typically not in production, because ember-cli-build.js leaves sourcemaps to its default. Turn them on explicitly for production builds. Under Embroider and the Vite-based build the switch lives in the bundler config instead — build.sourcemap for Vite — and the output is per-chunk rather than one monolith, which is a significant improvement on its own: a trace points at a real module instead of at concatenated vendor soup.
Second, get them to the tracker and not to the public. Upload the .map files during CI as a build artifact and delete them from the deployed assets, or serve them from a path your CDN restricts. Do not ship unreferenced maps alongside minified code on a public origin unless you are comfortable with anyone reading your source. The upload step needs a release identifier that matches exactly what the running app reports — a git SHA is the simplest choice that cannot drift.
If you take nothing else from this post: maps plus a matching release ID converts an unreadable tracker into a usable one, and it costs a day.
Hook Ember's error paths, all of them
An Ember app can fail in several places, and a generic window.onerror integration catches only some of them. The ones worth wiring deliberately:
Ember.onerror. Errors raised inside the run loop land here. Without it, async failures inside actions and tasks can be reported with useless context, or not at all.- Route
erroractions and the application-level error substate. A rejectedmodel()hook bubbles to the route'serroraction. Report it there with the route name and the transition's intended URL attached; a trace that says "failed loadingorders.show" is worth ten anonymous promise rejections. unhandledrejection. Still the most common source of invisible failures in an app with ten years of promise-based service code.- Ember Data / request layer failures. Decide explicitly which HTTP statuses are events and which are expected. A 404 on a deleted record is usually not an incident; a 500 always is.
And filter the known-benign at the point of capture, not in the dashboard: TransitionAborted, aborted fetches, and the browser-noise list above. Teams that filter in the UI end up paying for events they ignore and still scrolling past them.
Add the context that makes a trace actionable
Three pieces of metadata do most of the work. The current route name, set in a router routeDidChange listener, so every event says where the user was. The release SHA, matching the uploaded maps. And a breadcrumb trail of transitions — route name and params on each routeWillChange — which turns "it crashed" into "it crashed after returning from checkout to the cart with a stale query param." User identifiers are optional and come with privacy duties; route context does not, and it is more useful than most teams expect.
Keep it working through the build modernization
This is where the reporting usually breaks again. Moving to Embroider and then to Vite changes how assets are named, how chunks split, and where the maps come from. Two practical cautions. Verify the upload after the first Embroider build rather than assuming it carried over; chunk filenames change and an upload step pinned to assets/vendor-*.js will silently stop matching. And keep an unminified staging build in the pipeline to compare against — if a trace is readable in staging and gibberish in production, the maps are the problem, not the code.
It is worth re-checking the same way after any dependency upgrade that touches the build. Error reporting is infrastructure that fails quietly; nothing turns red when it stops working.
What good looks like
After the work, a team should be able to answer three questions in under a minute: what is the error rate for the release we shipped this morning, which route is responsible for most of it, and what line of our source raised it. That is a low bar, and plenty of ten-year-old Ember apps clear it. The ones that do not are usually one short engagement away.
If you are planning an LTS run or an Octane conversion this quarter, do this first. Every subsequent step gets easier to defend, and you will be measuring the upgrade instead of hoping for it.