Skip to content
All posts

Stack trace normalization

A raw stack trace is terrible data. It's full of cosmetic noise — query strings, deploy hashes, minified chunk ids, line numbers that drift by one every edit. Feed it directly into any deduplication or file-resolution system and you'll create duplicates on every deploy and fail to find the files you're looking for. This post describes the normalization layer we run on every stack trace that enters the pipeline.

The problems a raw stack causes

Three specific failure modes show up with raw stacks. First, fingerprinting: if the stack contains a deploy-specific hash (for example app.3a4b1c.js), every deploy creates a new fingerprint for every error and the dashboard fills with duplicates. Second, file resolution: the analyzer needs to fetch the file from the repository, but the stack references a bundle URL that isn't a valid repo path. Third, diffing across time: you can't tell whether today's TypeError at line 42 is the same bug as yesterday's if the file has been edited and line numbers shifted.

Normalization fixes all three by transforming the stack into a stable, repository-relative form before any downstream logic reads it.

The transformations

Normalization runs a small sequence of transformations on each file reference in the stack:

  • Strip the scheme and host. https://app.example.com/_next/static/chunks/pages/dashboard.3a4b1c.js becomes _next/static/chunks/pages/dashboard.3a4b1c.js.
  • Strip query strings. foo.js?v=123 becomes foo.js.
  • Strip Next.js bundle prefixes. _next/static/chunks/ is removed, since the equivalent source file in the repo sits at a different path.
  • Replace hex chunks. Runs of hexadecimal characters eight or longer are replaced with a placeholder, so bundle hashes stop shattering the fingerprint.
  • Replace numeric id segments. URL segments that look like ids (long pure-number or UUID-like runs) collapse to a :id placeholder.

The output is a stable string that's either a repo-relative path or something close enough that the VCS will recognize it when fetched.

Feeding fingerprinting

The fingerprint mixes the normalized message, error type, normalized source file, and a signature of the top frames. The signature itself is built from up to five normalized file:line pairs, with the line numbers snapped to their nearest 10-bucket. That composition is what makes the fingerprint robust across deploys while still splitting genuinely different bugs.

Snapping is an important detail. Without it, an innocent edit that adds two lines above a function shifts every error in that file to a new fingerprint. With it, the fingerprint tolerates normal editing while still distinguishing different functions in the same file.

Feeding file resolution

When the analyzer needs to fetch source to send to the model, it uses the normalized path. If error.source.file isn't set on the event, the analyzer parses up to eight frames out of the normalized stack and fetches each from the repo. Paths that fail to resolve are dropped silently; the remaining set is what the model sees.

The cap of eight frames (up from three) matters for deep async stacks. A promise rejection inside a Next.js route handler might have fifteen frames of framework glue above the real cause; we need to look deep enough to get there. The deny list and 404 responses prune the noise.

When normalization isn't enough

A few classes of error produce stacks that normalization can't rescue. Cross-origin script errors are opaque by browser design — "Script error" with no frames. Errors thrown from inline scripts without source maps are tricky. Errors captured from web workers sometimes have truncated stacks. In each case, the pipeline falls back to whatever information is present: message, error type, and file if available. Fingerprints are less robust but the pipeline still processes the error end to end.

Source maps help when present. If the browser sends mapped frames, the normalized path ends up closer to the actual repo path. We don't re-run source maps server-side today — the SDK is expected to do that before shipping the event. If you're seeing opaque stacks in the dashboard, check that source maps are uploaded and the SDK is picking them up.

Inspecting the normalization

Each error record on the dashboard shows the raw stack in the expanded detail view. The normalized file path shows up next to the severity badge. The fingerprint shown is the first sixteen characters of the full hash, which is enough to confirm uniqueness at a glance. If two errors should be the same but have different fingerprints, comparing their normalized file paths is the fastest way to diagnose the drift.

Evolution

Normalization rules evolve as we see new stack shapes in the wild. The current set was tuned against Next.js (both app and pages routers), Express, Fastify, and plain Node scripts; it handles browsers with reasonable behaviour out of the box. When we add support for a new framework, normalization typically gains a rule or two specific to that framework's bundle conventions.

If your stacks come from a framework we don't explicitly handle and the dashboard is showing duplicates or failing to fetch files, the normalization layer is almost certainly the gap. Reach out via contact with a sample stack and we'll add the rule.