@customerio/cdp-analytics-browser

中文文档
0.8.2发布于 2026/11/03MITTypeScriptESM + CJS构建来源已验证近 30 天下载 143.9万

Customer.io Data Pipelines analytics client for browsers.

原始使用文档
文档目录9 节
  1. Installation
  2. Usage
  3. Web push (internal alpha)
  4. Device attributes
  5. Notification payload
  6. Custom data and page events
  7. DEVIATIONS
  8. Alpha script demo
  9. Other Regions

Customer.io Data Pipelines analytics client for browsers.

Installation

npm install @customerio/cdp-analytics-browser

Usage

import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser'

const cioanalytics = AnalyticsBrowser.load({ writeKey: '<YOUR_WRITE_KEY>' })

cioanalytics.identify('hello world')

document.body?.addEventListener('click', () => {
  cioanalytics.track('document body clicked!')
})

Web push (internal alpha)

Install this package as above. Enable web push for your Customer.io workspace and configure its Web Push credential first. Load the plugin explicitly; it is not loaded by default. Loading does not register a service worker or request permission. It only checks for an existing subscription to reconcile a changed endpoint.

import {
  AnalyticsBrowser,
  WebPushPlugin,
} from '@customerio/cdp-analytics-browser'

const [analytics] = await AnalyticsBrowser.load({
  writeKey: '<YOUR_WRITE_KEY>',
})
await analytics.register(
  WebPushPlugin({
    vapidPublicKey: '<YOUR_PUBLIC_VAPID_KEY>',
    serviceWorkerUrl: '/cio-webpush-sw.js',
    trackUrl: 'https://track.customer.io',
  })
)
await analytics.identify('your-user-id')

// Invoke directly from a user gesture, especially on Safari/iOS.
document.querySelector('#subscribe')?.addEventListener('click', () => {
  void analytics.webPush!.subscribe().catch(console.error)
})

Alternatively, pass these settings under integrations['Customer.io Web Push Plugin'] in the second argument to AnalyticsBrowser.load. Set enabled: false to disable it. The host must only enable the alpha for workspaces with the web-push flag; the SDK does not query FlightDeck.

Options:

  • vapidPublicKey: required public base64url VAPID key (not the private key).
  • serviceWorkerUrl: defaults to /cio-webpush-sw.js; must be same-origin. Register the worker file directly (the plugin does, appending ?track=). If you import it into your own service worker with importScripts instead, your worker owns its lifecycle: set self.cioWebPushLifecycle = false before the import so the file skips skipWaiting()/clients.claim(), and register your worker with the same ?track= query so delivered and opened events still reach Customer.io (and the same ?runtime= query if you set runtimeUrl).
  • trackUrl: defaults to https://track.customer.io, or https://track-eu.customer.io when using the EU CDN. Set it explicitly when proxying the SDK or using the standalone plugin with a custom CDN.
  • runtimeUrl: where the worker loads its self-updating runtime (see below). Defaults to https://cdp.customer.io/v1/analytics-js/cio-webpush-sw-runtime.v1.js, or cdp-eu.customer.io when trackUrl is the EU host. Pass an absolute URL or a root-relative path (for example /cio/cio-webpush-sw-runtime.v1.js) to serve it first-party through your proxy, or false to keep the worker's bundled copy and make no request (registered as ?runtime=off). EU sites that proxy trackUrl get the US CDN by default; set runtimeUrl to the cdp-eu.customer.io URL or your own proxy.

Public API on the initialized analytics instance:

analytics.webPush!.requestPermission() // Promise<NotificationPermission>
analytics.webPush!.subscribe({ vapidPublicKey, serviceWorkerUrl }) // optional overrides
analytics.webPush!.subscription() // Promise<WebPushSubscriptionJSON | null>
analytics.webPush!.unsubscribe() // Promise<void>
analytics.webPush!.onClick((event) => console.log(event.customData)) // returns a cleanup function

Identify first. An anonymous subscribe can obtain browser permission and create a browser subscription, but its device registration promise waits for identify; no anonymous device event is sent. If the page closes (pagehide) or the plugin is unloaded before identification, that promise rejects with an identity error. Catch the promise, and do not rely on unload-time asynchronous work being displayed. If the worker has not registered and activated within 30 seconds, subscribe() rejects with Web push service worker did not activate within 30 seconds.; the browser may still finish installing it, so calling subscribe() again is safe. Device ownership is persisted per worker path: reset deletes the device of the person stored for the current path, and identifying another person registers it for them. Unsubscribe turns off the browser subscription even when logged out; deletion uses the stored owner when known. Unacknowledged logout deletes persist with their original event time/order and retry with backoff (30 seconds to one hour), including after reload. Reloads within backoff do not enqueue duplicate deletes. Cleanup stops on final HTTP 4xx (except 408/429), after 20 failed attempts, or after seven days, so it cannot permanently block registration. A buffered delete's acknowledgement or final rejection automatically reconciles the current identified user without reload. Device sequences use max(previous + 1, Date.now()) (64-bit epoch-ms scale) and persist across logout. Web Locks serialize sequence allocation across tabs when available; without locks, allocation remains best effort. After storage loss, the wall-clock floor assumes the clock has advanced beyond the prior sequence. Per-call worker URLs are persisted so later loads can find the subscription, as long as the serviceWorkerUrl load option still names the original path. Where localStorage is unavailable (for example, some private browsing modes), owners and per-call URLs last only for the current page. To migrate to another worker path, call analytics.webPush.unsubscribe() while still using the old path, then subscribe at the new path. This is the recommended approach: it removes the old browser subscription and device record; the host still owns its worker. Passing a new path directly to subscribe({ serviceWorkerUrl }) is supported with limits. If nothing is stored yet for the new path, the old path's stored owner is retained there before the new subscription is created; a successful subscribe then deletes that device (when its owner is known) and registers the new one. If the browser has no subscription at the new path and fails to create one, the previous path stays selected and its stored owner is unchanged, so subscription(), unsubscribe, identify and reset continue to act on the previous path. If the failure happens after a new-path subscription was created or an existing one was rotated, the new path stays selected and reset or a later subscribe can delete its retained owner. If the new path already has a stored record, that record wins and the old path's device is neither transferred nor deleted, so direct migration does not clean up both paths. Subscribe deletes the destination record's device only when its endpoint or owner differs; an identical endpoint and owner is sent as an update without a delete. In every direct case the old browser registration and subscription stay in place. Changing the serviceWorkerUrl load option itself does not read or migrate the old path's stored record; the plugin uses only what is stored for the new path. For a new filename in the same scope, the host must also unregister the old worker after unsubscribing, before subscribing with the new URL. The guard rejects replacement at that scope even when the old worker previously served web push; unsubscribe() removes the subscription, not the host's worker.

Copy node_modules/@customerio/cdp-analytics-browser/dist/cio-webpush-sw.js to /cio-webpush-sw.js on your site. Serve it as JavaScript over HTTPS (localhost is allowed for development), without a redirect. A root worker has root scope. To use /notifications/cio-webpush-sw.js, pass that path as serviceWorkerUrl; its default scope is /notifications/. Do not overwrite another application's service worker: use a dedicated scope or integrate the handlers into your existing worker and pass that worker's URL. Subscribe rejects a different worker at the same default scope before registering or replacing it. The plugin registers its worker as a classic script, so if your existing worker is a module service worker (type: 'module'), leave it as it is and serve cio-webpush-sw.js in a dedicated scope instead. The ordinary notification permission request still happens from your user action.

The worker reads the metrics endpoint from its ?track= query, and the runtime location from ?runtime= when you set runtimeUrl. When integrating with an existing worker, preserve both in every host registration call; registering the bare URL later disables new delivered/opened metrics, but queued reports retain and retry their original endpoint. Rebuild/copy the push handlers when updating the SDK.

The hosted worker updates itself. It bundles the complete runtime, so it works with no network request, and at startup it also tries importScripts on the Customer.io CDN's cio-webpush-sw-runtime.v1.js (the same file, published by every SDK release). When that copy is newer than the bundled one it runs instead, so worker fixes reach your site without re-copying the file. A blocked, failed (any HTTP error), older, or broken import leaves the bundled copy in charge and the worker still installs. A runtime host that drops or stalls the request is different: Firefox fails the install, or never finishes it, once the import has waited more than about 40–60 seconds, and Chromium waits for the connection to time out (about 75 seconds). So before a browser first registers the worker, subscribe() checks that the runtime host answers (a fetch with a 3-second timeout; any HTTP status counts). If it does not, the worker is registered with ?runtime=off and runs its bundled copy. An installed worker keeps its ?runtime= query on later calls, including off, so the worker URL does not change from page to page; a changed runtimeUrl is checked the same way and applied only if its host answers. The check is made once per page load and never with runtimeUrl: false. To re-enable upgrades in browsers registered with ?runtime=off (for example after a network that blocked cdp.customer.io is fixed, or after you serve the runtime first-party), set runtimeUrl explicitly: the next subscribe() in those browsers checks it and re-registers the worker with it once it answers. The page's connect-src Content Security Policy must allow the runtime host for the check to pass. A worker you register yourself (own importScripts) gets no check, so on networks that drop packets to the CDN serve the runtime first-party with runtimeUrl. Browsers re-check a successfully imported runtime during their service worker update checks and install a new worker version when it changes; a worker installed while the import failed keeps its bundled copy until your hosted file changes. Ad blockers' default lists (for example EasyPrivacy in uBlock Origin) block cdp.customer.io on other sites, so to upgrade those browsers too, serve the runtime first-party with runtimeUrl. Breaking changes ship as a new .v2.js file that existing hosted copies never load. If your site sets a Content Security Policy for the worker, allow https://cdp.customer.io and https://cdp-eu.customer.io in its script-src, or use runtimeUrl. With runtimeUrl: false, update your hosted copy yourself when you update the SDK.

Delivered/opened reports are saved in an IndexedDB outbox before sending. Offline requests and retryable HTTP failures retry on worker startup, push, notification click, and Background Sync (where supported), with 30-second exponential backoff capped at one hour. HTTP 4xx other than 408/429 are final and suppress replay while the receipt is retained. Background Sync is registered only while reports remain pending. Activation, notification display and navigation do not wait for reporting. An opened report waits for its queued delivered report to finish (including final rejection); unrelated deliveries can still proceed. The outbox retains at most 100 pending reports and 1,000 total records for seven days; overflow evicts the oldest pending report or acknowledged receipt. Repeated reports for the same delivery, event, and device (regardless of button) are suppressed while their record is retained. Retries preserve the first timestamp and action. If storage is unavailable or saving fails, the worker instead makes one direct best-effort send per event, without durable retries or deduplication. Older browsers without AbortSignal.timeout send without an SDK request timeout. The worker's implementation is scoped privately for importScripts hosts. Browser termination or a lost HTTP acknowledgement can still replay an accepted request; end-to-end exactly-once reporting requires server-side idempotency too.

A failed optional Web Push chunk import or plugin construction is warned once per analytics load and skipped; ordinary analytics continues to initialize.

Safari supports web push on supported macOS versions; iOS/iPadOS 16.4+ requires an installed Home Screen web app (with a web app manifest). Request permission from a user action. Unsupported browsers or denied permission reject subscribe. Changing VAPID keys invalidates existing subscriptions. Calling subscribe with the new key unsubscribes and deletes the old device before registering the new one.

Device attributes

Every Device Created or Updated registration (subscribe, updates, and endpoint reconciliation) sends these device attributes alongside webpush_p256dh and webpush_auth:

AttributeValue
user_agentOriginal navigator.userAgent (unchanged)
webpush_expiration_timePushSubscription.expirationTime as an integer epoch millisecond timestamp; sent as null to clear stale expiry when unknown (common on Chrome, Firefox, and Safari)
browserChrome, Edge, Firefox, Safari, Opera, Samsung Internet, or Other
browser_versionMajor version only, as a string (for example, '154'); omitted when unknown
osmacOS, Windows, iOS, iPadOS, Android, Linux, ChromeOS, or Other
originThe site's location.origin (for example, 'https://shop.example.com'), so each browser records which site it subscribed on

These finite browser/OS values support segments without parsing UA strings. The tiny built-in parser prefers navigator.userAgentData brands and platform when available, then falls back to UA tokens. It uses no high-entropy hints or third-party parser. Chromium is grouped under Chrome; specific Edge, Opera, and Samsung tokens take precedence over generic Chrome tokens. Desktop-mode iPadOS is detected using a Macintosh UA plus touch points. iOS Home Screen apps may omit a browser version: the worker's WebKit engine version is not a Safari major. Unknown expiry means unknown, not an expired subscription. The plugin sends null to clear a previously stored expiry. A persisted fingerprint of browser, major version, OS, origin, and expiry triggers re-registration on changes (including backfilling devices registered by older SDKs on their next page load). Unchanged attributes do not trigger another registration.

Notification payload

The worker accepts title, body, optional image (large picture), icon, badge, link (default click URL), and actions:

{
  "title": "Your order is ready",
  "body": "View your order for details.",
  "image": "https://example.com/order.png",
  "icon": "https://example.com/icon.png",
  "badge": "https://example.com/badge.png",
  "link": "https://example.com/orders",
  "actions": [
    {
      "action": "view",
      "title": "View order",
      "url": "https://example.com/orders/123"
    },
    { "action": "later", "title": "Later" }
  ],
  "CIO-Delivery-ID": "<delivery-id>",
  "CIO-Delivery-Token": "<subscription-endpoint>"
}

Use HTTPS URLs for icon, badge, and action destinations. Action IDs must be unique and match [a-z0-9_-]{1,32}; titles are trimmed of Unicode whitespace (including U+0085 and U+FEFF) and must be nonempty and at most 32 Unicode code points (an emoji counts as one), the rule Customer.io applies when it saves and sends a message. The worker drops malformed actions and keeps at most two valid entries. The browser decides how many buttons it shows: Chrome supports two; Firefox may show none. Image, icon, and badge rendering also depends on the browser and OS. Unknown payload fields are not notification options. Optional Advanced options are passed to showNotification only when present:

Relay payload fieldValue
requireInteractionBoolean; keep on screen until dismissed
silentBoolean; suppress sound/vibration
tagReplacement tag (1–64 characters: letters, digits, _ . : -)
renotifyBoolean; requires tag
vibrate1–10 integer durations, each 0–10000 ms
langBCP 47 language tag (up to 35 characters)
dirauto, ltr, or rtl
timestampEvent time as epoch milliseconds
app_badgeInteger 0–99999; 0 clears the app badge
custom_dataJSON object retained as notification.data.custom_data

The relay validates these fields (and converts a rendered ISO timestamp to epoch milliseconds). Composer require_interaction becomes relay requireInteraction. The worker does not add defaults when options are absent. It coerces option types, drops invalid dir/timestamps, drops vibrate when silent, and drops renotify without a nonempty tag. Malformed coercions fall back to base options. Browser/OS support varies; unsupported notification options may be ignored. app_badge is distinct from the notification badge image: it calls worker navigator.setAppBadge(count) or clearAppBadge() when available. Unsupported or rejected badging does not prevent notification display or delivery metrics.

An action click opens its url, falling back to link when omitted; a body click opens link. Click destinations must resolve to HTTPS. Relative links resolve against the site origin, wherever the worker is installed. An unsafe button URL falls back to the validated body link. Missing, malformed, or unsafe body links, including any HTTP link, fall back to /. On an HTTP development origin such as localhost, relative links therefore also open /. Delivered/opened metrics use the delivery ID and subscription endpoint. Action clicks add the action ID to the opened metric. Navigation never waits for the metrics POST; closing a notification sends no metric.

Custom data and page events

After starting the opened metric, the worker calls openWindow to open the click destination (the browser may reuse/focus an existing window). It hands the click only to that returned same-origin client, once, matching readiness by client ID rather than URL so redirects and router URL changes work. Other matching tabs receive no click. The plugin dispatches { deliveryId, action, customData } in a cio:webpush:click window CustomEvent and invokes click callbacks. action is the button ID, or '' for a body click; customData is undefined when absent or malformed (malformed data is logged once per plugin instance). Worker postMessage type names are private implementation details:

const stop = analytics.webPush!.onClick(
  ({ deliveryId, action, customData }) => {
    console.log(deliveryId, action, customData?.orderId)
  }
)
// Alternatively (do not use both unless you want both handlers):
window.addEventListener('cio:webpush:click', (event) => {
  console.log(event.detail.customData)
})
// Remove a callback when its component unmounts:
stop()

The published WebPushClickEvent, WebPushReceivedEvent, and WebPushAPI types include these APIs; window event names are typed through WindowEventMap. Register handlers during page initialization, including on destinations opened by a click. The worker waits up to 10 seconds for the plugin's readiness handshake so a newly opened page can install its message listener. No click data is handed to any tab except the client returned by openWindow. If no accessible client exists (for example, a cross-origin destination), or the plugin does not load in time, navigation and metrics still work but there is no page callback. The handoff is best-effort, not durable storage. Unloading the plugin removes its message listeners and click callbacks.

Delivery payloads do not identify the original user. To avoid handing an old user's in-flight data to a new identity, reset or an identity change since registration suppresses click and received handoffs for the rest of that plugin instance, even after re-registration. A fresh instance can resume under its current registered identity; this guard cannot identify old deliveries after a reload. Notifications and metrics still work. Do not put secrets in custom data.

On push, the worker also notifies controlled open window clients. The plugin dispatches cio:webpush:received with a cleaned { deliveryId, title, customData } detail, not the raw worker payload; it does not change page UI:

window.addEventListener('cio:webpush:received', (event) => {
  console.log(event.detail.deliveryId, event.detail.customData)
})

DEVIATIONS

The worker has no analytics instance: subscription changes are re-sent only via an open page or the next load. Reconciliation deletes the stored endpoint before registering a replacement. Legacy endpoint-only records have no known owner, so their previous person's device cannot be deleted safely.

Alpha script demo

With the existing Pipelines analytics snippet already loaded, serve the built packages/browser/dist/umd/webPushPlugin.min.js at the URL below, and copy packages/browser/dist/cio-webpush-sw.js to the site root. Replace the placeholders. The public write key belongs in the analytics snippet; never include a Track API key or a private VAPID key. The public snippet uses cioanalytics; if you set data-global-customerio-analytics-key, use that name instead.

<button id="subscribe" disabled>Enable notifications</button>
<script src="/dist/umd/webPushPlugin.min.js"></script>
<script>
  const analytics = window.cioanalytics // match your snippet's global name
  analytics.ready(async function () {
    try {
      await analytics.register(
        CustomerIOWebPush.WebPushPlugin({
          vapidPublicKey: '<YOUR_PUBLIC_VAPID_KEY>',
          serviceWorkerUrl: '/cio-webpush-sw.js',
          trackUrl: 'https://track.customer.io', // EU: https://track-eu.customer.io
        })
      )
      await analytics.identify('<YOUR_USER_ID>')
      const button = document.getElementById('subscribe')
      button.disabled = false
      button.onclick = function () {
        analytics.webPush.subscribe().then(console.log).catch(console.error)
      }
    } catch (error) {
      console.error(error)
    }
  })
</script>

yarn browser+deps build emits the UMD bundle in packages/browser/dist/umd/, the static worker and its CDN runtime in packages/browser/dist/, and npm JS/types in packages/browser/dist/{pkg,cjs,types}/. The worker ships unbuilt: yarn browser test runs its tests on src/plugins/web-push-plugin/cio-webpush-sw.js, so it needs no build, and the webpack build fails if the dist/cio-webpush-sw.js or dist/cio-webpush-sw-runtime.v1.js it writes differs from that source. Publish the runtime on the CDN at /v1/analytics-js/cio-webpush-sw-runtime.v1.js with every release, before sites deploy a hosted copy that imports it, and serve it with Cache-Control: no-cache (or max-age=0): Safari otherwise installs update after update that still run the HTTP-cached runtime. scripts/build-prep.sh sets the worker's version, which hosted copies compare with the CDN's. A hosted copy accepts a runtime only if it reports api: 1 and a strictly newer release version (a valid semver string; prereleases are never installed and build metadata is ignored), adds no listeners when evaluated, and registers exactly one complete handler set through install(env)'s addEventListener before install returns; otherwise none of its listeners are registered and the bundled copy installs. Listener options are ignored.

Other Regions

If you're in our EU data center you will need to specify an alternate endpoint:

import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser'

const cioanalytics = AnalyticsBrowser.load({
  cdnURL: 'https://cdp-eu.customer.io',
  writeKey: '<YOUR_WRITE_KEY>',
})

cioanalytics.identify('hello world')

版本与发布记录

@customerio/cdp-analytics-browser 共发布 70 个版本,首次发布于 2023/05/10;过去 12 个月发布了 49 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.2latest2026/11/037 天前
0.8.12026/10/2913 天前
0.8.02026/10/2418 天前
0.7.12026/10/2318 天前
0.7.02026/10/2319 天前
0.6.22026/10/2120 天前

其他发布通道:test-oidc → 0.0.0-test-oidc。

关于 @customerio/cdp-analytics-browser 的常见问题

@customerio/cdp-analytics-browser 是什么?

Customer.io Data Pipelines analytics client for browsers.

如何安装 @customerio/cdp-analytics-browser?

在项目目录运行 npm install @customerio/cdp-analytics-browser,或使用 pnpm add / yarn add / bun add。

@customerio/cdp-analytics-browser 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@customerio/cdp-analytics-browser 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@customerio/cdp-analytics-browser 可以商用吗?

@customerio/cdp-analytics-browser 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@customerio/cdp-analytics-browser 的下载量有多少?

根据 npm 官方下载统计,@customerio/cdp-analytics-browser 在 2026/10/11 – 2026/11/09 这 30 天内被下载 1,439,021 次,日均约 47,967 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@customerio/cdp-analytics-browser 还在维护吗?

最新版本 0.8.2 发布于 2026/11/03(7 天前);至今共发布 70 个版本,其中过去 12 个月发布了 49 个;首次发布于 2023/05/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。