Error Monitoring & Crash Reporting

Find out when a Manifest V3 extension fails on users' machines — capturing errors in every context, reporting within a privacy policy, symbolicating stacks, and turning reports into fixes.

An extension runs on machines you will never see, on websites you have never visited, against browser versions you did not test, and it fails there silently. The service worker throws during a cold start and is evicted with the error unlogged; a content script hits a DOM shape it did not expect on one site in a thousand; a migration fails for users who skipped three versions. Without monitoring, the first signal is a one-star review that says “stopped working”. This section covers building the feedback loop that turns those silent failures into actionable reports, within Testing, Debugging & Performance Optimization.

Error monitoring in an extension is harder than in a web application for three reasons. There are several isolated contexts, each needing its own capture. The most important one — the service worker — can be evicted before a report is sent. And the extension often holds sensitive permissions, so anything it transmits is scrutinised by users and store reviewers alike. Each guide below handles one of those.

From a failure on a user's machine to a fixAn error is captured in whichever context it occurs, scrubbed of personal data, queued durably in storage, sent in a batch, symbolicated against uploaded source maps, grouped, and prioritised by version and frequency.Error in any contextworker, page, contentCapture + scrubno URLs, no contentDurable queuestorage.localsent when safe, in batchesSymbolicateuploaded source mapsGroup by versionnew vs regressedFix + verifyerror rate drops
The durable queue is the extension-specific step — without it, errors from an evicted worker are lost before they are sent.

Prerequisites checklist

  • A decision on where reports go — a hosted service such as Sentry, or your own endpoint — and a privacy policy that says so.
  • A list of every context the extension runs code in: service worker, each extension page, offscreen document, content scripts.
  • Source maps generated at build time and kept out of the shipped package.
  • The extension version available at runtime, so every report can be bucketed by release.
  • A scrubbing rule for what must never leave the device: page URLs, page content, user identifiers, tokens.
  • An opt-out — or, for extensions holding sensitive permissions, an opt-in — for error reporting.

Manifest registration

 1{
 2  "manifest_version": 3,
 3  "version": "2.4.1",
 4  "permissions": ["storage", "alarms"],
 5  "host_permissions": [
 6    "https://errors.example.com/*"          // your reporting endpoint — nothing broader
 7  ],
 8  "content_security_policy": {
 9    "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://errors.example.com"
10  }
11}

Execution context: parsed at install. Declaring the reporting host explicitly — and restricting connect-src to it — makes the extension’s only telemetry destination visible to reviewers and to curious users. A reporting SDK that tries to contact another host fails loudly under this policy, which is exactly the behaviour you want, as set out in writing a strict Content Security Policy for MV3.

1. Capture in every context

Each context needs its own global handlers. The worker has self.addEventListener("error") and "unhandledrejection"; extension pages have the same on window; content scripts must filter to errors from their own files, because the page’s errors arrive on the same window.

1// shared/capture.js — imported first in every entry point
2export function installCapture(ctx) {
3  const target = typeof window !== "undefined" ? window : self;
4  target.addEventListener("error", (e) => enqueue(ctx, e.error ?? new Error(e.message)));
5  target.addEventListener("unhandledrejection", (e) => enqueue(ctx, e.reason instanceof Error ? e.reason : new Error(String(e.reason))));
6}

Execution context: every extension context. The details — including content-script filtering and errors thrown before the handler is installed — are in capturing uncaught errors in every context.

2. Queue durably, send later

A report built in a worker that is about to be evicted must be on disk before it is sent, or it is lost.

1async function enqueue(ctx, err) {
2  const { errQueue = [] } = await chrome.storage.local.get("errQueue");
3  errQueue.push(scrub({ ctx, name: err.name, message: err.message, stack: err.stack, v: chrome.runtime.getManifest().version, t: Date.now() }));
4  await chrome.storage.local.set({ errQueue: errQueue.slice(-50) });
5  chrome.alarms.create("err-flush", { delayInMinutes: 1 });
6}

Execution context: any extension context. The alarm flushes the queue from the worker in a batch, surviving any number of evictions in between — the same durability pattern as chaining alarms for long-running jobs.

3. Scrub before anything leaves

1function scrub(report) {
2  return {
3    ...report,
4    message: report.message.replace(/https?:\/\/[^\s)]+/g, "<url>").slice(0, 300),
5    stack: (report.stack ?? "").replace(/https?:\/\/(?!chrome-extension)[^\s)]+/g, "<url>").slice(0, 4000),
6  };
7}

Execution context: every context, before a report is queued. Error messages routinely contain the URL of the page the user was on — which, for an extension, is browsing history. Replacing URLs while keeping extension-origin frames preserves the stack’s usefulness and removes the privacy risk. The full policy is in reporting errors without breaking your privacy policy.

Error capture by contextService worker, extension pages, offscreen document and content scripts compared on the global handler used, what can be lost, and where the report is queued.ContextHandlerAt risk of lossQueue inService workerself error / unhandledrej…Eviction before sendstorage.localPopupwindow handlersPopup closesstorage.localOptions / side panelwindow handlersLowstorage.localOffscreen documentwindow handlersDocument closedstorage.localContent scriptwindow, filtered by filen…Tab closesMessage to worker
Content scripts are the only context that must filter — the page's own errors arrive on the same window.

4. Send in batches, from the worker

Once reports are queued in storage, sending them is the service worker’s job. Batching matters for three reasons: it keeps network activity to a minimum on the user’s machine, it survives eviction because the queue is durable, and it gives one place to apply consent, rate limits and a final scrub before anything leaves the device.

1chrome.alarms.onAlarm.addListener(async (a) => {
2  if (a.name !== "err-flush") return;
3  const { errorReporting } = await chrome.storage.sync.get("errorReporting");
4  const { errQueue = [] } = await chrome.storage.local.get("errQueue");
5  if (!errQueue.length) return;
6  if (errorReporting === false) return chrome.storage.local.remove("errQueue");
7  const res = await fetch("https://errors.example.com/v1/batch", { method: "POST", body: JSON.stringify(errQueue) });
8  if (res.ok) await chrome.storage.local.remove("errQueue");
9});

Execution context: the service worker, woken by the flush alarm. The queue is only cleared after a successful response, so a failed send is retried on the next flush; if the user has turned reporting off, the queue is discarded rather than kept “just in case”. Capping the queue at a few dozen entries when writing protects the device if the endpoint is unreachable for days.

5. Group, count and prioritise

A raw stream of error reports is not useful; grouped and counted, it is the clearest picture of extension health available. Group reports by a fingerprint made from the error type, a normalised message and the first stack frame inside the extension’s own code. Count occurrences and affected installs per group per version. Then prioritise by three questions: is this new in the latest version, how many installs does it affect, and does it block a core feature?

The version dimension is the one extensions most often lack and most need. Because updates roll out over hours or days, a new version and the previous one run side by side, and comparing their error rates per active install shows within hours whether a release is worse — early enough to halt a staged rollout before it reaches everyone, as described in rolling back a bad extension release.

6. Readable stacks without shipping source

Stack traces from a minified bundle point at column numbers in one enormous line, and they include an extension id that differs per install on Firefox and Safari. Two steps make them readable. At build time, generate source maps, upload them to the reporting service for the release, and delete them from the package so the original source never ships. At runtime, rewrite each stack frame’s extension URL to a stable prefix before sending, so the uploaded maps match regardless of engine or install. Hosted services such as Sentry support both steps; the details are in wiring Sentry into a Manifest V3 extension.

7. Failures that never throw

Error reporting sees exceptions. Many extension failures produce none: an alarm that is missing after an update simply never fires; a content script that finds no matching element on a redesigned site does nothing; a permission revoked by the user makes a feature silently inert. These need explicit signals. Record the last successful run of each scheduled job and flag any that is overdue; log a structured “selector miss” when a content script’s expected element is absent; and check granted permissions against enabled features on startup. Reported alongside exceptions — with the same privacy rules — they make silent failures as visible as crashes.

Error reporting is data collection, and it should be treated with the same care as any other. Extensions that hold sensitive permissions — history, all-sites host access — should ask before enabling it; extensions with narrow permissions can default it on with a clear, easy opt-out. Either way, the store’s data-disclosure form should list diagnostic data, the privacy policy should describe exactly what a report contains, and the payload should be built from an allowlist of fields so the description stays true as the code changes. The full treatment is in reporting errors without breaking your privacy policy.

9. When users report problems directly

Not every problem produces a report, and not every user has reporting enabled. For those cases, a diagnostics export on the options page gives users a one-click way to share what a developer needs: versions, granted permissions as counts, registered alarms and scripts, the storage schema, recent errors and log lines — with nothing personal in it and a preview before anything is copied. Combined with three short questions in the support form, it turns vague “it stopped working” reports into actionable ones, as described in diagnosing crashes from user reports.

10. Closing the loop

Monitoring only pays off if reports lead to fixes that stay fixed. When an error group is resolved, add a regression test built from the report — a fixture reproducing the user’s state, or a unit test for the failing function — and mark the group resolved in the version that contains the fix. If it reappears in a later version, the reporting service flags it as a regression rather than an old issue. Over time the extension accumulates exactly the tests its real users have needed, which is a far better guide to where testing effort belongs than any coverage percentage.

11. Choosing between a hosted service and your own endpoint

A hosted error service gives grouping, alerting, release tracking and source-map symbolication with little effort, and for most extensions it is the sensible choice. Its costs are a bundled SDK that adds tens of kilobytes to each context that loads it, a third-party host in the extension’s content security policy and privacy policy, and default integrations — automatic breadcrumbs, page error capture — that must be switched off in an extension, where they would capture the host website’s activity.

A small endpoint of your own is the alternative when the privacy story must be as simple as possible or the payload is already minimal. The queue-and-batch design above works unchanged against it; what you give up is grouping and symbolication, which must then be built or done by hand. A middle path many extensions take is to keep the SDK out of content scripts entirely — forwarding their errors to the worker with a few lines of code — and use a trimmed, scoped client only in the worker and extension pages.

12. Measuring the monitoring itself

A reporting pipeline can fail silently like anything else. A change to the content security policy that blocks the endpoint, a revoked ingest key, a consent default flipped by mistake — each stops reports arriving, and a quiet dashboard looks exactly like a healthy extension. Watch the volume of reports per active install per day; a sudden drop to zero after a release is more likely a broken pipeline than a perfect release. A daily synthetic report from a development build, sent through the same path, confirms end to end that reports still arrive.

13. Where to start

For an extension with no monitoring today, the order that returns the most for the least effort is: install capture handlers in the worker, first; add the durable queue and alarm-driven flush; add scrubbing and the consent setting; then extend capture to extension pages and, with filtering, content scripts. Source maps, version-based alerting and silent-failure signals follow once reports are flowing. Each step is independently useful, so the work can be spread across several releases without leaving the extension worse off at any point.

Whichever steps you take first, write down what a report contains and keep that description next to the code that builds it. The description is what goes into the privacy policy and the store disclosure, and keeping it beside the allowlist means anyone changing the payload sees immediately that the public description must change too. That small habit is what keeps an error pipeline trustworthy as the extension and its team grow, and it is usually the first thing a store reviewer or a privacy-conscious user will ask to see.

Review that description at least once a year, or whenever a new context, SDK or reporting destination is added, and treat any mismatch between it and the actual payload as a bug to fix before the next release.

MV3 constraints box

  • The worker can be evicted mid-report. Queue to storage first, send from an alarm.
  • Remote code is prohibited. A reporting SDK must be bundled, not loaded from a CDN, and must not use eval.
  • CSP restricts where reports go. List your endpoint in connect-src and host_permissions; nothing else can be reached.
  • Content scripts share window with the page. Filter errors by filename or you will report the site’s bugs as yours.
  • Source maps must not ship. Generate them, upload them to your reporting service, keep them out of the package.

Cross-browser notes

ConcernChrome / EdgeFirefoxSafari
Global error handlers in the workerself error + unhandledrejectionSame, on the event pageSame
Stack formatV8 (at fn (file:line:col))SpiderMonkey (fn@file:line:col)JavaScriptCore (fn@file:line:col)
Extension URL in stackschrome-extension://<id>/moz-extension://<uuid>/safari-web-extension://<uuid>/
Developer error viewchrome://extensions → ErrorsBrowser ConsoleWeb Inspector per context

What this section covers

The guides follow a report from failure to fix: capturing uncaught errors in every context covers the handlers and their edge cases; reporting errors without breaking your privacy policy covers scrubbing, consent and disclosure; wiring Sentry into a Manifest V3 extension covers a hosted service end to end, including source maps; and diagnosing crashes from user reports covers the manual route when a user writes in.

Time from a new bug shipping to the team knowingMedian time between a regression reaching users and the developers becoming aware, for extensions relying on store reviews, support email, and automated error reporting.Store reviews72 hoursSupport email30 hoursAutomated reporting1 hours
Automated reporting turns a days-long feedback loop into one measured in minutes after the rollout starts.