How the SDK works
The quick start gets the first event flowing. This guide explains the moving parts: where to initialize the SDK, which browser APIs are automatic, which helpers are explicit, how events are queued, and what each class of signal means in the dashboard.
Need the full contract for Codex, Claude, or a custom integration? Open the complete reference.
Quick Start vs SDK Guide vs Complete Reference
Use the shortest document that answers the current question.
Quick Start
Install, manifest, initialize, verify. Use it for a new project or first event.
SDK Guide
Understand browser contexts, automatic listeners, helper calls, queueing, retries, and how dashboard metrics are produced.
Complete Reference
Full agent-ready contract, direct HTTP shape, privacy rules, limits, and troubleshooting.
SDK mental model
One background integration owns durable identity, local queueing, automatic browser listeners, and best-effort delivery.
What the SDK stores locally
- A stable installation ID scoped to the project public key.
- A persistent event queue before network delivery.
- Retry/backoff state after rate limits or ingestion outages.
- The user telemetry consent state used by
setTrackingEnabled(...). - The uninstall token returned after a successful batch.
What the SDK sends
- Event name, timestamp, surface, source, and optional attributes.
- Extension version, manifest version, browser, OS, screen, network, CPU, memory, touch, locale, and timezone offset.
- The current SDK snapshots lightweight event context at queue time, so delayed flushes keep the version and environment from when the event occurred.
- Permission names/origins, toolbar state, context menu IDs, and notification IDs when relevant.
- Consent markers when a user turns telemetry off or back on.
- No project secret. The browser extension only ships the public key.
1. Required setup
Initialize once in the background/service worker.
pnpm add @extension-report/js@0.6.1
import { initExtensionReport } from "@extension-report/js";
const sdk = initExtensionReport({
projectPublicKey: "pk_er_...",
instrumentation: {
adoption: true,
engagement: true,
reliability: "full"
}
});The SDK automatically registers every browser signal it can observe from the background/service worker. Unsupported APIs and missing permissions are ignored. Events are queued in local extension storage before network delivery; helper calls are best-effort and do not block extension behavior during a temporary outage.
"permissions": ["storage", "alarms"],
"host_permissions": ["https://extension.report/*"],
"background": { "service_worker": "background.js", "type": "module" }pnpm dlx @extension-report/doctor@0.6.1 --public-key pk_er_... --manifest manifest.json
Use the doctor before shipping to verify the manifest, public key, remote config endpoint, and a non-ingesting events dry-run.
2. UI opens
Action clicks are automatic unless Chrome opens a declared popup.
// manifest.json
"action": { "default_popup": "popup.html" }
// popup.ts
import { connectExtensionReportUiSession } from "@extension-report/js";
connectExtensionReportUiSession({
surface: "popup",
entrypoint: "action_icon"
});If the extension has no action.default_popup, the SDK sees chrome.action.onClicked from the background and records the open automatically. If Chrome opens a declared popup, it does not fire that background click event, so the popup page opens a lightweight runtime port with connectExtensionReportUiSession(...). The background SDK records the open, then records extension_ui_closed with duration_ms when the port disconnects.
3. Context menu clicks
Automatic when the Chrome `contextMenus` API is available.
`initExtensionReport(...)` listens to `chrome.contextMenus.onClicked` automatically. Use `trackCustomEvent(...)` only when you need a product-specific event in addition to the standard context menu signal.
4. Permissions
Manifest permissions and later changes are automatic.
On startup, the SDK snapshots chrome.permissions.getAll(), compares it with the last local snapshot, and emits only differences. The first observed manifest permissions are recorded as a baseline state snapshot, not as fresh grants. Later user removals or re-grants are tracked through both browser listeners and the next snapshot. Explore keeps the latest active permission list available in installation details.
const granted = await sdk.permissions.request({
permissions: ["tabs"],
origins: ["https://example.com/*"],
reason: "capture-current-tab"
});The wrapper above is advanced: use it only for optional permissions requested during usage, such as screenshots, tab capture, Gmail/Notion/Jira integrations, customer domains, or site-specific automation. It is required forpermission_requested and permission_declined because a declined prompt does not change browser state, so Chrome has no global event to broadcast.
5. Notifications
Click/close engagement is automatic. Shown events use the helper.
await sdk.notifications.show({
notificationId: "welcome",
options: {
type: "basic",
title: "Ready",
message: "The extension is configured.",
iconUrl: "/icon.png"
}
});The background listener records clicked, button-clicked, closed, and closed-by-user signals automatically. Use the helper for reliable notification_shown events, because Chrome does not emit a separate global listener event when your own code calls create.
6. Advanced contexts
Only use explicit calls where no browser-wide listener exists.
What automatic: false means
Use it when importing the SDK outside the background service worker. It prevents a popup, options page, side panel, or content script from registering a second copy of lifecycle, permission, notification, context-menu, shortcut, and omnibox listeners. The page can still call focused helpers like connectExtensionReportUiSession(...) or sdk.trackCustomEvent(...).
Optional permissions
Manifest permissions and later grant/remove changes are automatic. Use sdk.permissions.request(...) only for optional permissions requested during usage: screenshots, tab capture, Gmail/Notion/Jira integrations, a customer domain, or site-specific automation. The wrapper records requested and declined prompts; Chrome only broadcasts grants/removals.
Notification shown
Notification clicks, button clicks, and closes are automatic. Use sdk.notifications.show(...) only when you also need to count shown notifications and measure shown to clicked conversion.
Content scripts and product events
The background cannot infer interactions inside a content script or injected UI. Send a message to the background or call trackCustomEvent(...) for product actions such as settings_saved, rule_created, or export_completed.
7. Runtime instrumentation groups
SDK 0.6.1 keeps remote control understandable without turning the extension into a wall of switches.
Core
Installation identity, lifecycle, remote configuration, queue, transport and delivery. Core remains one indivisible base.
Adoption
Toolbar pin state, permission state and shortcut readiness.
Engagement
UI sessions, menus, shortcut usage, omnibox, notifications, review prompts, custom events and daily counters.
Reliability
Off, errors only, or errors plus network diagnostics.
The options shipped in the extension are local capability ceilings. The dashboard can publish a lower mode, a deterministic percentage rollout, or a browser, OS, and extension-version audience without a store release. It cannot enable code or manifest permissions that are absent from the artifact.
const status = await sdk.getRemoteModuleStatus();
// { configVersion, modules: { core, adoption, engagement, reliability } }An active MV3 client applies a new policy on its next event or flush. A dormant service worker applies it on a later wake or cache refresh, so the dashboard reports convergence rather than claiming permanent real-time connectivity.
8. User consent and opt-out
Keep the setting visible, durable, and connected to the background SDK instance.
Ownership model
The settings page owns the visible preference, usually stored in chrome.storage.local so the switch renders correctly after reload. The background owns telemetry enforcement because that is where the SDK is initialized with automatic instrumentation. The settings page should send a message only when the user changes the switch.
// background.ts
const sdk = initExtensionReport({
projectPublicKey: "pk_er_..."
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === "telemetry:get-enabled") {
void sdk
.isTrackingEnabled()
.then((enabled) => sendResponse({ ok: true, enabled }))
.catch((error) => sendResponse({ ok: false, error: String(error) }));
return true;
}
if (message?.type === "telemetry:set-enabled") {
void sdk
.setTrackingEnabled(Boolean(message.enabled))
.then(() => sendResponse({ ok: true }))
.catch((error) => sendResponse({ ok: false, error: String(error) }));
return true;
}
return false;
});// settings.ts
async function loadTelemetrySetting() {
const stored = await chrome.storage.local.get("telemetryEnabled");
return stored.telemetryEnabled ?? true;
}
async function onTelemetryChanged(enabled: boolean) {
await chrome.storage.local.set({ telemetryEnabled: enabled });
await chrome.runtime.sendMessage({
type: "telemetry:set-enabled",
enabled
});
}Default
Telemetry is enabled unless the user explicitly turns it off.
Off
The SDK emits one minimal telemetry_opted_out marker, clears queued telemetry and daily counters, then blocks track and count calls.
On again
The SDK emits telemetry_opted_in and resumes normal telemetry from that point forward.
Do not create a second automatic SDK instance in Settings only for this switch. Do not resend the set message when the settings page reloads. If the background wants to reconcile after an extension update, it can read the stored preference once and call setTrackingEnabled(...); the SDK is idempotent and only emits opt-in/opt-out events when the consent state actually changes.
// Optional background reconciliation after init.
const { telemetryEnabled = true } = await chrome.storage.local.get(
"telemetryEnabled"
);
await sdk.setTrackingEnabled(telemetryEnabled);In the dashboard, Telemetry opt-out is computed from the latest known consent event per installation. It is a current stock, not just the number of opt-out clicks. Opt-in and opt-out event counts remain available to understand recent movement.
9. Local counters
Aggregate frequent product actions without flooding event volume.
await sdk.count("pages_scanned");
await sdk.count("rows_processed", 25);Use sdk.count(name, by?) for high-volume actions where one event per occurrence would be waste: rows processed, pages scanned, rules evaluated, retries, or local automation steps. Counters are persisted locally and flushed as one usage_agg_daily event per UTC day, then merged into Top actions. When tracking is disabled by user consent, counters are disabled too.
10. Custom events and errors
Use these for product-specific behavior.
await sdk.trackCustomEvent("rule_created", {
surface: "popup",
mode: "manual"
});
try {
await runExtensionJob();
} catch (error) {
await sdk.trackError(error);
}Error diagnostics are privacy-safe by default. Failed request URLs keep origin and path only, response bodies are not read, and content script errors keep only the visited page origin. Richer diagnostics require an explicit instrumentation.errorDiagnostics opt-in.
const sdk = initExtensionReport({
projectPublicKey: "pk_er_...",
instrumentation: {
errorDiagnostics: {
includeQueryStrings: true,
includeResponseBodies: true,
maxResponseBodyBytes: 1000,
includeContentScriptUrls: "origin"
}
}
});11. Identity and traits
Use traits for current state; use custom events for actions.
await sdk.identify(accountId ?? null, {
entitlement_type: "paid",
entitlement_status: "active",
plan_code: "subscription",
trial_expires_at: null
});
await sdk.trackCustomEvent("upgrade_clicked", {
source: "settings"
});Call identify(...) when the user id or traits change. The user id may be null; that is the right shape for anonymous installs where you still know installation traits such as plan, license, trial, workspace, or cohort.
Keep product actions in trackCustomEvent(...). For example, an upgrade button click is an action; the current plan and license are traits. The dashboard materializes common plan/license traits into ClickHouse so they can be displayed and analyzed with installation activity.
12. Uninstall tracking and disclosure
The browser uninstall URL is armed after the first successful batch.
The ingestion API returns an uninstall token with successful batches. The SDK stores it, calls chrome.runtime.setUninstallURL, and refreshes the URL on service-worker wakes so the last-seen timestamp stays current.
For Chrome Web Store Data Usage, disclose extension usage analytics and diagnostics. Disclose identifiers if you call identify(...). Default error settings do not require browsing-history disclosure because content-script page URLs are reduced to origin.