---
title: 'Error Tracking'
source: 'https://academia.sh/en/courses/frontend-quality/error-tracking'
course: 'Frontend Quality'
language: en
updated: '2026-08-17T18:11:06+00:00'
license: 'CC BY-SA 4.0'
---

# Error Tracking

The points where client errors are caught, making the stack trace readable with a source map, grouping by generating a fingerprint, measuring a group's impact, and decisions about noise and privacy.

Metrics say how fast the application is; they do not say whether it works. If an exception
is thrown while the measurement table is rendering, that session's duration metrics keep
looking fine. The user looks at a blank screen and, most of the time, tells no one.

This lesson builds the mechanism that makes those silent failures visible: catching the
error, sending it, and reading thousands of events not one by one but in groups that trace
back to the same origin.

## Points of Capture

Client errors do not come from a single place. There are four separate sources, and each has
its own boundary.

Uncaught exceptions are reported through the document-level `error` event; the event object
is an `ErrorEvent` instance and carries the message, the source, the location, and the error
object. Unhandled rejected promises come through the `unhandledrejection` event — this is the
browser's counterpart to the unhandled rejection concept defined in the Asynchronous
JavaScript and the Runtime course. Resource loading errors do not bubble; they can be seen
only by listening during the capture phase. The fourth source is errors the application
reports itself: states that were caught but could not be recovered from.

```js
// error-collector.js — collecting client errors
const endpoint = '/errors';
const common = { version: '2.4.1', path: location.pathname };

function report(type, message, stackTrace) {
  navigator.sendBeacon(endpoint, JSON.stringify({ ...common, type, message, stackTrace }));
}

addEventListener('error', (event) => {
  if (event.target !== window) {           // resource loading error
    report('resource', `${event.target.tagName} failed to load`, event.target.src ?? '');
    return;
  }
  report('exception', event.message, event.error?.stack ?? '');
}, true);

addEventListener('unhandledrejection', (event) => {
  const reason = event.reason;
  report('rejection', String(reason?.message ?? reason), reason?.stack ?? '');
});
```

One boundary must be known from the start. An error thrown from a script loaded from a
different origin is given by the browser without detail: the message only says it is a
script error, the stack trace is empty. The reason is cross-origin isolation — the rules
from the Cross-Origin Resource Sharing lesson apply here too. Opening up the detail requires
the script to be requested cross-origin and the server to send the header that allows it.

The second boundary is at the component level. The error boundary in the Component-Based
Interface Development course catches an error thrown during rendering and shows a fallback
interface; these errors never reach the document-level listener and must be reported
separately.

## Making the Stack Trace Readable

The code in the production build is minified: function names have shrunk to single letters,
lines have been merged. A stack trace like this cannot be read. The fix is the **source
map** defined in the Modules, Tooling, and the Ecosystem course: a table that maps a
minified location back to the original file, line, and name.

The mapping is done not on the client but on the side that collects errors. The source map
is not shipped together with the published assets — it makes the original source readable.
It is produced separately during the build and uploaded only to the collection side.

## Grouping

A single defect produces thousands of events. An error on the same line fires once for every
user who opens the list; for this to be readable, events that trace back to the same origin
need to be gathered into a single group. The key for this is called a **fingerprint**.

The fingerprint is produced from the stable parts of the event, and the unstable parts are
left out. A file name carrying a build digest, line and column numbers, query strings, and
identity and numeric values in the message change with every release or every event; if
these enter the key, the same defect scatters across dozens of groups.

```js
// fingerprint.mjs — produces a grouping key from a stack trace
const FRAME = /^\s*at\s+(?:(.+?)\s+\()?([^()]+?)\)?$/;

export function frames(stackTrace) {
  return stackTrace
    .split('\n')
    .slice(1)
    .map((line) => line.match(FRAME))
    .filter(Boolean)
    .map((m) => ({ func: m[1] ?? '<anonymous>', source: m[2] }));
}

export function normalizeSource(source) {
  if (source === '<anonymous>' || source.startsWith('node:')) return source;
  return source
    .replace(/^[a-z]+:\/\/[^/]+/, '')           // origin
    .replace(/[?#][^:]*$/, '')                  // query and fragment
    .replace(/:\d+:\d+$/, '')                   // line and column
    .replace(/\.[0-9a-f]{6,}\.(m?js)$/, '.$1'); // build digest
}

export function normalizeMessage(message) {
  return message
    .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, '<id>')
    .replace(/\b\d+\b/g, '<number>');
}

function digest(text) {
  let h = 0x811c9dc5;
  for (const ch of text) {
    h ^= ch.codePointAt(0);
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h.toString(16).padStart(8, '0');
}

export function fingerprint(event, { depth = 3 } = {}) {
  const [firstLine] = event.stackTrace.split('\n');
  const [type, ...rest] = firstLine.split(': ');
  const key = [
    type,
    normalizeMessage(rest.join(': ')),
    ...frames(event.stackTrace)
      .slice(0, depth)
      .map((f) => `${f.func}@${normalizeSource(f.source)}`),
  ].join('|');
  return { key, digest: digest(key) };
}

export function group(events) {
  const groups = new Map();
  for (const event of events) {
    const { key, digest: id } = fingerprint(event);
    const bucket = groups.get(id) ?? { id, key, count: 0, sessions: new Set(), versions: new Set() };
    bucket.count += 1;
    bucket.sessions.add(event.session);
    bucket.versions.add(event.version);
    groups.set(id, bucket);
  }
  return [...groups.values()].sort((a, b) => b.count - a.count);
}
```

The following six events contain examples of the same defect coming from different releases
and different sessions.

```js
// report.mjs — grouping six error events
import { group } from './fingerprint.mjs';

const EVENTS = [
  { session: 's-1', version: '2.4.0', stackTrace:
`TypeError: Cannot read properties of undefined (reading 'value')
    at drawRow (https://north-slope.example/assets/table.4f2a9c.js:12:31)
    at Array.map (<anonymous>)
    at MeasurementTable (https://north-slope.example/assets/table.4f2a9c.js:40:22)` },
  { session: 's-2', version: '2.4.1', stackTrace:
`TypeError: Cannot read properties of undefined (reading 'value')
    at drawRow (https://north-slope.example/assets/table.9b1d70.js:12:47)
    at Array.map (<anonymous>)
    at MeasurementTable (https://north-slope.example/assets/table.9b1d70.js:41:22)` },
  { session: 's-3', version: '2.4.1', stackTrace:
`TypeError: Cannot read properties of undefined (reading 'value')
    at drawRow (https://north-slope.example/assets/table.9b1d70.js:12:47?v=3)
    at Array.map (<anonymous>)
    at MeasurementTable (https://north-slope.example/assets/table.9b1d70.js:41:22)` },
  { session: 's-2', version: '2.4.1', stackTrace:
`Error: measurement 84213 could not be saved (400)
    at submit (https://north-slope.example/assets/form.1c8e55.js:88:11)` },
  { session: 's-4', version: '2.4.1', stackTrace:
`Error: measurement 91007 could not be saved (400)
    at submit (https://north-slope.example/assets/form.1c8e55.js:88:11)` },
  { session: 's-5', version: '2.4.1', stackTrace:
`TypeError: contract.submit is not a function
    at save (https://north-slope.example/assets/form.1c8e55.js:120:8)` },
];

for (const g of group(EVENTS)) {
  console.log(`${g.id}  events=${g.count}  sessions=${g.sessions.size}  versions=${[...g.versions].join(',')}`);
  console.log(`          ${g.key}`);
}
```

```sh
node report.mjs
```

```
f76aa486  events=3  sessions=3  versions=2.4.0,2.4.1
          TypeError|Cannot read properties of undefined (reading 'value')|drawRow@/assets/table.js|Array.map@<anonymous>|MeasurementTable@/assets/table.js
9581ca18  events=2  sessions=2  versions=2.4.1
          Error|measurement <number> could not be saved (<number>)|submit@/assets/form.js
d3fd0a38  events=1  sessions=1  versions=2.4.1
          TypeError|contract.submit is not a function|save@/assets/form.js
```

Six events came down to three groups. The first group's three examples carried different
build digests, different line and column numbers, and one of them a query string;
normalization eliminated these. The second group's two examples contained different
measurement numbers; because the numbers were converted to a placeholder, they appeared as a
single defect.

Grouping can be miscalibrated in two directions. **Over-grouping** collects independent
defects into a single row: discarding the message entirely and looking only at the error
type leads to this. **Under-grouping**, on the other hand, splits a single defect into
separate groups per release, and none of their impact becomes visible. The measure of
calibration is usefulness: a group is drawn correctly if it can be closed with a single fix.

## Reading a Group

The numbers a group carries determine prioritization. **Event count** says how often the
defect fires, **affected session count** says how many people it touches. The two must be
kept separate: an error that fires thousands of times in a loop within a single session
tops the list by event count but concerns one user. Sorting is done by session count.

Version distribution is the second piece of information. If a group appears only in the
latest version, the defect arrived with that release and rollback is an option. First-seen
and last-seen times answer the same question on the time axis.

Noise is a separate matter. Errors from browser extensions and external scripts are not in
the application's code; they can be filtered out if the stack trace's first frame does not
belong to the application's domain. Errors produced by requests canceled while the user is
leaving the page are not real defects either.

Volume must be controlled. An error thrown inside a loop can send thousands of events from a
single session; a per-session rate limit and sampling cut this off. When sampling is
applied, the count of events not sent is also transmitted, otherwise the impact measure is
thrown off.

Privacy is the final decision. An error message can contain text the user typed, and the
address can carry an identifier. The collector masks known identifier formats and free text
before sending. Personal data is data that is hard to delete once it has been collected.

## Summary

- Client errors come from four points: uncaught exceptions, unhandled promise rejections,
  resource loading errors, and errors the application reports itself.
- An error from a script loaded from a different origin is given without detail; detail
  requires a cross-origin request and a permission header. The stack trace is resolved on
  the collection side with a source map.
- The fingerprint is produced from the error type, the normalized message, and the first few
  frames; build digest, line and column number, query string, and numbers in the message are
  left out.
- Six events came down to three groups: examples from different releases and different
  measurement numbers appeared as a single defect.
- Prioritization is done by affected session count, not event count; noise is filtered,
  volume is limited, and personal data is masked before sending.

## Course Wrap-Up

The Frontend Quality course tied four topics to the same principle: **there is no quality
claim without measurement.** The performance topic defined user-centric metrics and tied
them to a budget. The accessibility topic addressed criteria at the standard's level and
showed where automated auditing ends and manual verification begins. The security topic
turned client-side risks into layers of defense. This last topic answered the question
common to all three: when what was built breaks, what reports it? The answer ran in two
directions — tests that run before a change and monitoring that runs after a release. There
is a loop between the two: a defect seen in the field turns into a test that will never let
it through again; a case the test missed comes back in the field as an error group.

With this course, the Frontend Development curriculum also comes to a close. The path
started from the document — building structure with HTML and how the browser processes that
structure. Then it moved to styling: cascading, the box model, and visual properties. Layout
systems and responsive design addressed adapting that structure to different screen and
input conditions. The browser and the web platform turned the page from a document into an
application with behavior. Component-based development split that behavior into reusable
pieces; application architecture brought those pieces together into a whole with routing,
state, and data access. Rendering strategies asked whether the same application would be
produced on the server or the client, when, and at what cost. And this last course showed
how to know that all these decisions stay correct: from document to style, from layout to
behavior, from component to architecture, from rendering to quality.

One question was deliberately left out across the curriculum. How a button would work, what
role it would carry, which criteria it would meet in which states was covered; **why** that
button sits at that size, that color, and in that place was not. The rationale behind a
spacing scale, a typographic scale, color roles, and state design is a field of its own. The
Interface Design and Design Systems curriculum addresses this question: it ties visual and
interaction decisions to justifiable principles and turns those decisions into a system that
scales. Its first course, Fundamentals of Interface Design, is well suited to be read
alongside this curriculum — the decisions about the appearance of the components built here
find their rationale there.
