Diagnosing Crashes from User Reports

Turn a vague 'it stopped working' report into a fix — an in-extension diagnostics export, the questions that narrow MV3 failures quickly, reproducing a user's state locally, and closing the loop.

Published September 18, 2026 Updated September 18, 2026 8 min read
Table of Contents

Most bug reports for an extension arrive as a single sentence — “it doesn’t work anymore” — from someone who cannot open DevTools, does not know what a service worker is, and may be describing any of a dozen failures: a disabled extension, a revoked permission, a site that changed its markup, a migration that failed three versions ago. Automated error reporting catches thrown exceptions; user reports catch everything else, including the failures that never throw. The skill is turning the sentence into evidence quickly, without asking the user to become a developer. This guide is part of error monitoring and crash reporting.

What “stopped working” usually means

Root causes behind "it stopped working" reportsShare of vague user reports by eventual root cause in a sample of extension support tickets.Permission missing or revoked24 % of repor…Site changed its markup21 % of repor…Uncaught error in our code19 % of repor…Stale state after update14 % of repor…Conflict with another extension12 % of repor…Extension disabled / not pinned10 % of repor…
Fewer than a third involve an exception — which is why error reporting alone is not enough.

Step-by-step

1. Build a one-click diagnostics export

The single most valuable support tool is a button on the options page that copies everything you would ask for, with nothing personal in it.

 1async function diagnostics() {
 2  const manifest = chrome.runtime.getManifest();
 3  const [perms, alarms, scripts, local] = await Promise.all([
 4    chrome.permissions.getAll(),
 5    chrome.alarms.getAll(),
 6    chrome.scripting.getRegisteredContentScripts().catch(() => []),
 7    chrome.storage.local.get(["schema", "errQueue", "traceLog", "migrationFailed"]),
 8  ]);
 9  return {
10    version: manifest.version,
11    browser: navigator.userAgentData?.brands?.map((b) => `${b.brand} ${b.version}`).join(", ") ?? navigator.userAgent,
12    platform: navigator.userAgentData?.platform ?? "unknown",
13    permissions: perms.permissions,
14    hostCount: perms.origins.length,                 // count, not the list
15    alarms: alarms.map((a) => a.name),
16    registrations: scripts.map((s) => s.id),
17    schema: local.schema,
18    migrationFailed: !!local.migrationFailed,
19    recentErrors: (local.errQueue ?? []).slice(-10),
20    recentLog: (local.traceLog ?? []).slice(-40),
21  };
22}

Execution context: the options page. Every field answers a question you would otherwise have to ask: which version, which browser, which permissions are actually granted, whether scheduled jobs exist, whether a migration failed. Host permissions are reported as a count rather than a list because the list reveals which sites the user enabled. The log and error entries come from the persisted streams built in logging across contexts without losing messages.

2. Let the user see and copy it

1document.querySelector("#copy-diagnostics").addEventListener("click", async () => {
2  const text = JSON.stringify(await diagnostics(), null, 2);
3  preview.textContent = text;                      // shown before it is copied
4  await navigator.clipboard.writeText(text);
5  status.textContent = "Copied. Paste it into your support message.";
6});

Execution context: the options page, which is focused and can use the clipboard. Showing the text before copying lets the user see exactly what they are sharing — a small thing that builds a lot of trust, and a direct consequence of the rules in reporting errors without breaking your privacy policy.

3. Ask the three questions that narrow it fastest

Before diagnostics arrive — or when a user cannot find the button — three questions separate most causes:

  1. Does the toolbar icon still appear, and what happens when you click it? Distinguishes a disabled or unpinned extension from a broken one.
  2. Is it every site, or one site? Every site points at the extension; one site points at that site’s markup or its permission.
  3. When did it last work — before or after the browser or extension updated? Separates update-related state problems from everything else.

Put these in the support form itself, as three short fields, rather than in a reply that costs a round trip.

4. Map the diagnostics to a cause

1function triage(d) {
2  const hints = [];
3  if (d.migrationFailed) hints.push("Migration failed — check recentErrors for phase and from-version.");
4  if (!d.alarms.includes("daily-sync")) hints.push("Sync alarm missing — onInstalled rebuild did not run.");
5  if (!d.registrations.length && d.hostCount > 0) hints.push("Hosts granted but no content-script registration.");
6  if (d.hostCount === 0) hints.push("No host permissions — feature cannot run on any site.");
7  if (d.recentErrors.length) hints.push(`${d.recentErrors.length} recent errors — top: ${d.recentErrors.at(-1).message}`);
8  return hints;
9}

Execution context: a support tool, or a script you paste the user’s JSON into. Codifying the checks you find yourself doing by hand means the second person to handle support gets the same answers as the first — and the missing-alarm and missing-registration checks catch the post-update failures described in auditing scheduled alarms with getAll.

5. Reproduce with the user’s state

For state-dependent bugs, load a fresh profile with the user’s version and their storage shape. Diagnostics give you the schema version and the failure; a matching fixture from your release archive gives you the data.

1// In a fresh profile's worker console, with the user's version loaded unpacked
2const fixture = await (await fetch(chrome.runtime.getURL("test-fixtures/v2.1.0.json"))).json();
3await chrome.storage.sync.set(fixture.sync);
4await chrome.storage.local.set(fixture.local);
5chrome.runtime.reload();                                   // re-runs onInstalled as an update path

Execution context: a development build’s service worker console. The fixtures are the same ones kept for contract testing a storage schema — which is one more reason to keep one per released version.

6. Close the loop

When the fix ships, tell the user which version contains it, and add a regression test built from their report. A report that becomes a test is a bug that cannot return silently.

From a one-line report to a regression testA vague report is narrowed by three questions and a diagnostics export, triaged into a likely cause, reproduced with a matching storage fixture, fixed, and captured as a regression test."It stopped working"one sentenceThree questionsicon, sites, whenDiagnostics pasteversion, perms, alarms, errorsthentriage()likely causeReproduce with fixturetheir version + schemaFix + regression testtell the user
The diagnostics export does most of the work — it replaces a week of back-and-forth with one paste.

Site-specific failures

A fifth of vague reports trace to a website changing its markup, and those never throw in a way error reporting sees — the content script simply finds nothing to act on. Two habits turn them from mysteries into routine fixes.

First, make the content script say when it expected something and did not find it. A structured “selector miss” log line — which feature, which selector, the site category — appears in the diagnostics export and, aggregated from opt-in reports, tells you a site changed before most users notice.

1function need(selector, feature) {
2  const el = document.querySelector(selector);
3  if (!el) log.warn("selector-miss", feature, selector);
4  return el;
5}

Execution context: the content script. Logging the selector and feature — never the page’s URL or content — keeps the signal useful and private.

Second, keep selectors in one module, so a markup change is a one-file fix, and keep a fixture of each supported site’s relevant markup for tests, refreshed when a miss is reported. That turns “site X broke” into a failing test within minutes of receiving the diagnostics.

Evidence sources for each failure typeFive common failure types mapped to whether automated error reporting, the diagnostics export or the three support questions reveal them.FailureError reportingDiagnostics exportThree questionsUncaught exceptionYesYesPartlyPermission revokedNoYesPartlySite markup changedNoVia selector-miss logYes — one siteMissing alarm after updateNoYesYes — after updateExtension disabledNoNo — cannot openYes — icon question
No single source covers everything; the export covers the most, which is why it is worth building first.

Cross-browser variation

  • Chrome / Edge: navigator.userAgentData gives brand and version reliably; getRegisteredContentScripts and getContexts provide useful state for the export.
  • Firefox: userAgentData is not available — fall back to navigator.userAgent or browser.runtime.getBrowserInfo(), which returns name and version directly.
  • Safari: include the Safari and macOS/iOS versions; many Safari-only issues are OS-version specific. Some APIs used in the export may be missing — guard each with a catch.
  • All three: the options page is the one surface every user can reach, making it the right home for the export button on every engine.

Verification

  1. Click the export button on a fresh profile and confirm the JSON contains version, browser, permissions and alarms — and no URLs:
1const d = await diagnostics();
2JSON.stringify(d).match(/https?:\/\//g);
3// null

Execution context: the options page console. Any match means a field is leaking an address.

  1. Revoke a host permission and confirm triage() reports it.
  2. Delete an alarm and confirm the missing-alarm hint appears.
  3. Load a v2.1.0 fixture into a fresh profile of the current build and confirm the migration runs as it would for that user.

FAQ

Should the export be sent automatically?

No. Make it an explicit user action with a visible preview. Automatic diagnostics upload is exactly the kind of collection that needs disclosure and consent.

What if the user cannot open the options page?

That itself is diagnostic — the extension may be disabled, or its pages failing to load. Ask them to check chrome://extensions for the toggle and an Errors button, as described in reading errors from the extensions page.

How do I handle reports about conflicts with other extensions?

Ask which other extensions are installed, reproduce with them in a fresh profile, and look for shared keyboard shortcuts, context-menu crowding or two content scripts modifying the same elements.

Other Testing, Debugging & Performance Optimization Resources