SDK 0.6.1 Guide

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.