SDK 0.6.1 complete reference

Everything needed to add Extension Report to a browser extension.

Use this page when you need the full contract: product intent, SDK setup, browser context rules, emitted events, direct ingestion, remote config, privacy rules, and troubleshooting. For a first integration, start with the Quick Start, then come back here for edge cases.

Reference updated 2026-07-10

Reading path

Which Doc to Use

Product model

What Extension Report Does

Extension Report is analytics for browser extension owners. It focuses on owner metrics that browser stores do not expose in enough detail: installs, updates, active installations, opens, toolbar pin state, versions, browsers, permissions, context menus, shortcuts, omnibox usage, notification engagement, uninstall feedback, errors, and custom product actions.

If a user asks an AI agent to add Extension Report to an extension, the correct default is to install @extension-report/js, initialize it once in the background/service worker, add the minimal manifest permissions, then add explicit helper calls only for browser gaps such as declared popups, optional permission prompts, notification shown events, content scripts, and product-specific actions.

Environment data belongs to the SDK context and to the compact environment_state_seen state event, not to custom product events. SDK 0.3.8+ sends SDK, extension, manifest, browser/OS, locale and timezone there. The current SDK also snapshots the lightweight event context at enqueue time so queued events keep the extension version they occurred under after an update. Screen, DPR, network, CPU, memory and touch stay in event/batch context when the browser exposes them.

SDK 0.3.9+ also treats identity and repeated SDK errors as state: changed traits or changed error messages emit immediately, while identical repeats are suppressed for 24 h. The first permission snapshot is a baseline, not a set of fresh grant events.

User telemetry opt-out is part of the product contract. SDK 0.3.8+ exposes setTrackingEnabled(enabled) and emits consent markers so owners can measure opt-out without continuing product telemetry after the user disables it.

Preferred path

Install and Initialize

The public project key starts with pk_er_. It is safe to ship in the extension. The project secret is server-side only and must never be bundled into a browser extension.

terminal
pnpm add @extension-report/js@0.6.1
background.ts
import { initExtensionReport } from "@extension-report/js";

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  instrumentation: {
    adoption: true,
    engagement: true,
    reliability: "full"
  }
});
manifest.json
{
  "manifest_version": 3,
  "name": "My extension",
  "version": "1.0.0",
  "permissions": ["storage", "alarms"],
  "host_permissions": ["https://extension.report/*"],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}

Add feature permissions such as contextMenus, notifications, commands, omnibox, or optional host permissions only when the extension already uses those features.

MV3 behavior

Browser Context Rules

Background/service worker

Initialize once with automatic instrumentation enabled. This is the normal setup.

The SDK registers lifecycle, alarm flushes, toolbar pin state, action clicks, context menus, permissions, notifications, shortcuts, and omnibox listeners when the browser exposes those APIs.

background.ts
import { initExtensionReport } from "@extension-report/js";

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  instrumentation: {
    adoption: true,
    engagement: true,
    reliability: "full"
  }
});

Declared popup

Needed when action.default_popup exists.

Chrome opens declared popups directly and does not fire chrome.action.onClicked in the background. The popup must report its own load once.

popup.ts
// popup.ts
import { connectExtensionReportUiSession } from "@extension-report/js";

connectExtensionReportUiSession({
  surface: "popup",
  entrypoint: "action_icon"
});

Options page and side panel

UI contexts should not register the full background listener set.

Import with instrumentation.automatic: false and call only the focused helper for that page.

ui-surface.ts
const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  instrumentation: { automatic: false }
});

await sdk.optionsPage.opened();
await sdk.sidePanel.opened();

Content scripts and injected UI

The background cannot infer interactions inside the page.

Send a message to the background or import the SDK with automatic instrumentation disabled and only call explicit helpers.

content-script.ts
// content-script.ts
chrome.runtime.sendMessage({
  type: "extension-report:track",
  name: "page_action_used",
  attributes: { surface: "content_script" }
});

// background.ts
chrome.runtime.onMessage.addListener((message) => {
  if (message?.type !== "extension-report:track") return;

  void sdk.trackCustomEvent(message.name, message.attributes);
});

Dashboard signals

Standard Events

Lifecycle and health

A user installs the extension, Chrome starts the service worker, the SDK records installed/started/update-available state, then a daily heartbeat proves the installation is still alive.

Dashboard use

Installs, active installations, version adoption, alive today, alive 7d, and at-risk installations. Development installs are excluded by default and can be included from the Report toggle.

Example

extension_started with source sdk_init and context.extension_version, install_type, browser_name, os, locale.

extension_installedextension_updatedextension_update_availableextension_startedextension_heartbeat

Extension surfaces

A user opens the popup, options page, side panel, or clicks the extension action. Declared popups open a lightweight session port from the popup page.

Dashboard use

Opened installations, popup users, popup session duration, side-panel users, options users, and alive-but-never-opened segments.

Example

extension_ui_closed with surface popup, duration_ms set, and attributes.entrypoint set to action_icon.

extension_ui_openedextension_ui_closedextension_options_openedextension_side_panel_opened

Adoption diagnostics

The SDK reads extension/browser environment and toolbar pin state when the background starts, then listens for toolbar pin changes when the browser supports it.

Dashboard use

Pinned vs not pinned, adoption by extension version, browser split, manifest version, and SDK rollout health.

Example

environment_state_seen with attributes.extension_version, sdk_version, manifest_version, browser_name, browser_version, os, locale, and timezone_offset_min. Screen/device stay in batch context when available.

toolbar_pin_state_seentoolbar_pin_state_changedenvironment_state_seen

Extension workflows

A user clicks a context menu item, runs a configured shortcut, or starts an omnibox workflow.

Dashboard use

Workflow adoption, power users, configured shortcuts, shortcut users, context-menu users, and omnibox users.

Example

context_menu_clicked with context_menu_id, context_menu_context, and attributes.selection true or false.

context_menu_clickedkeyboard_shortcut_configuredkeyboard_shortcut_missingkeyboard_shortcut_usedomnibox_session_startedomnibox_input_changedomnibox_input_entered

Permissions

A low-permission extension asks for tabs or a customer domain only when the user enables a feature, and records requested, granted, declined, and removed states.

Dashboard use

Permission funnel, grant rate, decline rate, removals, host permission adoption, and configuration health.

Example

permission_requested with permission_names ['tabs'], permission_origins ['https://example.com/*'], and attributes.reason.

permission_requestedpermission_state_seenpermission_grantedpermission_declinedpermission_removedhost_permission_grantedhost_permission_removed

Notifications

The extension shows an onboarding or alert notification, then the SDK captures shown, clicked, button clicked, closed, and closed-by-user outcomes.

Dashboard use

Notification shown-to-clicked conversion, button engagement, and user-dismissed notifications.

Example

notification_shown with attributes.notification_id and custom campaign metadata.

notification_shownnotification_clickednotification_button_clickednotification_closednotification_closed_by_user

Review funnel

The extension shows a review ask after a successful workflow and records whether the user clicks through.

Dashboard use

Review prompt exposure and click-through in event feeds and custom product analysis.

Example

review_prompt_clicked with attributes.placement settings.

review_prompt_shownreview_prompt_clicked

Aggregated product usage

A high-volume workflow counts pages scanned, rows processed, rules evaluated, or retries locally during the day.

Dashboard use

Top actions, high-frequency usage trends, and volume diagnostics without flooding raw event ingestion.

Example

usage_agg_daily with attributes.day 2026-07-07 and attributes.counters.pages_scanned 42.

usage_agg_daily

User telemetry consent

A Settings toggle lets the user disable or re-enable owner telemetry. The settings page stores the visible preference and asks the background SDK instance to update tracking state.

Dashboard use

Telemetry opt-out installations, opt-out rate over known installed base, and recent opt-in/opt-out movement.

Example

telemetry_opted_out with source sdk_consent, no user_id, no payload, then product telemetry is blocked.

telemetry_opted_outtelemetry_opted_in

Product-specific telemetry

The extension records product-specific actions such as rule_created, export_completed, settings_saved, or background job failures.

Dashboard use

Custom usage slices in event feeds and debugging context for extension owners.

Example

custom_event with attributes.custom_name rule_created, surface popup, and mode manual.

custom_eventsdk_error

Use where the browser has gaps

Explicit Helpers

User telemetry opt-out

Store the visible switch in extension storage, but call setTrackingEnabled(enabled) from the background SDK instance. The SDK persists consent, emits one opt-out or opt-in marker when state changes, clears queued telemetry on opt-out, and blocks future product telemetry until opt-in.

telemetry-consent.ts
// 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 onTelemetryChanged(enabled: boolean) {
  await chrome.storage.local.set({ telemetryEnabled: enabled });
  await chrome.runtime.sendMessage({
    type: "telemetry:set-enabled",
    enabled
  });
}

High-frequency counters

Use counters for frequent actions that should feed Top actions without creating one raw event per occurrence. Counters roll up as usage_agg_daily and are blocked automatically when tracking is disabled.

usage-counters.ts
await sdk.count("pages_scanned");
await sdk.count("rows_processed", 25);

Custom product events and errors

Use custom events for product behavior that Extension Report cannot infer from browser APIs. Keep names stable and attributes small.

product-events.ts
await sdk.trackCustomEvent("rule_created", {
  surface: "popup",
  mode: "manual"
});

try {
  await runBackgroundJob();
} catch (error) {
  await sdk.trackError(error);
}

Optional permissions

Manifest permission snapshots are automatic and feed configuration state plus the latest active permission list in installation details. Optional prompt funnels need the SDK wrapper because Chrome does not emit a global event for declined prompts.

permissions.ts
const granted = await sdk.permissions.request({
  permissions: ["tabs"],
  origins: ["https://example.com/*"],
  reason: "capture-current-tab"
});

if (granted) {
  await sdk.trackCustomEvent("tab_capture_enabled", {
    surface: "options_page"
  });
}

Notification shown events

Clicks, button clicks, closes, and user closes are automatic. Create notifications through the SDK when you also need shown-to-clicked conversion.

notifications.ts
await sdk.notifications.show({
  notificationId: "welcome",
  options: {
    type: "basic",
    title: "Ready",
    message: "The extension is configured.",
    iconUrl: "/icon.png"
  },
  attributes: {
    campaign: "onboarding"
  }
});

Only when the SDK cannot be used

Direct HTTP Ingestion

Prefer the SDK. Custom clients can send batches to POST https://extension.report/api/v2/events. A successful response returns ok, schema_version, accepted, received, and uninstall_token.

custom-client.ts
await fetch("https://extension.report/api/v2/events", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    schema_version: 2,
    project_public_key: "pk_er_...",
    installation_id: "stable-installation-id",
    sdk: {
      name: "custom",
      version: "1.0.0"
    },
    context: {
      extension_version: "1.0.0",
      manifest_version: 3,
      browser_name: "Chrome",
      browser_version: "126.0.0",
      os: "macOS",
      locale: "en-US",
      timezone_offset_min: -120
    },
    events: [
      {
        event_id: crypto.randomUUID(),
        name: "custom_event",
        occurred_at: new Date().toISOString(),
        source: "custom-client",
        attributes: {
          custom_name: "settings_saved"
        }
      }
    ]
  })
});
  • Request body limit: 1,000,000 bytes.
  • Batch limit: 200 events.
  • schema_version must be 2.
  • installation_id must be stable for one browser extension install.
  • event_id must be unique per event.
  • occurred_at should be an ISO 8601 timestamp.
  • attributes are limited to 16 KB per event and 128 KB per batch.
  • A global project-level flood limit also applies across installations and IPs.
  • Server-side custom clients can sign the exact raw request body with the project secret using x-extension-report-timestamp and x-extension-report-signature: sha256=<hex HMAC-SHA256 of timestamp.rawBody>.
  • HMAC is only enforced when security.ingestion_mode is server_proxy and security.require_ingestion_hmac is true. Do not use that mode for direct browser SDK traffic.
  • Respect retry-after and retry_after_ms on rate limits or ingestion outages.

Runtime controls

Remote Config and Uninstall Tracking

The SDK reads GET https://extension.report/api/v2/config?pk=... and merges the result with defaults. SDK 0.6.1groups instrumentation into Adoption, Engagement, and Reliability, with remote modes, deterministic rollout percentages, and browser/OS/version audiences. Core identity, lifecycle, queueing, delivery, and configuration remain one unit.

remote-config.ts
const config = await sdk.loadRemoteConfig(true);

if (config?.enabled === false) {
  // Telemetry is disabled remotely for this project.
}

const showReviewPrompt = await sdk.getFlag("review_prompt", false);
const betaPanel = await sdk.getFlag("beta_panel", false); // supports { rollout: 0.25 }
const cohort = await sdk.getConfigValue("rollout.cohort", "default");

const status = await sdk.getRemoteModuleStatus();
// { configVersion, modules: { core, adoption, engagement, reliability } }

Local instrumentation options are capability ceilings: a remote policy can reduce what the store artifact collects, but cannot add missing code or manifest permissions. Active MV3 clients converge on the next event or flush; dormant workers apply the policy on a later wake. The effective state and configuration version are exposed by getRemoteModuleStatus().

After the first successful batch, the API returns an uninstall token. The SDK stores it and, when supported, calls chrome.runtime.setUninstallURL with the project key, stable installation ID, token, version, and last-seen timestamp.

Implementation rules

Privacy and Data Hygiene

  • Do not send secrets, access tokens, email addresses, private document text, or full URLs with sensitive query strings in attributes.
  • Use userId only when the product owner needs signed-in user correlation. Prefer a stable internal ID, not an email address.
  • The public project key belongs in the extension. The secret key is server-side only.
  • Custom event names should describe behavior, not personal data.
  • Keep high-cardinality values out of attributes unless they are truly needed for debugging or segmentation.
  • A user-facing telemetry opt-out must call sdk.setTrackingEnabled(false), not only hide the UI or skip custom events.
  • Telemetry is enabled by default. setTrackingEnabled(false) persists the user choice and takes precedence over owner module controls.

Common integration failures

Troubleshooting

No events appear after installation

Check that the background/service worker imports the SDK once, the public key starts with pk_er_, the manifest includes storage and host permission for https://extension.report/*, and the extension has been reloaded after the code change.

Popup opens are missing

If manifest.action.default_popup is set, Chrome opens the popup directly and does not fire chrome.action.onClicked. Keep the SDK initialized in the background and call connectExtensionReportUiSession(...) from the popup.

The service worker stops before sending

The SDK writes events to extension storage first, uses alarms for retry/flush, and flushes best-effort. Keep storage and alarms permissions in the manifest.

Permission declines are not tracked

Chrome only broadcasts state changes. Use sdk.permissions.request() for optional permissions so requested, granted, and declined outcomes are all recorded.

Notification shown counts are zero

Chrome has listeners for clicks and closes, not for your own create calls. Use sdk.notifications.show() or call sdk.notifications.shown(notificationId).

Telemetry opt-out stays at zero

Use @extension-report/js 0.6.1, call sdk.setTrackingEnabled(enabled) from the background SDK instance when the user changes the visible setting, and do not only store a UI preference.

The opt-out marker is sent repeatedly

Only send telemetry:set-enabled from the Settings UI on an actual user change. Page reload should only read storage and render the switch. The SDK is idempotent, but repeated UI writes usually mean the integration is doing unnecessary work.

High-frequency product actions create too many events

Use sdk.count(name, by?) for frequent local actions. It emits one usage_agg_daily rollup per UTC day and is blocked automatically when user telemetry is disabled.

Custom events are too noisy

Use remote config sampling, blocked_event_names, max_events_per_minute, and meaningful event names. Keep high-cardinality values out of attributes when possible.