Chrome extensions run in a fragmented environment: background scripts in a hidden worker context, content scripts injected into arbitrary web pages, and UI code in isolated sandboxed frames. When errors occur, they often vanish into browser logs or fail to surface at all. Implementing Chrome extension error monitoring contexts across these boundaries requires understanding both how extensions isolate their code and how to bridge those silos back to a centralized error tracker. This guide covers the patterns and gotchas for reliably capturing every error in your extension.
Why Extension Error Tracking Is Different
Extensions aren't typical web applications. A service worker (formerly background page) runs silently in the browser; a content script shares a page with third-party JavaScript but lives in an isolated execution world; a popup or options page is a regular web page but with limited permissions. Each context has its own global scope and limited visibility into the others.
When an unhandled error occurs in a service worker, the browser console captures it—but you can't see it without opening the extension's debugging UI. Content script errors may be buried in a page's own console noise. The isolation that makes extensions secure also makes error collection transparent to a centralized system.
Traditional error tracking setups (like those you'd use for browser JavaScript error tracking) assume a single page context and direct DOM access. Extensions require a different approach: establish a communication bridge between isolated contexts and a central collection point.
Setting Up the Sentry SDK for Extensions
The Sentry JavaScript SDK (which is compatible with LightTrace—a faster, more affordable hosted alternative) works in extension contexts, but requires configuration. Install it in your extension's manifest-compatible way:
// manifest.json (MV3)
{
"manifest_version": 3,
"name": "My Error-Tracked Extension",
"permissions": ["tabs", "scripting"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
Then, initialize Sentry in your service worker. Since extensions are bundled, you can include @sentry/browser via a build step (webpack, esbuild, etc.):
// background.js
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
environment: "production",
tracesSampleRate: 1.0,
});
// Service worker setup
self.addEventListener("error", (event) => {
Sentry.captureException(event.error);
});
self.addEventListener("unhandledrejection", (event) => {
Sentry.captureException(event.reason);
});
// Your extension logic
console.log("Extension initialized");
The dsn should point to your LightTrace instance. If you're self-checking on localhost, use something like https://your-key@localhost:18080/1. In production, use your hosted LightTrace domain.
MV3 vs MV2: Chrome is phasing out Manifest V2. If you're still on MV2, replace service_worker with background: { "scripts": ["background.js"] } and use the same initialization code. Consider migrating to MV3 soon.
Capturing Errors from Background Scripts
Background scripts (service workers in MV3) are your extension's always-on engine: they handle alarms, respond to messages, and manage state. Errors here are invisible unless you attach listeners.
Attach global error and promise rejection handlers early:
// background.js (top of file, before other code)
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
tracesSampleRate: 1.0,
});
// Capture uncaught exceptions
globalThis.addEventListener("error", (event) => {
Sentry.captureException(event.error || event.message);
});
// Capture unhandled promise rejections
globalThis.addEventListener("unhandledrejection", (event) => {
Sentry.captureException(event.reason);
});
// Example: handle chrome.alarms
chrome.alarms.onAlarm.addListener((alarm) => {
try {
if (alarm.name === "fetch-data") {
doSomethingAsync();
}
} catch (err) {
Sentry.captureException(err);
}
});
Wrap async operations and explicitly capture errors:
async function doSomethingAsync() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) throw new Error(`API error: ${response.status}`);
const data = await response.json();
// Process data
} catch (err) {
Sentry.captureException(err);
// Optionally, add context
Sentry.setContext("async_task", { taskName: "fetch-data" });
}
}
Catching Errors in Content Scripts
Content scripts are injected into web pages, which means they share the page's DOM but execute in an isolated world (in MV3). Errors in content scripts are harder to observe because they're not visible to the page's console, and they don't appear in your extension's background logs.
The catch: content scripts can't directly import Sentry's full SDK due to CSP (Content Security Policy) restrictions on many pages. Instead, use a lightweight approach: send errors to the background script via chrome.runtime.sendMessage, and let the service worker report to LightTrace.
// content.js
(function setupErrorTracking() {
// Capture global errors
window.addEventListener("error", (event) => {
chrome.runtime.sendMessage({
type: "ERROR",
error: {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack || "",
},
});
});
// Capture unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
chrome.runtime.sendMessage({
type: "ERROR",
error: {
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack || "",
},
});
});
})();
// Example function
function injectScript() {
try {
// Interact with the page's DOM or window
const data = JSON.parse(document.body.textContent);
console.log(data);
} catch (err) {
chrome.runtime.sendMessage({
type: "ERROR",
error: {
message: err.message,
stack: err.stack,
context: "content_script_injection",
},
});
}
}
injectScript();
In your background script, listen for these messages and forward to Sentry:
// background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "ERROR") {
Sentry.captureException(new Error(request.error.message), {
contexts: {
content_script: {
url: sender.url,
frameId: sender.frameId,
context: request.error.context,
},
},
tags: {
source: "content_script",
},
});
}
});
This relay pattern is reliable and sidesteps CSP issues entirely.
Minimize bundle size: Content scripts add overhead to every page your extension touches. Keep content.js as small as possible—just error listeners and the message relay. Lazy-load heavier logic only when needed.
Handling Context Isolation and CSP Challenges
MV3's isolated worlds mean content scripts can't directly access window or document from the main page context (by design). This also means you can't inject a full Sentry SDK into a page via <script> tags in the page's context.
Content Security Policy on many sites blocks eval() and external script injection. If a page's CSP is strict, you may not be able to run even lightweight inline scripts. The message relay approach avoids these problems—you capture errors inside your extension's controlled context, not on the untrusted page.
For UI pages (popups, options, sidebars) that you fully control, you can use Sentry directly because there's no CSP restriction on your own HTML:
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Extension Popup</title>
</head>
<body>
<h1>Settings</h1>
<button id="save">Save</button>
<script src="popup.js"></script>
</body>
</html>
// popup.js
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
});
document.getElementById("save").addEventListener("click", async () => {
try {
await chrome.storage.local.set({ some_setting: true });
} catch (err) {
Sentry.captureException(err);
}
});
Best Practices for Extension Error Monitoring
1. Add context to every error report. Extensions are hard to debug remotely—make errors informative. Include the extension version, the affected page URL, user browser version, and what the user was doing:
Sentry.setContext("extension", {
version: chrome.runtime.getManifest().version,
manifestVersion: chrome.runtime.getManifest().manifest_version,
});
Sentry.setContext("page", {
url: sender.url,
title: document.title,
});
2. Use tags for filtering. Tag errors by origin (background, content, ui) so you can quickly filter in your error dashboard:
Sentry.captureException(err, {
tags: {
extension_context: "service_worker",
error_type: "api_call",
},
});
3. Strip sensitive data. Extensions often handle user API keys, tokens, or PII. Configure Sentry's data-scrubbing rules to redact these before transmission:
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
beforeSend(event) {
if (event.request?.url?.includes("api_key")) {
event.request.url = event.request.url.replace(/api_key=[^&]+/, "api_key=***");
}
return event;
},
});
4. Throttle high-volume errors. Some errors (like permission denied on restricted pages) may fire repeatedly. Use a sample rate or deduplication to avoid flooding your quota:
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
tracesSampleRate: 0.1, // 10% of transactions
beforeSend(event, hint) {
// Ignore certain errors entirely
if (hint.originalException?.message?.includes("iframe")) return null;
return event;
},
});
Permissions matter. If your extension requests https://sensitive-site.com but the user hasn't granted it, that's a feature, not a bug—don't track it as an error. Use error grouping to merge noisy errors and focus on actionable ones.
Debugging Extensions with LightTrace
Once errors are flowing into LightTrace, you gain a dashboard view of all extension failures. Stack traces show you the exact line and file. Use the GitHub source links feature to jump straight from a stack frame to your repository (if your extension source is public or you're viewing it internally).
Group errors by fingerprint so a single content-script CSP violation doesn't create 1,000 duplicate issues. Use breadcrumbs to trace the sequence of events leading up to an error—for example, log user actions (button clicks, form submissions) so you can replay the failure scenario.
Error tracking is crucial for extensions because they're always running silently in the background. A user might never report a bug; your dashboard becomes your window into what's failing.
Start tracking errors in minutes
Start monitoring your extension's errors today with LightTrace. Sign up free — 5,000 events per month, no credit card required. Set up is just a few lines of code.