Grouping and Deduplicating Extension Errors
Keep extension error reports actionable: fingerprinting by normalised stack, separating your errors from page noise, rate-limiting duplicates on the client, handling expected errors like closed tabs, and grouping by context and browser.
Table of Contents
The error dashboard has 4,000 issues. Most are the same three bugs split into hundreds of groups because the extension ID, page URLs or error message details differ; another large share are errors thrown by the web pages your content script runs on, not by your code; and a few hundred are expected conditions — “No tab with id”, “Cannot access contents of the page” — reported as if they were bugs. Real regressions drown. Extensions produce an unusually noisy error stream because they run in many contexts and inside other people’s pages. Grouping, filtering and client-side deduplication turn that stream into a short list of real problems. This guide shows how. It belongs to error monitoring and crash reporting.
Why extension errors fragment
Error services group events by a fingerprint, usually derived from the exception type, message and top stack frames. Extension errors defeat default fingerprints in predictable ways: frame URLs include per-installation IDs (Firefox’s UUIDs are random per install); messages include variable data (“No tab with id: 18342”); the same bug in Chrome and Firefox yields different message wording; and content scripts see errors from page scripts through global handlers. Fixing grouping means normalising the inputs to the fingerprint, filtering out events that are not yours, and classifying expected conditions so they are recorded differently or not at all.
Step-by-step: cleaner error groups
1. Drop errors that aren’t yours
1// error-filter.js
2const ORIGIN = chrome.runtime.getURL("");
3
4export function isOurs(error) {
5 const stack = String(error?.stack ?? "");
6 return stack.includes(ORIGIN) || stack.includes("app:///");
7}
8
9// content script global handler
10addEventListener("error", (e) => {
11 if (!isOurs(e.error)) return; // page script error — not ours
12 report(e.error, { context: "content" });
13});
Execution context: every context, especially content scripts. Content scripts share window events with the page, so a global error listener sees the page’s own errors. Only report errors whose stack contains your extension’s origin. In isolated worlds, most page errors are not delivered to content-script listeners at all, but unhandledrejection and some framework errors can leak through; the filter makes it explicit. See capturing uncaught errors in every context.
2. Classify expected conditions
1const EXPECTED = [
2 { re: /No tab with id|Invalid tab ID/i, kind: "tab-closed" },
3 { re: /Cannot access (contents of|a chrome:\/\/) |Missing host permission/i, kind: "restricted-page" },
4 { re: /Receiving end does not exist|Could not establish connection/i, kind: "no-receiver" },
5 { re: /Extension context invalidated/i, kind: "context-invalidated" },
6 { re: /QUOTA_BYTES|MAX_WRITE_OPERATIONS/i, kind: "storage-quota" },
7];
8
9export function classify(error) {
10 const msg = String(error?.message ?? error);
11 return EXPECTED.find((x) => x.re.test(msg))?.kind ?? null;
12}
Execution context: every context. These errors happen in normal operation — the user closed a tab mid-operation, the page is a browser page, the content script was orphaned by an update. They should be handled in code, and if they reach the reporter, recorded as a low-severity tag or a counter rather than an issue. storage-quota is the exception worth reporting, because it means data is not being saved. See cleaning up orphaned content scripts after reload.
3. Normalise messages and frames
1export function normaliseMessage(msg) {
2 return String(msg)
3 .replace(/chrome-extension:\/\/[a-p]{32}\//g, "app:///")
4 .replace(/moz-extension:\/\/[0-9a-f-]{36}\//g, "app:///")
5 .replace(/https?:\/\/[^\s'")]+/g, "<url>")
6 .replace(/\b\d{3,}\b/g, "<n>")
7 .replace(/'[^']{20,}'/g, "'<str>'");
8}
9// "No tab with id: 18342" → "No tab with id: <n>"
Execution context: the reporter, before computing a fingerprint. Stripping extension IDs, page URLs, long numbers and long quoted strings makes the same bug produce the same message on every machine. It also removes personal data such as URLs from messages, which matters for privacy. See reporting errors without breaking your privacy policy.
4. Set an explicit fingerprint
1export function fingerprint(error, context) {
2 const top = String(error.stack ?? "")
3 .split("\n").slice(1, 4)
4 .map((l) => l.replace(/:\d+:\d+\)?$/, "").replace(ORIGIN, "app:///").trim()); // drop line:col (minified)
5 return [error.name, normaliseMessage(error.message), context, ...top].join("|");
6}
7
8// Sentry example
9Sentry.init({ beforeSend(event, hint) {
10 event.fingerprint = [fingerprint(hint.originalException, event.tags?.context)];
11 return event;
12}});
Execution context: the reporter. Including the context (service worker, popup, content) separates the same message thrown in different places. Dropping line and column numbers from minified frames keeps groups stable across releases where unrelated code shifts positions; after symbolication, the service can still show exact lines. See uploading source maps for readable stack traces.
5. Rate-limit duplicates on the client
1const seen = new Map(); // fingerprint → count (per context lifetime)
2
3export function shouldSend(fp) {
4 const n = (seen.get(fp) ?? 0) + 1;
5 seen.set(fp, n);
6 return n === 1 || n === 10 || n === 100; // first, then order-of-magnitude milestones
7}
Execution context: each context’s reporter. An error in a scroll or mutation handler can fire hundreds of times per page; sending each one wastes the user’s bandwidth and your event quota and can trip the service’s rate limits, dropping other events. Sending the first occurrence and a few milestones preserves the signal (“this happens a lot”) at a fraction of the cost.
6. Tag every event with context and browser
Tag events with context (sw, popup, options, sidepanel, content, offscreen), browser and version, and extension version. Grouping by fingerprint plus these tags lets you see that one issue is Firefox-only or started with a specific release, without fragmenting the group itself.
7. Review and merge groups regularly
Even with good fingerprints, some duplicates slip through after refactors. Schedule a short weekly triage: merge duplicates, mark expected conditions as ignored with a note, and link groups to tracker issues. Use the release tag to spot regressions — a group that first appeared in the latest release is the first thing to look at.
Common mistakes
- Reporting page errors from content scripts. Most “errors” aren’t yours.
- IDs and URLs in messages. Every event becomes its own group.
- Expected conditions as bugs. Real issues drown.
- No client-side rate limit. One loop floods your quota.
- Line numbers in fingerprints of minified code. Groups split every release.
Cross-browser variation
- Chrome / Edge: messages like “No tab with id” and “Cannot access contents of url”.
- Firefox: different wording (“Invalid tab ID”, “Missing host permission for the tab”); random UUIDs in URLs make normalisation essential.
- Safari: different messages again; keep classification patterns per browser and test them with real errors.
Verification
- Throw the same error on two machines and confirm one group.
- Trigger “No tab with id” by closing a tab mid-operation and confirm it is classified, not reported as a bug.
- Throw an error from a page script and confirm the content script does not report it.
- Loop an error 500 times and confirm three events at most.
FAQ
Should I filter on the client or the server?
Both. Client filtering saves bandwidth and privacy; server rules catch what slips through.
Will rate limiting hide how widespread a bug is?
No — the number of users affected is unchanged; only repeated events from one session are reduced. Milestone events carry counts.
How do I group errors from different browsers?
Normalise messages per browser to a common form where wording differs, or accept separate groups tagged by browser.
Can I use the same filters for logs?
Yes. Apply the same “is it ours” and normalisation steps to anything you ship off-device, including breadcrumbs.
Related
- Capturing uncaught errors in every context — collecting errors.
- Adding breadcrumbs to error reports — context per event.
- Alerting on error spikes after a release — using clean groups.
- Error monitoring and crash reporting — the parent topic.