2026-07-08 · 6 min read
Browser extension analytics for WXT and Plasmo: the MV3-safe setup
WXT and Plasmo remove a lot of extension build-system work. They do not remove the hard analytics problems: the background is still a Manifest V3 service worker, popups still disappear when the user clicks away, and the Chrome Web Store still cannot tell you which version caused churn. The right setup keeps telemetry in the background, persists before network delivery, and treats UI pages as short sessions.
The rule: initialize once in the background
The background service worker is the only place that can reliably observe lifecycle, alarms, permission changes, update availability, and uninstall URL refreshes. Initialize the SDK there once. Popup, options, and side-panel pages should only open a lightweight UI session back to the background.
Minimal manifest permissions{
"permissions": ["storage", "alarms"],
"host_permissions": ["https://extension.report/*"]
}storage keeps the queue durable across service-worker shutdowns. alarms wakes the worker for retry, heartbeat, and uninstall URL refresh. Host permission lets the extension post batches to the ingestion endpoint.
WXT
WXT projects can use the base SDK directly, but the WXT package can also run as a module: it adds the required manifest permissions and keeps the manual helpers available when you already own the background entrypoint.
Install exact SDK 0.6 packagespnpm add @extension-report/js@0.6.1 @extension-report/wxt@0.6.1wxt.config.tsexport default defineConfig({
modules: ["@extension-report/wxt/module"],
extensionReport: {
publicKey: "pk_er_..."
}
});entrypoints/background.tsimport { initExtensionReportWxt } from "@extension-report/wxt";
export default defineBackground(() => {
const sdk = initExtensionReportWxt({
projectPublicKey: "pk_er_...",
instrumentation: {
adoption: true,
engagement: true,
reliability: "full"
}
});
return sdk;
});entrypoints/popup/main.tsimport { connectExtensionReportWxtUiSession } from "@extension-report/wxt";
connectExtensionReportWxtUiSession({
surface: "popup",
entrypoint: "action_icon"
});Plasmo
Plasmo follows the same background-first model. The adapter is deliberately thin: no hidden build plugin, no remote code, no extra storage format.
Install exact SDK 0.6 packagespnpm add @extension-report/js@0.6.1 @extension-report/plasmo@0.6.1background.tsimport { initExtensionReportPlasmo } from "@extension-report/plasmo";
export const sdk = initExtensionReportPlasmo({
projectPublicKey: "pk_er_...",
instrumentation: {
adoption: true,
engagement: true,
reliability: "full"
}
});popup.tsximport { connectExtensionReportPlasmoUiSession } from "@extension-report/plasmo";
connectExtensionReportPlasmoUiSession({
surface: "popup",
entrypoint: "action_icon"
});What you get from this shape
Background initialization records install/update/startup, daily heartbeat, current environment, update availability, toolbar pin state when available, permission changes, and uninstall URL refresh after the first successful batch. UI sessions add popup/options/side panel opens and close durations without trying to run analytics inside a disappearing page.
For custom product behavior, keep using sdk.trackCustomEvent(...)from the background. Content scripts should message the background rather than sending analytics directly from visited pages.
SDK 0.6 keeps Core as one unit and groups runtime controls into Adoption, Engagement, and Reliability. The dashboard may reduce those shipped capabilities, roll them out progressively, or target browser and extension versions. Active clients apply the policy on their next event or flush; dormant service workers apply it on a later wake.
The result is boring in the best way: one SDK, one queue, one background owner, and small framework adapters that make the integration obvious to teams already using WXT or Plasmo.
Start with the quick start or inspect the complete SDK reference.