npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@extension-report/js

v0.6.2

Published

Modular owner metrics SDK for browser extensions with remotely controlled adoption, engagement and reliability instrumentation.

Readme

@extension-report/js

Owner metrics SDK for https://extension.report.

MV3-native and zero dependencies. The 0.6 architecture separates core, configuration, transport and instrumentation modules while keeping every remotely reactivatable capability in the store artifact. A 20 KB Brotli size-limit budget prevents silent growth.

The normative browser/framework support matrix and the full-SDK decision are in https://github.com/Amorem/extension.report/blob/main/docs/sdk-compatibility.md.

Install

pnpm add @extension-report/[email protected]

For store builds, pin the exact version in package.json ("@extension-report/js": "0.6.2", not ^0.6.2) and run npx @extension-report/[email protected] against the built manifest before every submission. If you use a companion package, keep every @extension-report/* SDK package on the same version. The store-validation runbook is in the repository docs: https://github.com/Amorem/extension.report/blob/main/docs/sdk-store-validation.md.

Quick start

import {
  connectExtensionReportUiSession,
  initExtensionReport,
} from "@extension-report/js";

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  debug: { log: false, dryRun: false },
});

That single background/service-worker call auto-registers every standard browser signal the SDK can observe safely. Missing browser APIs or permissions are ignored.

Initialize at top level in the background service worker. MV3 can miss events when listeners are registered after the first service-worker turn, and the doctor cannot detect that timing issue. If a browser runtime does not expose navigator.locks, initialize the SDK in that background context only; multi-context queue writes are not supported without a cross-context lock.

Test-only helpers live under @extension-report/js/testing; they are not part of the integration API and may change outside the public SDK contract.

Standard signals

  • Lifecycle: extension_installed, extension_updated, extension_update_available, extension_started, extension_heartbeat
  • Extension opens: extension_ui_opened, extension_ui_closed, extension_options_opened, extension_side_panel_opened
  • Toolbar adoption: toolbar_pin_state_seen, toolbar_pin_state_changed
  • Environment diagnostics: environment_state_seen (SDK, extension, install type, manifest, browser, OS, locale and timezone snapshot)
  • Identity: user_identify
  • Context menus: context_menu_clicked
  • Keyboard shortcuts: keyboard_shortcut_configured, keyboard_shortcut_missing, keyboard_shortcut_used
  • Omnibox: omnibox_session_started, omnibox_input_entered (per-keystroke input is aggregated into the omnibox_input_changed daily counter, not sent as individual events)
  • Permissions: permission_requested, permission_granted, permission_declined, permission_removed, host_permission_granted, host_permission_removed
  • Notifications: notification_shown, notification_clicked, notification_button_clicked, notification_closed, notification_closed_by_user
  • Review funnel: review_prompt_shown, review_prompt_clicked
  • Errors and custom usage: sdk_error, custom_event

Helpful calls

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
});

await sdk.popup.opened();
await sdk.review.promptShown({ placement: "settings" });
await sdk.review.promptClicked({ placement: "settings" });

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

await browser.notifications.create("welcome", {
  type: "basic",
  title: "Ready",
  message: "The extension is configured.",
  iconUrl: "/icon.png",
});
void sdk.notifications.shown("welcome", { kind: "setup" }).catch(() => null);

const queue = await sdk.getQueueStatus();
await sdk.identify("user_123", {
  entitlement_type: "paid",
  entitlement_status: "active",
  plan_code: "subscription",
});
await sdk.identify(null, {
  entitlement_type: "trial",
  entitlement_status: "active",
  plan_code: "anonymous_trial",
  trial_expires_at: "2026-07-31T23:59:59.000Z",
});
await sdk.trackCustomEvent("rule_created", { source: "popup" });

Local daily counters

For high-frequency signals (keystrokes, scroll, polling loops), sending one event per occurrence is waste. sdk.count(name) increments a local counter instead; counters are delivered once per UTC day as a single usage_agg_daily event and feed the "Top actions" usage breakdown in the dashboard:

await sdk.count("pages_scanned");
await sdk.count("rows_processed", 25);

Use identify(...) for user/account traits, trackCustomEvent(...) when each occurrence matters (funnels, feature adoption), and count(...) when only the daily volume does. identify(...) queues immediately when the user id or traits change, then at most once per 24 h for the same state. The queued event follows the SDK's normal durable flush path. The user id can be null for anonymous installations; traits such as plan, license, trial state, workspace, or rollout cohort still describe the current installation state. Use custom events for actions such as upgrade clicks, not for the state itself.

Most signals are automatic. Keep these explicit helpers for two concrete browser gaps:

  • If your manifest has "action": { "default_popup": "popup.html" }, Chrome opens the popup directly and does not fire chrome.action.onClicked in the background. Prefer connectExtensionReportUiSession(...) from the popup page; the background SDK emits extension_ui_opened, then extension_ui_closed with duration_ms when the popup port disconnects. Use sdk.popup.opened() only for open-only tracking.
  • If you need notification_shown, keep product-critical notifications on the native browser API, then call sdk.notifications.shown(...) after the native call succeeds. Use sdk.notifications.show(...) only for simple cases where a wrapper is acceptable. Clicked, button-clicked, closed, and closed-by-user are listened to automatically.

instrumentation.automatic: false is for non-background contexts. Use it when importing the SDK in a popup, options page, side panel, or content script so that only the focused helper runs there. The full lifecycle and browser listeners should stay registered once in the background service worker.

Permissions

Manifest permissions are automatic. On startup the SDK calls chrome.permissions.getAll(), compares the result with the last local snapshot, and emits only changes:

  • first observed permissions become one permission_state_seen baseline, not grant events,
  • newly added permissions become permission_granted / host_permission_granted,
  • removed permissions become permission_removed / host_permission_removed,
  • unchanged snapshots are ignored, so service-worker restarts do not spam events.

Use sdk.permissions.request(...) only for optional permissions requested during product usage:

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

That pattern is useful when an extension wants fewer install-time warnings and asks for access only when the user enables a feature: tab capture, screenshots, Gmail/Notion/Jira integrations, a specific customer domain, or site-specific automation. The wrapper is needed only to measure permission_requested and permission_declined; Chrome broadcasts grants/removals automatically.

Advanced contexts

  • Notification clicks, button clicks, and closes are automatic. For notification_shown, prefer browser.notifications.create(...) followed by best-effort sdk.notifications.shown(...) so telemetry cannot block a product-critical notification.
  • Content scripts and injected UIs are not visible to background listeners. Send a message to the background or use trackCustomEvent(...) for product actions such as settings_saved, rule_created, or export_completed.

For unusual integrations you can opt out of the auto setup:

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

Remotely controlled modules

SDK 0.6 groups automatic observation by product outcome rather than exposing one remote switch per browser API:

  • Core — identity, lifecycle, queue, delivery and remote config. The dashboard exposes one emergency stop for the complete SDK.
  • Adoption — pin state, permissions and shortcut readiness.
  • Engagement — UI sessions, menus, shortcut usage, omnibox, notifications, review prompts, custom events and daily counters.
  • Reliabilityoff, errors only, or errors plus network diagnostics.

The dashboard can publish modes, deterministic rollout percentages and context audiences without a store release. Local options remain the ceiling: remote config cannot enable a group explicitly disabled by the extension or add a missing manifest capability.

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

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

Active MV3 clients apply a new release on the next event/flush. A dormant service worker applies it on a later wake or cache refresh; the dashboard reports convergence instead of claiming impossible always-on real-time delivery.

User opt-out (consent)

Tracking is enabled by default. Give your users a way to opt out — the Chrome Web Store user-data policies and GDPR expect one for EU audiences:

// settings page (via a message to the background, where the SDK lives)
await sdk.setTrackingEnabled(false); // persists, purges the local queue, blocks all events
await sdk.isTrackingEnabled(); // read the current state for the toggle

await sdk.setTrackingEnabled(true); // opt back in

Disabling is local to the installation: it survives service worker restarts and extension updates. The SDK sends one minimal telemetry_opted_out consent marker, then blocks product events until the user opts back in; opting back in sends telemetry_opted_in.

For opt-in markets, initialize with initialConsent: "denied" before registering automatic instrumentation. The SDK persists that default, blocks telemetry until setTrackingEnabled(true), and replays the first extension_installed event with its original timestamp when consent is granted within the server attribution window.

Debug and dry run

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  debug: {
    log: true,
    dryRun: true,
  },
});

await sdk.trackCustomEvent("debug_probe");
console.log(await sdk.getQueueStatus());
  • debug.log prints local SDK diagnostics in addition to any remote debug flag.
  • debug.dryRun builds and drains batches locally without sending network requests.
  • getQueueStatus() reports queued events, queue date bounds, backoff, server-dropped, stale, and locally evicted events, pending daily counters, and dry-run state.

Network diagnostics

sdk.trackError(error) reports handled errors. With automatic instrumentation enabled, the SDK also captures uncaught errors and wraps fetch so a later error report can include request method, status, duration, and whether the failure was an HTTP response or a network error.

The defaults are privacy-safe:

  • 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 previews are not read or sent,
  • repeated identical error fingerprints are suppressed for 24 h.

If your extension needs richer diagnostics, opt in explicitly:

const sdk = initExtensionReport({
  projectPublicKey: "pk_er_...",
  instrumentation: {
    errorDiagnostics: {
      includeQueryStrings: true,
      includeResponseBodies: true,
      maxResponseBodyBytes: 1000,
      includeContentScriptUrls: "origin",
    },
  },
});

Even with includeQueryStrings: true, sensitive query keys such as token, key, secret, authorization, auth, password, and session are redacted. Disable the fetch wrapper entirely with instrumentation: { networkDiagnostics: false }.

Chrome Web Store data disclosure

At minimum, disclose extension usage analytics and diagnostics in your Chrome Web Store Data Usage form, and disclose any user identifier you pass through identify(...). You should not need to declare browsing history when using the default SDK settings: content-script errors keep only the page origin, not the full visited URL. If you opt in to full content-script URLs or response bodies, update your disclosure and privacy policy accordingly.

Manifest requirements

{
  "permissions": ["storage", "alarms"],
  "host_permissions": ["https://extension.report/*"],
  "background": { "service_worker": "background.js", "type": "module" }
}

Add "contextMenus" only when you instrument context menu clicks. Add "notifications" only when your extension already shows notifications. Add "commands" or "omnibox" manifest keys only when your extension exposes those workflows.

Notes

  • The SDK sends batches to /api/v2/events with retry/backoff.
  • SDK 0.3.8+ emits one compact environment_state_seen state event instead of the deprecated extension_version_seen, browser_version_seen, sdk_version_seen, and manifest_version_seen events. It is sent only when the environment snapshot changes or after the state-signal resend window, so service-worker wakes do not multiply version rows. Screen and device fields are not part of this event; they stay in the batch context when the runtime can observe them, including UI sessions opened through connectExtensionReportUiSession(...).
  • SDK 0.3.11+ exposes sdk.trackError(error). SDK 0.3.13+ keeps error diagnostics privacy-safe by default: fetch query strings are stripped, response bodies are off unless explicitly opted in, and content-script page URLs are reduced to origin.
  • SDK 0.3.9+ records the first permission snapshot as state only. It does not count already installed manifest permissions as freshly granted optional permissions. Non-empty permission snapshots still feed configuration-state metrics and the latest active permission list shown in installation details.
  • SDK 0.3.10+ snapshots the lightweight event context at enqueue time: extension version, SDK version, manifest version, browser, OS, locale, timezone and languages. Queued events therefore keep the version/environment they actually occurred under even when they flush after an extension update. Screen and device stay in the current batch context.
  • Automatic signals and custom events debounce their network flush (batching.flushDebounceMs, default 2000 ms, remote-config key flush_debounce_ms, 0 = immediate): a burst becomes one batched request. Events are persisted locally before the window, so a dying service worker loses nothing — the flush alarm delivers on the next wake. Explicit track() and flush() remain immediate and strict.
  • The SDK reads remote config from /api/v2/config. SDK 0.5.0+ exposes feature flags with await sdk.getFlag("review_prompt", false) and advanced config values with await sdk.getConfigValue("rollout.cohort", "default"). SDK 0.5.1+ flags can be booleans or rollouts such as { "beta_panel": { "rollout": 0.25 } }. 0.5.2+ rollout objects can also include small context audiences, for example { "firefox_beta": { "rollout": 0.5, "match": { "browser_name": "firefox", "min_extension_version": "2.1.0" } } }. An installation with an unknown context value does not match an audience that constrains that field. Event responses include config_version; when it changes, active clients refresh config on the next successful flush instead of waiting for the normal TTL. Project plan and limits are resolved server-side into the public config; SDK-facing limits remain available as top-level keys such as max_events_per_minute, while server-only limits such as project_requests_per_minute are not returned to the browser.
  • SDK 0.6 adds grouped modules policies. Every batch carries the effective config_version and module state so the dashboard can measure active-installation convergence and safe rollback.
  • After the first successful event batch, the API returns an uninstall token. SDK 0.3.13+ stores it, calls chrome.runtime.setUninstallURL, and refreshes the URL on service-worker wakes so the ls last-seen parameter stays current.
  • Events are written to local extension storage before network delivery. Helper methods are best-effort, so a temporary extension.report outage does not break extension behavior.
  • flush() is intentionally strict for diagnostics; explicit calls reject on ingestion failure while queued events remain stored locally for retry.
  • If your UI or content scripts run in another context, send a message to the background to call the SDK there.
  • Delivery is fail-safe: batches rejected as invalid or oversized (4xx) are dropped and counted in getQueueStatus().droppedEvents instead of blocking the queue; an unknown project key (rotated without shipping a new build) drops and rechecks once a day. Locally stale events older than the server attribution window are counted separately in getQueueStatus().staleEvents.
  • SDK 0.4.0+ uses deterministic remote-config sampling per installation and event name, exposes local queue evictions through getQueueStatus().evictedEvents, bounds daily counter cardinality with an other bucket, micro-batches debounced queue writes, and uses Web Locks when available to coordinate queue writes across extension contexts.
  • max_events_per_minute is enforced in memory for the current service-worker lifetime. MV3 can reset that window on wake; the server still enforces ingestion rate limits.
  • Reliability mode errors disables fetch diagnostics collection without an extension update. The wrapper remains installed but becomes pass-through, so full can reactivate it later.
  • SDK 0.4.0 removes the deprecated trackRaw alias and app.type option. track(...) is strict and accepts only standard event names; use trackCustomEvent(...) for product-specific events. The backend treats all SDK events as extension events.
  • SDK 0.5.0 adds remote-config flags for product toggles without a store release; SDK 0.5.1 adds percentage rollouts and faster active-client convergence through config_version.
  • SDK 0.5.0 captures chrome.management.getSelf().installType when available. Dashboard product metrics exclude development installs by default; the Report has an include-dev toggle for local QA.
  • WXT users can install @extension-report/wxt for the WXT module (modules: ["@extension-report/wxt/module"]) and WXT-named background/UI session helpers.
  • Plasmo users can install @extension-report/plasmo for the same thin adapter pattern with Plasmo-named helpers.
  • Run npx @extension-report/[email protected] --public-key pk_er_... --manifest manifest.json to check manifest permissions, remote config reachability, and a non-ingesting event dry-run.
  • The package is ESM-only (type: "module"), which every extension bundler (Vite/WXT/Plasmo/webpack) consumes natively.
  • In Node/SSR/build contexts with no extension runtime and no page globals, the SDK returns an inert no-op instance by design. In that mode isTrackingEnabled() resolves to false.

License

MIT — see LICENSE.