# Extension Report AI integration documentation Canonical HTML: https://extension.report/docs/ai Canonical text: https://extension.report/docs/ai.txt Current SDK version: 0.6.1 Current SDK package: @extension-report/js@0.6.1 Current WXT adapter package: @extension-report/wxt@0.6.1 Current Plasmo adapter package: @extension-report/plasmo@0.6.1 Current diagnostics CLI: @extension-report/doctor@0.6.1 Current ingestion endpoint: https://extension.report/api/v2/events Current remote config endpoint: https://extension.report/api/v2/config?pk=pk_er_... Current owner stats endpoint: https://extension.report/api/v2/stats?pk=pk_er_... ## Product purpose Extension Report is a self-hosted analytics product for browser extension owners. It records owner metrics that product teams cannot reliably get from browser stores alone: installs, updates, active installations, opens, toolbar pin state, extension versions, browser versions, permissions, context menus, keyboard shortcuts, omnibox usage, notifications, consent opt-out, uninstall feedback, aggregated product usage, errors, and custom product events. Use it when a user asks to integrate Extension Report into a Chrome, Edge, Firefox, Safari, or other WebExtension-style extension. The preferred integration is the JavaScript SDK. Direct HTTP ingestion is available only when the SDK cannot be used. ## Preferred SDK setup Install: ```bash pnpm add @extension-report/js@0.6.1 # WXT projects may also install the adapter: pnpm add @extension-report/js@0.6.1 @extension-report/wxt@0.6.1 # Plasmo projects may also install the adapter: pnpm add @extension-report/js@0.6.1 @extension-report/plasmo@0.6.1 ``` Check an integration without ingesting events: ```bash pnpm dlx @extension-report/doctor@0.6.1 --public-key pk_er_... --manifest manifest.json ``` Initialize exactly once in the background/service worker: ```ts import { initExtensionReport } from "@extension-report/js"; const sdk = initExtensionReport({ projectPublicKey: "pk_er_...", instrumentation: { adoption: true, engagement: true, reliability: "full" } }); ``` Minimal Manifest V3: ```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" } } ``` The public key identifies the Extension Report project. It is safe to ship in the extension. Do not ship the project secret in the extension. ## Browser context rules - Background/service worker: initialize the SDK with automatic instrumentation enabled. This is the normal setup. - Popup with manifest.action.default_popup: keep the SDK initialized in the background and call connectExtensionReportUiSession(...) from the popup. - Options page and side panel: import the SDK with instrumentation.automatic: false and call the matching opened helper. - Content scripts and injected UI: either send a message to the background or import with instrumentation.automatic: false and only call explicit helpers/custom events. - Settings page telemetry toggle: keep the visible preference in extension storage, but message the background SDK instance and call sdk.setTrackingEnabled(enabled) there. Do not create a second automatic SDK instance in Settings. - Never initialize automatic instrumentation in multiple contexts for the same extension install. - Telemetry is enabled by default. Persisted user opt-out always wins after sdk.setTrackingEnabled(false). ## SDK 0.6 instrumentation modules - Core is always one unit: installation identity, lifecycle, remote configuration, queue, transport and delivery. - Adoption covers toolbar pin state, permissions and shortcut readiness. - Engagement covers UI sessions, menus, shortcut usage, omnibox, notifications, review prompts, custom events and daily counters. - Reliability supports off, errors only, or errors plus network diagnostics. - Set instrumentation.adoption, instrumentation.engagement and instrumentation.reliability in the shipped extension as local capability ceilings. Remote policy can reduce those capabilities but cannot enable code or manifest permissions absent from the store artifact. - The dashboard can publish module modes, deterministic rollout percentages and browser/OS/version audiences. Active MV3 clients apply a new policy on the next event or flush; dormant service workers apply it on a later wake or cache refresh. - sdk.getRemoteModuleStatus() returns the effective module state and configuration version so the dashboard can measure convergence. Popup example: ```ts // popup.ts import { connectExtensionReportUiSession } from "@extension-report/js"; connectExtensionReportUiSession({ surface: "popup", entrypoint: "action_icon" }); ``` Options and side panel example: ```ts const sdk = initExtensionReport({ projectPublicKey: "pk_er_...", instrumentation: { automatic: false } }); await sdk.optionsPage.opened(); await sdk.sidePanel.opened(); ``` Content script message example: ```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); }); ``` ## Standard events ### Lifecycle and health Events: extension_installed, extension_updated, extension_update_available, extension_started, extension_heartbeat When it fires: 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. Notes: Captured from the background/service worker and periodic alarms. Heartbeats are daily by default. The current SDK also records runtime.onUpdateAvailable to measure release lag and chrome.management.getSelf().installType to separate production from development installs. ### Extension surfaces Events: extension_ui_opened, extension_ui_closed, extension_options_opened, extension_side_panel_opened When it fires: 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. Notes: Action clicks are automatic only when Chrome does not open a declared default popup. Declared popups should use connectExtensionReportUiSession(...) so the background SDK records open and close duration. ### Adoption diagnostics Events: toolbar_pin_state_seen, toolbar_pin_state_changed, environment_state_seen When it fires: 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. Notes: Used to explain adoption by toolbar state and environment. SDK 0.3.8+ emits environment_state_seen instead of the deprecated extension_version_seen/browser_version_seen/sdk_version_seen/manifest_version_seen rows. ### Extension workflows Events: context_menu_clicked, keyboard_shortcut_configured, keyboard_shortcut_missing, keyboard_shortcut_used, omnibox_session_started, omnibox_input_changed, omnibox_input_entered When it fires: 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. Notes: Captured when the matching browser APIs exist and the extension has the matching manifest features. ### Permissions Events: permission_requested, permission_state_seen, permission_granted, permission_declined, permission_removed, host_permission_granted, host_permission_removed When it fires: 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. Notes: Manifest permission state is snapshotted automatically. The first snapshot is a baseline; grants are emitted only for later additions. The latest active permission list is available in installation details. Optional prompt requested/declined funnels require sdk.permissions.request(). ### Notifications Events: notification_shown, notification_clicked, notification_button_clicked, notification_closed, notification_closed_by_user When it fires: 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. Notes: Click and close listeners are automatic. Shown events require sdk.notifications.show() or sdk.notifications.shown(). ### Review funnel Events: review_prompt_shown, review_prompt_clicked When it fires: 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. Notes: The current SDK exposes sdk.review.promptShown() and sdk.review.promptClicked(). Call them around your own Chrome Web Store review prompt; the SDK does not display UI. ### Aggregated product usage Events: usage_agg_daily When it fires: 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. Notes: Emitted by sdk.count(name, by?) as one daily rollup instead of one event per occurrence. Counters are local, durable, and blocked when user telemetry is disabled. ### User telemetry consent Events: telemetry_opted_out, telemetry_opted_in When it fires: 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. Notes: Emitted only by sdk.setTrackingEnabled(enabled) when the persisted consent state changes. The opt-out marker is minimal and exists so owners can measure opt-out stock and rate. ### Product-specific telemetry Events: custom_event, sdk_error When it fires: 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. Notes: Use for extension-specific behavior and caught application errors. Error diagnostics strip request query strings, omit response bodies, and reduce content-script page URLs to origin by default. Do not send secrets or personal data in custom attributes. ## Explicit helpers Custom events and errors: ```ts await sdk.trackCustomEvent("rule_created", { surface: "popup", mode: "manual" }); try { await runBackgroundJob(); } catch (error) { await sdk.trackError(error); } ``` Error diagnostics opt-in: ```ts const sdk = initExtensionReport({ projectPublicKey: "pk_er_...", instrumentation: { errorDiagnostics: { includeQueryStrings: true, includeResponseBodies: true, maxResponseBodyBytes: 1000, includeContentScriptUrls: "origin" } } }); ``` Optional 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" }); } ``` High-frequency counters: ```ts await sdk.count("pages_scanned"); await sdk.count("rows_processed", 25); ``` Notifications: ```ts await sdk.notifications.show({ notificationId: "welcome", options: { type: "basic", title: "Ready", message: "The extension is configured.", iconUrl: "/icon.png" }, attributes: { campaign: "onboarding" } }); ``` User telemetry opt-out: ```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 }); } ``` 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 } } ``` ## Direct HTTP ingestion Prefer the SDK. If building a custom client, send batches to https://extension.report/api/v2/events: ```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" } } ] }) }); ``` Contract: - Max request body: 1,000,000 bytes. - Max events per batch: 200. - schema_version must be 2. - project_public_key is required. - installation_id must be stable for one browser extension install. - events[].event_id must be unique per event. - events[].occurred_at should be ISO 8601. - attributes max size: 16 KB per event and 128 KB per batch, measured as UTF-8 bytes. - Rate limit: roughly 120 requests per minute per project, installation, and IP, plus a global project-level flood limit. Respect retry-after and retry_after_ms. - Optional HMAC for server-side custom clients/proxies only: set x-extension-report-timestamp to the current Unix timestamp in milliseconds and x-extension-report-signature to sha256=. Signatures are accepted within a five-minute timestamp window; like Stripe/Svix-style signatures, they do not use nonces. Never ship the project secret in a browser extension. - HMAC is reserved for a server proxy: set remote config security.ingestion_mode to `server_proxy` and security.require_ingestion_hmac to true. Never enable this mode for direct browser SDK traffic because an extension must not contain the project secret. - Successful response includes ok, schema_version, accepted, received, uninstall_token, and config_version. ## Owner Stats API Server-side owners can read daily project metrics from https://extension.report/api/v2/stats. Do not call this endpoint from a browser extension because it requires the project secret. ```bash curl "https://extension.report/api/v2/stats?pk=pk_er_...&from=2026-07-01&to=2026-07-08" \ -H "Authorization: Bearer er_secret_..." ``` Contract: - pk query parameter is required and must be the project public key. - Authorization header is required: Bearer . - from and to are optional YYYY-MM-DD dates. Default range is the latest 30 days. - Maximum range is 366 days. - Rate limit is a 30-request IP burst with gradual refill. - Hosted ClickHouse-backed projects return schema_version 2 with days[].metrics fields such as alive, opened, pinned, configured, power_users, permission_requested, notification_clicked, and events. - Local or fallback deployments without ClickHouse can return schema_version 1 with the stored aggregate JSON shape. - Full browser documentation: https://extension.report/docs/api. ## Remote config The SDK reads https://extension.report/api/v2/config?pk=pk_er_... and merges it with defaults. Supported public keys include enabled, modules, plan, limits, heartbeat_period_minutes, flush_alarm_minutes, flush_debounce_ms, max_batch_size, max_queue_size, max_queue_bytes, max_events_per_minute, network_diagnostics, sampling, flags, blocked_event_names, debug, and min_sdk_version. SDK 0.4.0+ applies sampling deterministically per installation and event name. A sampled event is consistently kept or dropped for the same installation instead of changing on each service-worker wake. Client max_events_per_minute is enforced in memory for the current MV3 service-worker lifetime and can reset on wake. Treat it as a local safety valve; server ingestion rate limits remain authoritative. Plan limits are resolved server-side from remote config plan/limits and then exposed as SDK-compatible top-level limits. The public limits object includes SDK/product limits only; server-only values such as project_requests_per_minute are used by ingestion but are not returned by /api/v2/config. Use modules.adoption, modules.engagement and modules.reliability for SDK 0.6 controls. Each module supports a deterministic rollout and optional browser, OS and extension-version audience. Reliability mode is off, errors, or full. The legacy network_diagnostics key remains compatible and maps to the Reliability group. The current SDK exposes feature flags through sdk.getFlag(name, fallback) and nested values through sdk.getConfigValue(path, fallback). Flags can be boolean values or rollout objects such as { rollout: 0.25 }; rollouts are evaluated deterministically per installation. Rollout objects can include match audiences on browser_name, os, locale, install_type, extension_version, min_extension_version, and max_extension_version. If the current install does not match the audience, getFlag returns false. If remoteConfig.enabled is false in SDK options, getFlag returns the fallback. Successful event responses include config_version; active clients force a config refresh after a flush when the version changes. For extension store submissions, pin exact package versions such as "@extension-report/js": "0.6.1" instead of caret ranges, and run npx @extension-report/doctor@0.6.1 against the built manifest before submission. Public SDK packages are released lockstep: use the same version for @extension-report/js, @extension-report/wxt, @extension-report/plasmo, and @extension-report/doctor. ## Project alerts The dashboard's project alerts use lib/metrics/project-alerts.ts as the single source of truth for alert predicates. Settings can send the same alerts to a Slack or Discord webhook. Webhook URLs are stored server-side in ProjectAlertWebhook, limited to official Slack/Discord webhook hosts, and are never included in SDK remote config. ## Uninstall tracking After a successful event batch, the API returns uninstall_token. The SDK stores it and calls chrome.runtime.setUninstallURL with https://extension.report/u?pk=...&ins=...&t=...&v=...&ls=... when supported. SDK 0.3.13+ also refreshes this URL on service-worker wakes so the ls last-seen parameter stays current. The page records the uninstall and can collect optional feedback. ## Error diagnostics The SDK exposes sdk.trackError(error). With automatic instrumentation enabled, it also captures uncaught errors and wraps fetch so recent failed requests can be correlated with later sdk_error reports. Defaults: - request URLs keep origin and path only; query strings and hashes are removed. - content-script runtime URLs keep only the visited page origin. - response_body_preview is not read and not sent. - repeated identical error fingerprints are suppressed for 24 h. Options: - instrumentation.errorDiagnostics.includeQueryStrings: include query strings, but sensitive keys token, key, secret, authorization, auth, password, and session stay redacted. - instrumentation.errorDiagnostics.includeResponseBodies: read and send a bounded response_body_preview. - instrumentation.errorDiagnostics.maxResponseBodyBytes: preview byte cap, default 1000, maximum 4096. - instrumentation.errorDiagnostics.includeContentScriptUrls: "origin" by default, or "path" / "full" when the extension disclosure allows it. ## Privacy and data hygiene - Do not put secrets, access tokens, email addresses, full URLs with private query strings, or personal content in attributes. - userId is optional. Use a stable internal ID only when the product owner needs signed-in user correlation. - Public key belongs in the extension. Secret key is server-side only. - Custom event names should describe product behavior, not personal data. - User telemetry opt-out must be honored by calling sdk.setTrackingEnabled(false). The SDK persists disabled state, purges queued telemetry and local counters, emits one minimal telemetry_opted_out marker, then blocks future product telemetry until opt-in. - For opt-in markets, initialize with initialConsent: "denied". The SDK persists that initial state, blocks telemetry until sdk.setTrackingEnabled(true), and replays the deferred first extension_installed event when consent is granted within the attribution window. - Chrome Web Store Data Usage should disclose extension usage analytics and diagnostics. Disclose identifiers if identify(...) is used. Default SDK settings do not collect browsing history; opting in to full content-script URLs or response bodies requires matching store and privacy-policy disclosure. - SDK 0.4.0 removes deprecated trackRaw and app.type. track(...) is strict and accepts only standard event names; use trackCustomEvent(...) for product-specific behavior. - getQueueStatus() includes droppedEvents, staleEvents, and evictedEvents. droppedEvents are server-rejected non-retryable batches; staleEvents are local drops older than the server attribution window; evictedEvents are local queue evictions caused by queue count or byte limits. ## 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.