tracklane 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,9 +28,18 @@ function ga4(measurementIdOrConfig = {}, config = {}) {
28
28
  send_to: measurementId
29
29
  });
30
30
  },
31
- identify(user, traits) {
31
+ identify(user, traits, report) {
32
32
  if (user.userId !== void 0) push$1("set", { user_id: user.userId });
33
- if (traits) push$1("set", "user_properties", traits);
33
+ if (!traits) return;
34
+ const properties = {};
35
+ const unclearable = [];
36
+ for (const [name, value] of Object.entries(traits)) if (value === null) unclearable.push(name);
37
+ else properties[name] = value;
38
+ if (Object.keys(properties).length > 0) push$1("set", "user_properties", properties);
39
+ if (unclearable.length > 0) report?.(`user properties not cleared, because clearing one through gtag is not verified: ${unclearable.join(", ")}`);
40
+ },
41
+ reset() {
42
+ push$1("set", { user_id: null });
34
43
  },
35
44
  consent(command, state) {
36
45
  push$1("consent", command, state);
@@ -286,6 +295,8 @@ function meta(config = {}) {
286
295
  }
287
296
  //#endregion
288
297
  //#region src/providers/posthog.browser.ts
298
+ /** PostHog documents that it shortens any `distinct_id` longer than this. */
299
+ const MAX_DISTINCT_ID = 200;
289
300
  function posthogWindow() {
290
301
  return typeof window === "undefined" ? void 0 : window;
291
302
  }
@@ -331,9 +342,25 @@ function posthog(config = {}) {
331
342
  track(name, data, _options) {
332
343
  sdk().capture(name, data);
333
344
  },
334
- identify(user, traits) {
335
- if (user.userId !== void 0) sdk().identify(user.userId);
336
- if (traits) sdk().setPersonProperties(traits);
345
+ identify(user, traits, report) {
346
+ if (user.userId !== void 0) {
347
+ if (user.userId.length > MAX_DISTINCT_ID) report?.(`distinct_id is longer than ${MAX_DISTINCT_ID} characters: PostHog truncates it, so two ids sharing that prefix become one person`);
348
+ sdk().identify(user.userId);
349
+ }
350
+ if (!traits) return;
351
+ const set = {};
352
+ const unset = [];
353
+ for (const [name, value] of Object.entries(traits)) if (value === null) unset.push(name);
354
+ else set[name] = value;
355
+ if (Object.keys(set).length > 0) sdk().setPersonProperties(set);
356
+ if (unset.length > 0) {
357
+ const instance = sdk();
358
+ if (instance.unsetPersonProperties) instance.unsetPersonProperties(unset);
359
+ else instance.capture("$set", { $unset: unset });
360
+ }
361
+ },
362
+ reset() {
363
+ sdk().reset();
337
364
  },
338
365
  consent(_command, state) {
339
366
  const { analytics_storage } = state;
@@ -353,7 +380,7 @@ function posthog(config = {}) {
353
380
  * lost to loading and this library needs no queue of its own.
354
381
  *
355
382
  * @param options - The providers to send to, and an optional error channel.
356
- * @returns The `track`, `identify` and `consent` calls.
383
+ * @returns The `track`, `identify`, `reset` and `consent` calls.
357
384
  *
358
385
  * @example
359
386
  * ```ts
@@ -412,11 +439,25 @@ function createTracking(options) {
412
439
  },
413
440
  identify(user, traits) {
414
441
  for (const provider of providers) try {
415
- provider.identify?.(user, traits);
442
+ provider.identify?.(user, traits, (message) => {
443
+ report({
444
+ provider: provider.name,
445
+ event: "identify",
446
+ severity: "warning",
447
+ message
448
+ });
449
+ });
416
450
  } catch (cause) {
417
451
  fail(provider.name, "identify", cause);
418
452
  }
419
453
  },
454
+ reset() {
455
+ for (const provider of providers) try {
456
+ provider.reset?.();
457
+ } catch (cause) {
458
+ fail(provider.name, "reset", cause);
459
+ }
460
+ },
420
461
  consent(command, state) {
421
462
  for (const provider of providers) try {
422
463
  provider.consent?.(command, state);
@@ -429,4 +470,4 @@ function createTracking(options) {
429
470
  //#endregion
430
471
  export { ga4 as a, linkedin as i, posthog as n, meta as r, createTracking as t };
431
472
 
432
- //# sourceMappingURL=browser-DhzO5qPS.js.map
473
+ //# sourceMappingURL=browser-ChcWOQ3N.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-ChcWOQ3N.js","names":["push"],"sources":["../src/providers/ga4.browser.ts","../src/providers/linkedin.browser.ts","../src/providers/meta.browser.ts","../src/providers/posthog.browser.ts","../src/browser.ts"],"sourcesContent":["// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n TrackOptions,\n UserData,\n} from '../index.js';\n\ninterface GtagWindow {\n gtag?: (...args: unknown[]) => void;\n}\n\n/** Optional configuration for the GA4 browser provider. */\nexport interface Ga4BrowserConfig {\n /**\n * How this property spells each canonical event. Rarely needed: the\n * canonical vocabulary already is GA4's.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction gtagWindow(): GtagWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as GtagWindow);\n}\n\n/**\n * Sends a command, or fails the way a hand-written call would.\n *\n * Writing `gtag(…)` on a page without Google's snippet throws, and this\n * library replicates that rather than swallowing it: a silent no-op is how a\n * host ends up believing it is measuring something.\n */\nfunction push(...args: unknown[]): void {\n const gtag = gtagWindow()?.gtag;\n\n if (!gtag) {\n throw new Error(\n \"ga4: window.gtag is not defined. Add Google's tag to the page before creating tracking.\",\n );\n }\n\n gtag(...args);\n}\n\n/**\n * Google Analytics 4 in the browser, through `gtag.js`.\n *\n * **This provider does not load Google's tag.** The tag belongs on the page,\n * installed the way Google documents it: a snippet in the HTML, or a tag\n * manager. This library talks to the tag that is already there; it never\n * injects a third-party script into your page, and it never issues the\n * property configuration that would emit a second page view.\n *\n * Which also means the initial consent declaration is not ours to make:\n * Google requires it before the property is configured, and the snippet that\n * configures the property is yours. Use {@link ConsentState} through\n * `consent()` for changes after that.\n *\n * With no measurement ID, events keep the tag's configured routing. Pass a\n * measurement ID to address that destination through `send_to` instead.\n * Neither form installs or configures the tag.\n *\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * // Google's own snippet is already in the page.\n * const { track } = createTracking({ providers: [ga4()] });\n *\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function ga4(config?: Ga4BrowserConfig): BrowserProvider;\n/**\n * Addresses one configured Google destination instead of using the tag's\n * default routing. Existing calls keep their `send_to` behavior.\n *\n * @param measurementId - The `G-` ID of the configured data stream.\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n * @example\n * ```ts\n * const provider = ga4('G-XXXXXXX', { events: { my_event: 'my_ga4_name' } });\n * ```\n */\nexport function ga4(measurementId: string, config?: Ga4BrowserConfig): BrowserProvider;\nexport function ga4(\n measurementIdOrConfig: string | Ga4BrowserConfig = {},\n config: Ga4BrowserConfig = {},\n): BrowserProvider {\n const measurementId =\n typeof measurementIdOrConfig === 'string' ? measurementIdOrConfig : undefined;\n const { events } = typeof measurementIdOrConfig === 'string' ? config : measurementIdOrConfig;\n\n return {\n name: 'ga4',\n // The canonical names already are GA4's, so every event passes through\n // under its own name unless the host says otherwise.\n default: 'passthrough',\n ...(events ? { events } : {}),\n\n track(name: string, data: EventData, _options: TrackOptions): void {\n // GA4 documents no browser-to-server deduplication, so `dedupId` maps\n // to nothing here. That is the exact translation of \"this concept does\n // not exist in this vendor\". Inventing a param would imply a merge\n // the platform never performs.\n //\n // With an explicit destination, it wins over a payload's `send_to`.\n // Without one, forward the manual call unchanged and let gtag route it.\n push('event', name, measurementId === undefined ? data : { ...data, send_to: measurementId });\n },\n\n identify(\n user: UserData,\n traits?: Record<string, unknown>,\n report?: (message: string) => void,\n ): void {\n // GA4 documents no browser slot for email, phone or names. They map to\n // nothing, which is what \"this concept does not exist here\" means.\n if (user.userId !== undefined) push('set', { user_id: user.userId });\n if (!traits) return;\n\n // `null` is \"no longer known\". Whether gtag clears a user property\n // given `null` is not verified here, so it is left out and reported\n // rather than sent on a guess (ADR-0014).\n const properties: Record<string, unknown> = {};\n const unclearable: string[] = [];\n for (const [name, value] of Object.entries(traits)) {\n if (value === null) unclearable.push(name);\n else properties[name] = value;\n }\n\n if (Object.keys(properties).length > 0) push('set', 'user_properties', properties);\n if (unclearable.length > 0) {\n report?.(\n `user properties not cleared, because clearing one through gtag is not verified: ${unclearable.join(', ')}`,\n );\n }\n },\n\n reset(): void {\n // Google's documented way to stop attributing hits to a user. User\n // properties already set stay with the tag until the page reloads.\n push('set', { user_id: null });\n },\n\n consent(command: 'default' | 'update', state: ConsentState): void {\n // Verbatim: the library's consent vocabulary is Google's own spelling,\n // so there is nothing to translate and no chance to expand a signal\n // the host did not declare.\n push('consent', command, state);\n },\n };\n}\n","import type { BrowserProvider, EventBinding, EventData, TrackOptions } from '../index.js';\n\n/** Configuration for the LinkedIn Insight Tag provider. */\nexport interface LinkedinBrowserConfig {\n /** Conversion IDs minted in Campaign Manager. `null` explicitly suppresses an event. */\n events: Record<string, EventBinding<string>>;\n}\n\ninterface LinkedinWindow {\n lintrk?: (command: 'track', data: Record<string, unknown>) => void;\n}\n\n/**\n * Calls the host-installed LinkedIn Insight Tag for event-specific conversions.\n * LinkedIn has no event names, user-identification command or consent command.\n * Items and custom parameters have no documented slot and are not forwarded.\n *\n * @param config - The canonical event to conversion ID map.\n * @returns A browser provider; it does not install the Insight Tag.\n * @example\n * ```ts\n * import { createTracking, linkedin } from 'tracklane/browser';\n * const { track } = createTracking({\n * providers: [linkedin({ events: { purchase: '12345' } })],\n * });\n * track('purchase', { value: 15, currency: 'BRL' }, { dedupId: 'order-1' });\n * ```\n */\nexport function linkedin(config: LinkedinBrowserConfig): BrowserProvider {\n return {\n name: 'linkedin',\n default: 'ignore',\n events: config.events,\n track(name: string, data: EventData, options: TrackOptions): void {\n const conversionId = Number(name);\n if (!/^\\d+$/.test(name) || !Number.isSafeInteger(conversionId) || conversionId <= 0) {\n throw new Error(\n 'linkedin: conversion_id must be a positive safe integer from Campaign Manager',\n );\n }\n const sdk =\n typeof window === 'undefined' ? undefined : (window as unknown as LinkedinWindow).lintrk;\n if (!sdk) {\n throw new Error(\n 'linkedin: window.lintrk is missing. Install the Insight Tag before tracking.',\n );\n }\n const payload: Record<string, unknown> = { conversion_id: conversionId };\n if (data.currency !== undefined) payload.conversion_currency = data.currency;\n if (data.value !== undefined) payload.conversion_value = data.value;\n if (options.dedupId !== undefined) payload.event_id = options.dedupId;\n sdk('track', payload);\n },\n };\n}\n","// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n EventItem,\n TrackOptions,\n} from '../index.js';\n\ninterface FbqWindow {\n fbq?: (...args: unknown[]) => void;\n}\n\n/**\n * The names Meta accepts through `fbq('track')`. Anything else is a custom\n * event and goes through `trackCustom`, which is a different command rather\n * than a different argument.\n *\n * Seventeen standard events plus `PageView`, which Meta documents separately\n * from the seventeen and which the base snippet fires by itself.\n */\nconst STANDARD_EVENTS: readonly string[] = [\n 'AddPaymentInfo',\n 'AddToCart',\n 'AddToWishlist',\n 'CompleteRegistration',\n 'Contact',\n 'CustomizeProduct',\n 'Donate',\n 'FindLocation',\n 'InitiateCheckout',\n 'Lead',\n 'PageView',\n 'Purchase',\n 'Schedule',\n 'Search',\n 'StartTrial',\n 'SubmitApplication',\n 'Subscribe',\n 'ViewContent',\n];\n\n/**\n * How Meta spells the canonical vocabulary, which is GA4's.\n *\n * Only canonical names appear here. Meta has eight more standard events with\n * no GA4 counterpart (`Subscribe`, `StartTrial`, `Schedule`, `Contact`,\n * `Donate`, `FindLocation`, `CustomizeProduct`, `SubmitApplication`), and\n * inventing canonical names for them would make this library the author of a\n * vocabulary it deliberately borrows. A host that fires those maps its own\n * event name to them through `events`.\n *\n * A canonical event with no standard counterpart is **not** dropped: it goes\n * to `trackCustom` under its own name, because by hand that event would have\n * been sent.\n */\nconst CANONICAL_EVENTS: Record<string, EventBinding<string>> = {\n page_view: 'PageView',\n view_item: 'ViewContent',\n search: 'Search',\n add_to_cart: 'AddToCart',\n add_to_wishlist: 'AddToWishlist',\n begin_checkout: 'InitiateCheckout',\n add_payment_info: 'AddPaymentInfo',\n purchase: 'Purchase',\n sign_up: 'CompleteRegistration',\n generate_lead: 'Lead',\n};\n\n/** Optional configuration for the Meta browser provider. */\nexport interface MetaBrowserConfig {\n /**\n * How this pixel spells each canonical event, merged over the built-in map.\n *\n * This is how the eight standard events with no GA4 counterpart are\n * reached, and how an event is kept out of Meta entirely with `null`.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction fbqWindow(): FbqWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as FbqWindow);\n}\n\n/**\n * Sends a command, or fails the way a hand-written call would.\n *\n * Writing `fbq(…)` on a page without Meta's snippet throws, and this library\n * replicates that rather than swallowing it: a silent no-op is how a host\n * ends up believing it is measuring something.\n */\nfunction push(...args: unknown[]): void {\n const fbq = fbqWindow()?.fbq;\n\n if (!fbq) {\n throw new Error(\n \"meta: window.fbq is not defined. Add Meta's pixel to the page before creating tracking.\",\n );\n }\n\n fbq(...args);\n}\n\n/**\n * GA4's `items` become Meta's `contents`, which is a closed shape: `id`,\n * `quantity`, `item_price` and `delivery_category`, and nothing else.\n *\n * The rest of a GA4 item, `item_name` and `item_brand`, is dropped rather than\n * forwarded, because a key Meta does not read inside `contents` looks mapped\n * and arrives nowhere. That is the failure mode this project keeps finding.\n */\nfunction contents(items: readonly EventItem[]): Record<string, unknown>[] | undefined {\n // All or none: a partial list would silently describe only part of the order.\n // Check whitespace without rewriting the identifier the host supplied.\n if (items.some((item) => typeof item.item_id !== 'string' || !item.item_id.trim()))\n return undefined;\n\n return items.map((item) => {\n const content: Record<string, unknown> = {};\n\n if (item.item_id !== undefined) content.id = item.item_id;\n if (item.quantity !== undefined) content.quantity = item.quantity;\n if (item.price !== undefined) content.item_price = item.price;\n\n return content;\n });\n}\n\n/**\n * `value` and `currency` are spelled identically by both vendors, so they\n * pass through untouched. Everything the host named itself passes through\n * too: Meta keeps unknown keys as custom properties, which is exactly what a\n * hand-written call would have produced.\n *\n * `transaction_id` stays under its own name here. Meta's pixel documents no\n * order id among its object properties. `order_id` exists on the Conversions\n * API and not on this surface, so translating it would be inventing a slot.\n */\nfunction pixelParams(data: EventData): Record<string, unknown> {\n const { items, ...rest } = data;\n const params: Record<string, unknown> = { ...rest };\n\n if (items) {\n const mapped = contents(items);\n if (mapped !== undefined) params.contents = mapped;\n else delete params.contents;\n }\n\n return params;\n}\n\n/**\n * Meta's pixel has one consent switch where this library carries Google's\n * four signals, so the four are collapsed into it.\n *\n * The collapse is the strictest declared answer: any `denied` produces\n * `revoke`, and `grant` needs every declared signal to be `granted`. There is\n * no field to partition, so this is the only reduction that cannot widen a\n * denial, and widening is the one thing a projection must never do.\n *\n * A host that wants Meta to follow only its advertising category, and not its\n * analytics one, decides that before `createTracking` by leaving this\n * provider out, which is what `@tracklane/consent` exists to do.\n */\nfunction collapse(state: ConsentState): 'grant' | 'revoke' | undefined {\n const signals = [\n state.ad_storage,\n state.analytics_storage,\n state.ad_user_data,\n state.ad_personalization,\n ].filter((signal) => signal !== undefined);\n\n // Nothing declared produces no command. Reading silence as either answer\n // puts words in the visitor's mouth.\n if (signals.length === 0) return undefined;\n\n return signals.every((signal) => signal === 'granted') ? 'grant' : 'revoke';\n}\n\n/**\n * The Meta pixel in the browser, through `fbq`.\n *\n * **This provider does not load Meta's pixel.** The base code belongs on the\n * page, installed the way\n * [Meta documents it](https://developers.facebook.com/docs/meta-pixel/get-started):\n * a snippet in the HTML, or a tag manager. This library talks to the pixel\n * that is already there; it never injects a third-party script and never\n * issues `fbq('init')`.\n *\n * Three consequences follow from that, and all three are Meta's rules rather\n * than ours:\n *\n * - **There is no `identify` here.** Meta accepts advanced matching only in\n * the base code (\"be sure to place advanced matching parameters in the\n * pixel base code or the values will not be treated as manual advanced\n * matching values\"), and the base code is yours. Put `em`, `ph`, `fn`, `ln`\n * in your own `fbq('init')`, where the pixel normalises and hashes them for\n * you. On the server, where there is no snippet, the same identity travels\n * in `context.user` and is hashed by the adapter.\n * - **The initial consent declaration is not ours.** Meta wants\n * `fbq('consent', 'revoke')` before `init`, and by the time you hold what\n * `createTracking` returns that moment has passed. Ours is every\n * declaration after it, through `consent()`.\n * - **Limited Data Use is not ours either.** `fbq('dataProcessingOptions', …)`\n * is documented as running before `init`. It belongs in your snippet. The\n * server half, which has no snippet, carries it on its factory.\n *\n * **There is no pixel id here**, and that is deliberate rather than an\n * omission. `fbq('track')` reaches every pixel the page initialised, so an id\n * passed in would address nothing and the parameter would promise something\n * this adapter cannot deliver. Which pixels exist is declared by your snippet,\n * in `fbq('init')`.\n *\n * If any item lacks a nonempty `item_id`, the complete `contents` list is\n * omitted while totals and order metadata remain unchanged. This avoids\n * presenting a partial order or inventing product identifiers. The error\n * channel receives one payload-free warning after the browser command returns.\n *\n * @param config - Event name overrides, merged over the built-in map.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, meta } from 'tracklane/browser';\n *\n * // Meta's own base code is already in the page, and it named the pixel.\n * const { track } = createTracking({ providers: [meta()] });\n *\n * // fbq('track', 'Purchase', { value: 49.9, currency: 'BRL' }, { eventID: 'T-1' })\n * track('purchase', { value: 49.9, currency: 'BRL' }, { dedupId: 'T-1' });\n * ```\n */\nexport function meta(config: MetaBrowserConfig = {}): BrowserProvider {\n return {\n name: 'meta',\n // An event Meta has no standard name for is still an event Meta accepts,\n // through `trackCustom`. Nothing is silently dropped.\n default: 'passthrough',\n events: { ...CANONICAL_EVENTS, ...config.events },\n\n track(\n name: string,\n data: EventData,\n options: TrackOptions,\n report?: (message: string) => void,\n ): void {\n // `track`, which is what a hand-written call is, and the only form Meta\n // documents the deduplication object for.\n //\n // `trackSingle` exists and would address one pixel, the way GA4's\n // `send_to` does. It was used here and then abandoned: Meta publishes no\n // signature for `trackSingle` carrying `{ eventID }`, so the placement\n // that deduplication depends on had no contract behind it. It worked when\n // verified against a real pixel, and an undocumented shape that works\n // today can stop working with no error and no visible symptom — the\n // browser and server hits would simply both count.\n //\n // The cost is Meta's own: on a page with two pixels initialised, `track`\n // reaches both. That is what `fbq('track')` does by hand, so this\n // replicates it rather than deciding otherwise on the host's behalf.\n const command = STANDARD_EVENTS.includes(name) ? 'track' : 'trackCustom';\n const params = pixelParams(data);\n const args: unknown[] = [command, name, params];\n\n // Meta's key is `eventID` here and `event_id` on the Conversions API,\n // matched together with the event name. The library never invents the\n // value: only one the host already owns can match on both sides.\n if (options.dedupId !== undefined) args.push({ eventID: options.dedupId });\n\n push(...args);\n if (data.items && params.contents === undefined) {\n report?.('contents omitted: every item needs a nonempty item_id; totals were preserved');\n }\n },\n\n consent(_command: 'default' | 'update', state: ConsentState): void {\n // Meta has one consent command and no equivalent of Google's\n // default/update split, so the command maps to nothing. What is\n // declared is the state, and that is what is forwarded.\n const answer = collapse(state);\n if (answer) push('consent', answer);\n },\n };\n}\n","// Through the public entry point, exactly as a provider written elsewhere\n// would import it. A provider that needs something not exported here is\n// telling us the contract is incomplete, which is a conversation rather than\n// a reason to reach inside.\nimport type {\n BrowserProvider,\n ConsentState,\n EventBinding,\n EventData,\n TrackOptions,\n UserData,\n} from '../index.js';\n\n/** The slice of `posthog-js` this adapter calls. */\ninterface PosthogSdk {\n capture: (event: string, properties?: EventData) => void;\n identify: (distinctId: string) => void;\n setPersonProperties: (properties: Record<string, unknown>) => void;\n /** Since posthog-js 1.387.0. */\n unsetPersonProperties?: (names: string[]) => void;\n opt_in_capturing: () => void;\n opt_out_capturing: () => void;\n reset: () => void;\n}\n\ninterface PosthogWindow {\n posthog?: PosthogSdk;\n}\n\n/** Optional configuration for the PostHog browser provider. */\nexport interface PosthogBrowserConfig {\n /**\n * How this project spells each canonical event. Rarely needed: `capture()`\n * accepts any string, so the canonical vocabulary already is PostHog's.\n */\n events?: Record<string, EventBinding<string>>;\n}\n\n/** PostHog documents that it shortens any `distinct_id` longer than this. */\nconst MAX_DISTINCT_ID = 200;\n\n// Server rendering reaches this module with no browser at all, where\n// `window` is a ReferenceError rather than `undefined`.\nfunction posthogWindow(): PosthogWindow | undefined {\n return typeof window === 'undefined' ? undefined : (window as unknown as PosthogWindow);\n}\n\n/**\n * Returns the vendor SDK, or fails the way a hand-written call would.\n *\n * Calling `posthog.capture(...)` on a page without PostHog's snippet throws,\n * and this library replicates that rather than swallowing it: a silent no-op\n * is how a host ends up believing it is measuring something.\n */\nfunction sdk(): PosthogSdk {\n const instance = posthogWindow()?.posthog;\n\n if (!instance) {\n throw new Error(\n \"posthog: window.posthog is not defined. Add PostHog's snippet to the page before creating tracking.\",\n );\n }\n\n return instance;\n}\n\n/**\n * PostHog in the browser, through `posthog-js`.\n *\n * **This provider does not load PostHog's snippet.** It belongs on the page,\n * installed the way PostHog documents it: a snippet in the HTML, or a tag\n * manager. This library talks to the instance that is already there; it\n * never injects a third-party script into your page.\n *\n * @param config - Rarely needed event name overrides.\n * @returns A provider for `createTracking` from `tracklane/browser`.\n *\n * @example\n * ```ts\n * import { createTracking, posthog } from 'tracklane/browser';\n *\n * // PostHog's own snippet is already in the page.\n * const { track } = createTracking({ providers: [posthog()] });\n *\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function posthog(config: PosthogBrowserConfig = {}): BrowserProvider {\n const { events } = config;\n\n return {\n name: 'posthog',\n default: 'passthrough',\n ...(events ? { events } : {}),\n\n track(name: string, data: EventData, _options: TrackOptions): void {\n // capture() accepts any string as the event name and documents no\n // browser-to-server deduplication key, so dedupId maps nowhere — the\n // same gap GA4 has, for the same reason: there is no field to put it\n // in.\n sdk().capture(name, data);\n },\n\n identify(\n user: UserData,\n traits?: Record<string, unknown>,\n report?: (message: string) => void,\n ): void {\n // distinct_id is PostHog's only identity slot. email, phone and names\n // have no separate slot the way Meta's user_data has one for each —\n // they map to nothing here, exactly the gap GA4 has for the same\n // fields.\n if (user.userId !== undefined) {\n if (user.userId.length > MAX_DISTINCT_ID) {\n report?.(\n `distinct_id is longer than ${MAX_DISTINCT_ID} characters: PostHog truncates it, so two ids sharing that prefix become one person`,\n );\n }\n sdk().identify(user.userId);\n }\n if (!traits) return;\n\n const set: Record<string, unknown> = {};\n const unset: string[] = [];\n for (const [name, value] of Object.entries(traits)) {\n if (value === null) unset.push(name);\n else set[name] = value;\n }\n\n // setPersonProperties works against whichever identity is already\n // current, so it needs no id of its own — the same shape as GA4's\n // separate `set user_properties` push.\n if (Object.keys(set).length > 0) sdk().setPersonProperties(set);\n if (unset.length > 0) {\n const instance = sdk();\n // posthog-js 1.387.0 added `unsetPersonProperties`, which also clears\n // its flag and dedupe caches, so a later set of the same value is not\n // skipped. Older versions document only `$unset` inside a capture,\n // which is the same event that method sends.\n if (instance.unsetPersonProperties) instance.unsetPersonProperties(unset);\n else instance.capture('$set', { $unset: unset });\n }\n },\n\n reset(): void {\n // PostHog's own sign-out. It also clears posthog-js persistence,\n // including the opt-in state, which is why a host calls `consent`\n // again after it.\n sdk().reset();\n },\n\n consent(_command: 'default' | 'update', state: ConsentState): void {\n // opt_in_capturing/opt_out_capturing is one binary switch, local SDK\n // state, not a per-signal API — nothing like Google's four commands.\n // analytics_storage is the one signal that actually describes what\n // this toggle governs (whether PostHog stores and sends analytics\n // events at all), so it is the one signal this collapses onto. The\n // other three have no honest PostHog equivalent; folding them in too\n // would risk an ad-only denial silently switching off analytics\n // capture the visitor never objected to, so they are left alone.\n const { analytics_storage } = state;\n\n if (analytics_storage === 'denied') {\n sdk().opt_out_capturing();\n } else if (analytics_storage === 'granted') {\n sdk().opt_in_capturing();\n }\n // Absent: no command fires. Whatever posture the page's own PostHog\n // snippet already set — opted in by default, or opted out through\n // `opt_out_capturing_by_default` — stands, rather than being guessed at.\n },\n };\n}\n","import { assertUniqueProviders, makeReporter, messageOf, resolveEvent } from './dispatch.js';\n\nexport { type Ga4BrowserConfig, ga4 } from './providers/ga4.browser.js';\nexport { type LinkedinBrowserConfig, linkedin } from './providers/linkedin.browser.js';\nexport { type MetaBrowserConfig, meta } from './providers/meta.browser.js';\nexport { type PosthogBrowserConfig, posthog } from './providers/posthog.browser.js';\n\nimport type {\n ConsentState,\n EventBinding,\n EventData,\n ProviderDefault,\n TrackingError,\n UserData,\n} from './vocabulary.js';\n\n/**\n * Everything the browser needs beyond the event itself.\n *\n * There is one field, and that is the point: in the browser each vendor tag\n * reads its own cookies and holds its own session, so the only thing left\n * for the host to supply is the value that ties this hit to a server-side\n * one.\n */\nexport interface TrackOptions {\n /**\n * The value that lets a vendor recognise this hit and a server-side hit as\n * one event. The library never invents it: the browser and the server do\n * not share a process, so only a value the host already owns (an order id)\n * can match on both sides.\n */\n dedupId?: string;\n}\n\n/**\n * A destination for browser events.\n *\n * This is the whole contract. A provider written outside this package\n * implements it exactly as the providers shipped here do.\n */\nexport interface BrowserProvider<Target = string> {\n /** The vendor's fixed name. Registering one twice is an error. */\n name: string;\n /** What happens to a canonical event this vendor has no binding for. */\n default: ProviderDefault;\n /** How this vendor spells each canonical event. */\n events?: Record<string, EventBinding<Target>>;\n /**\n * Loads the vendor tag, at creation.\n *\n * **Make it idempotent.** A host that rebuilds its tracking, as\n * `@tracklane/consent` does on every consent answer, creates again and\n * installs again, including providers that were already there.\n */\n install?(): void | Promise<void>;\n /**\n * Sends one event to this vendor. Throwing here reaches `onError`, not the caller.\n * The optional report callback emits a payload-free warning; it is not proof\n * of delivery or provider-side processing. Older three-argument providers\n * and direct calls remain supported.\n */\n track(\n name: Target,\n data: EventData,\n options: TrackOptions,\n report?: (message: string) => void,\n ): void;\n /**\n * Absent where the vendor keeps no user of its own. A trait set to `null`\n * means \"no longer known\". The optional report callback emits a\n * payload-free warning, as on `track`.\n */\n identify?(\n user: UserData,\n traits?: Record<string, unknown>,\n report?: (message: string) => void,\n ): void;\n /** Forgets the user, at sign-out. Absent where the vendor keeps no user of its own. */\n reset?(): void;\n /** Absent where the vendor documents no consent command. */\n consent?(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/** Options accepted by {@link createTracking}. */\nexport interface BrowserTrackingOptions {\n /** The vendors to send every event to. Registering the same `name` twice throws. */\n providers: readonly BrowserProvider[];\n /**\n * The one diagnostic channel. Without it the library is silent, exactly as\n * a hand-written vendor call is.\n * Unmapped events on an `ignore` provider report warnings; explicit `null`\n * exclusions stay silent. Attach a reporter in development to catch omissions.\n */\n onError?: (error: TrackingError) => void;\n}\n\n/** What {@link createTracking} returns. */\nexport interface BrowserTracking {\n /** Sends one event to every configured vendor. Never throws. */\n track(name: string, data?: EventData, options?: TrackOptions): void;\n /** Forwards the user to the vendors that hold one, and to nobody else. */\n identify(user: UserData, traits?: Record<string, unknown>): void;\n /**\n * Forgets the user in every vendor that holds one, at sign-out. Never throws.\n *\n * Vendors differ: GA4 clears `user_id` and keeps user properties until the\n * page reloads; PostHog's own reset also forgets its opt-in, so call\n * `consent('update', …)` again after it.\n */\n reset(): void;\n /**\n * Forwards a consent declaration to every vendor that documents a command\n * for it. It never decides, stores or withholds anything.\n */\n consent(command: 'default' | 'update', state: ConsentState): void;\n}\n\n/**\n * Creates the browser tracking surface.\n *\n * Providers install eagerly, which is what pasting a vendor snippet does and\n * what lets each vendor record its own initial page view. Every vendor\n * shipped here queues its own commands until its script lands, so nothing is\n * lost to loading and this library needs no queue of its own.\n *\n * @param options - The providers to send to, and an optional error channel.\n * @returns The `track`, `identify`, `reset` and `consent` calls.\n *\n * @example\n * ```ts\n * import { createTracking, ga4 } from 'tracklane/browser';\n *\n * const { track } = createTracking({ providers: [ga4('G-XXXXXXX')] });\n * track('purchase', { transaction_id: 'T-1', value: 49.9, currency: 'BRL' });\n * ```\n */\nexport function createTracking(options: BrowserTrackingOptions): BrowserTracking {\n const { providers } = options;\n assertUniqueProviders(providers);\n\n const report = makeReporter(options.onError);\n\n const fail = (provider: string, event: string, cause: unknown): void => {\n report({ provider, event, severity: 'error', message: messageOf(cause), cause });\n };\n\n for (const provider of providers) {\n try {\n const installing = provider.install?.();\n // An async loader that rejects would otherwise become an unhandled\n // rejection, the silent failure this channel exists to prevent.\n void Promise.resolve(installing).catch((cause: unknown) => {\n fail(provider.name, 'install', cause);\n });\n } catch (cause) {\n fail(provider.name, 'install', cause);\n }\n }\n\n return {\n track(name, data = {}, trackOptions = {}) {\n for (const provider of providers) {\n const target = resolveEvent(provider, name, (message) => {\n report({ provider: provider.name, event: name, severity: 'warning', message });\n });\n if (target === undefined) continue;\n\n try {\n provider.track(target, data, trackOptions, (message) => {\n report({ provider: provider.name, event: name, severity: 'warning', message });\n });\n } catch (cause) {\n fail(provider.name, name, cause);\n }\n }\n },\n\n identify(user, traits) {\n for (const provider of providers) {\n try {\n provider.identify?.(user, traits, (message) => {\n report({ provider: provider.name, event: 'identify', severity: 'warning', message });\n });\n } catch (cause) {\n fail(provider.name, 'identify', cause);\n }\n }\n },\n\n reset() {\n for (const provider of providers) {\n try {\n provider.reset?.();\n } catch (cause) {\n fail(provider.name, 'reset', cause);\n }\n }\n },\n\n consent(command, state) {\n for (const provider of providers) {\n try {\n provider.consent?.(command, state);\n } catch (cause) {\n fail(provider.name, 'consent', cause);\n }\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -67,13 +67,28 @@ interface Ga4ServerCredentials {
67
67
  }
68
68
  /** Optional configuration for the GA4 server provider. */
69
69
  interface Ga4ServerConfig {
70
+ /** How this property spells each canonical event. Rarely needed. */
70
71
  events?: Record<string, EventBinding<string>>;
72
+ /**
73
+ * Sends `user_data` built from `user` (email, phone, first and last name),
74
+ * normalised and SHA-256 hashed the way Google documents. Off by default:
75
+ * it starts a new flow of personal data to Google, and the property needs
76
+ * user-provided data collection turned on in GA4's admin.
77
+ *
78
+ * There is no consent check in it. To withhold it, pass `user: { userId }`.
79
+ */
80
+ userProvidedData?: boolean;
71
81
  }
72
82
  /**
73
83
  * Google Analytics 4 on the server, through the Measurement Protocol.
74
84
  *
85
+ * **`identify` sends user properties with no event**, which Google
86
+ * documents. With no user property to send it sends nothing, because each
87
+ * `track` already carries `user_id`.
88
+ *
75
89
  * @param credentials - The measurement id and API secret.
76
- * @param config - Rarely needed event name overrides.
90
+ * @param config - Rarely needed event name overrides, and the opt-in for
91
+ * user-provided data.
77
92
  * @returns A provider for `createTracking` from `tracklane/server`.
78
93
  *
79
94
  * @example
@@ -233,6 +248,12 @@ interface PosthogServerConfig {
233
248
  * Deduplication is also eventual, happening during a background merge, so both
234
249
  * rows are visible for a while and a test that counts immediately will fail.
235
250
  *
251
+ * **`identify` sends one `$identify`**, with `$set` and `$unset` from the
252
+ * traits. It merges a visitor into the user only when the context carries
253
+ * `anonymousId`, never from the cookie: a merge is irreversible, and PostHog
254
+ * reports a refused one only as an ingestion warning in its own interface.
255
+ * Identified events are billed with person processing.
256
+ *
236
257
  * @param credentials - The project API key, and the ingestion host for
237
258
  * anything other than PostHog's default US Cloud instance.
238
259
  * @param config - Rarely needed event name overrides.
@@ -430,8 +451,31 @@ interface EventContext {
430
451
  ip?: string;
431
452
  /** The visitor's user agent. Meta's Conversions API reads it alongside `ip`. */
432
453
  userAgent?: string;
433
- /** What is known *about* the person: GA4 user properties, PostHog `$set`. */
454
+ /**
455
+ * What is known *about* the person: GA4 user properties, PostHog `$set`.
456
+ *
457
+ * A trait set to `null` means "no longer known". PostHog receives it in
458
+ * `$unset`. GA4 leaves it out and reports that it did, because clearing a
459
+ * user property through the Measurement Protocol is not yet verified.
460
+ */
434
461
  traits?: Record<string, unknown>;
462
+ /**
463
+ * When the traits were true, if not when the event happened. Same units as
464
+ * `timestamp`. GA4 stamps each user property with it; vendors with no slot
465
+ * ignore it.
466
+ */
467
+ traitsAt?: number | Date;
468
+ /**
469
+ * Your own stable id for a visitor nobody has identified yet. Never
470
+ * generated by the library.
471
+ *
472
+ * PostHog uses it as `distinct_id` when there is no `user.userId`, ahead of
473
+ * its cookie, and `identify` merges it into the user. It must be the id the
474
+ * earlier PostHog events were captured under, or it merges nothing.
475
+ */
476
+ anonymousId?: string;
477
+ /** Cancels the vendor requests. An aborted send is reported as an error. */
478
+ signal?: AbortSignal;
435
479
  /**
436
480
  * Travels inside the event, for the vendors that document such a field.
437
481
  * Today that is GA4's Measurement Protocol, and it reads two of the
@@ -440,11 +484,23 @@ interface EventContext {
440
484
  consent?: ConsentState;
441
485
  }
442
486
  /** The context an adapter receives, with the ambiguity resolved. */
443
- interface ResolvedContext extends Omit<EventContext, "cookies" | "timestamp"> {
487
+ interface ResolvedContext extends Omit<EventContext, "cookies" | "timestamp" | "traitsAt"> {
444
488
  /** The request's cookies, already split into a `name → value` map. */
445
489
  cookies: Record<string, string>;
446
490
  /** Epoch milliseconds. Adapters convert to their vendor's spelling. */
447
491
  timestamp: number;
492
+ /** Epoch milliseconds, when the host gave one. */
493
+ traitsAt?: number;
494
+ }
495
+ /**
496
+ * The context {@link ServerTracking.identify} takes: the same one `track`
497
+ * takes, with the person's id required.
498
+ */
499
+ interface IdentifyContext extends EventContext {
500
+ /** Who the person is. `userId` is required: identifying nobody is not a call. */
501
+ user: UserData & {
502
+ userId: string;
503
+ };
448
504
  }
449
505
  /**
450
506
  * A destination for server events.
@@ -466,6 +522,11 @@ interface ServerProvider<Target = string> {
466
522
  events?: Record<string, EventBinding<Target>>;
467
523
  /** Sends one event to this vendor's conversion API. May throw, or call `report` for a caveat. */
468
524
  track(name: Target, data: EventData, context: ResolvedContext, report: (message: string) => void): Promise<void>;
525
+ /**
526
+ * Tells this vendor who the person is, with no event. Absent where the
527
+ * vendor keeps no person of its own. May throw, or call `report` for a caveat.
528
+ */
529
+ identify?(context: ResolvedContext, report: (message: string) => void): Promise<void>;
469
530
  }
470
531
  /** Options accepted by {@link createTracking}. */
471
532
  interface ServerTrackingOptions {
@@ -490,21 +551,31 @@ interface ServerTracking {
490
551
  * guaranteed.
491
552
  */
492
553
  track(name: string, data?: EventData, context?: EventContext): Promise<void>;
554
+ /**
555
+ * Tells every vendor that keeps a person who this one is, with no event:
556
+ * PostHog's `$identify` and a GA4 request with user properties only.
557
+ * Vendors that keep no person receive nothing.
558
+ *
559
+ * A merge happens only when `anonymousId` is given. PostHog merges are
560
+ * irreversible, so pass it only when you know that visitor is this person.
561
+ * Resolves once every provider has settled and never rejects.
562
+ */
563
+ identify(context: IdentifyContext): Promise<void>;
493
564
  }
494
565
  /**
495
566
  * Creates the server tracking surface.
496
567
  *
497
- * There is no `identify` here and nothing is held between calls: a server
498
- * instance is shared by every request, so a retained identity would attach
499
- * one visitor to another visitor's conversion. Identity travels with the
500
- * call, which is also what every conversion API expects.
568
+ * Nothing is held between calls: a server instance is shared by every
569
+ * request, so a retained identity would attach one visitor to another
570
+ * visitor's conversion. Identity travels with each call, `identify`
571
+ * included, which is also what every conversion API expects.
501
572
  *
502
573
  * There is no `consent` call either. Consent commands are properties of a
503
574
  * vendor's on-page tag, and a server request has no tag. The one vendor that
504
575
  * accepts consent server-side reads it from the context.
505
576
  *
506
577
  * @param options - The providers to send to, and an optional error channel.
507
- * @returns The `track` call.
578
+ * @returns The `track` and `identify` calls.
508
579
  *
509
580
  * @example
510
581
  * ```ts
@@ -742,8 +813,14 @@ interface BrowserProvider<Target = string> {
742
813
  * and direct calls remain supported.
743
814
  */
744
815
  track(name: Target, data: EventData, options: TrackOptions, report?: (message: string) => void): void;
745
- /** Absent where the vendor keeps no user of its own. */
746
- identify?(user: UserData, traits?: Record<string, unknown>): void;
816
+ /**
817
+ * Absent where the vendor keeps no user of its own. A trait set to `null`
818
+ * means "no longer known". The optional report callback emits a
819
+ * payload-free warning, as on `track`.
820
+ */
821
+ identify?(user: UserData, traits?: Record<string, unknown>, report?: (message: string) => void): void;
822
+ /** Forgets the user, at sign-out. Absent where the vendor keeps no user of its own. */
823
+ reset?(): void;
747
824
  /** Absent where the vendor documents no consent command. */
748
825
  consent?(command: "default" | "update", state: ConsentState): void;
749
826
  }
@@ -765,6 +842,14 @@ interface BrowserTracking {
765
842
  track(name: string, data?: EventData, options?: TrackOptions): void;
766
843
  /** Forwards the user to the vendors that hold one, and to nobody else. */
767
844
  identify(user: UserData, traits?: Record<string, unknown>): void;
845
+ /**
846
+ * Forgets the user in every vendor that holds one, at sign-out. Never throws.
847
+ *
848
+ * Vendors differ: GA4 clears `user_id` and keeps user properties until the
849
+ * page reloads; PostHog's own reset also forgets its opt-in, so call
850
+ * `consent('update', …)` again after it.
851
+ */
852
+ reset(): void;
768
853
  /**
769
854
  * Forwards a consent declaration to every vendor that documents a command
770
855
  * for it. It never decides, stores or withholds anything.
@@ -780,7 +865,7 @@ interface BrowserTracking {
780
865
  * lost to loading and this library needs no queue of its own.
781
866
  *
782
867
  * @param options - The providers to send to, and an optional error channel.
783
- * @returns The `track`, `identify` and `consent` calls.
868
+ * @returns The `track`, `identify`, `reset` and `consent` calls.
784
869
  *
785
870
  * @example
786
871
  * ```ts
@@ -792,5 +877,5 @@ interface BrowserTracking {
792
877
  */
793
878
  declare function createTracking(options: BrowserTrackingOptions): BrowserTracking;
794
879
  //#endregion
795
- export { posthog$1 as A, VendorResponseError as B, EventData as C, UserData as D, TrackingError as E, LinkedinServerCredentials as F, linkedin$1 as I, Ga4ServerConfig as L, MetaServerCredentials as M, meta$1 as N, PosthogServerConfig as O, LinkedinServerConfig as P, Ga4ServerCredentials as R, EventBinding as S, ProviderDefault as T, isVendorResponseError as V, ServerTracking as _, createTracking as a, ActionSource as b, MetaBrowserConfig as c, linkedin as d, Ga4BrowserConfig as f, ServerProvider as g, ResolvedContext as h, TrackOptions as i, MetaServerConfig as j, PosthogServerCredentials as k, meta as l, EventContext as m, BrowserTracking as n, PosthogBrowserConfig as o, ga4 as p, BrowserTrackingOptions as r, posthog as s, BrowserProvider as t, LinkedinBrowserConfig as u, ServerTrackingOptions as v, EventItem as w, ConsentState as x, createTracking$1 as y, ga4$1 as z };
796
- //# sourceMappingURL=browser-34-CHcxK.d.ts.map
880
+ export { PosthogServerCredentials as A, ga4$1 as B, EventBinding as C, TrackingError as D, ProviderDefault as E, LinkedinServerConfig as F, isVendorResponseError as H, LinkedinServerCredentials as I, linkedin$1 as L, MetaServerConfig as M, MetaServerCredentials as N, UserData as O, meta$1 as P, Ga4ServerConfig as R, ConsentState as S, EventItem as T, VendorResponseError as V, ServerProvider as _, createTracking as a, createTracking$1 as b, MetaBrowserConfig as c, linkedin as d, Ga4BrowserConfig as f, ResolvedContext as g, IdentifyContext as h, TrackOptions as i, posthog$1 as j, PosthogServerConfig as k, meta as l, EventContext as m, BrowserTracking as n, PosthogBrowserConfig as o, ga4 as p, BrowserTrackingOptions as r, posthog as s, BrowserProvider as t, LinkedinBrowserConfig as u, ServerTracking as v, EventData as w, ActionSource as x, ServerTrackingOptions as y, Ga4ServerCredentials as z };
881
+ //# sourceMappingURL=browser-k4gaaQgF.d.ts.map
package/dist/browser.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as createTracking, c as MetaBrowserConfig, d as linkedin, f as Ga4BrowserConfig, i as TrackOptions, l as meta, n as BrowserTracking, o as PosthogBrowserConfig, p as ga4, r as BrowserTrackingOptions, s as posthog, t as BrowserProvider, u as LinkedinBrowserConfig } from "./browser-34-CHcxK.js";
1
+ import { a as createTracking, c as MetaBrowserConfig, d as linkedin, f as Ga4BrowserConfig, i as TrackOptions, l as meta, n as BrowserTracking, o as PosthogBrowserConfig, p as ga4, r as BrowserTrackingOptions, s as posthog, t as BrowserProvider, u as LinkedinBrowserConfig } from "./browser-k4gaaQgF.js";
2
2
  export { BrowserProvider, BrowserTracking, BrowserTrackingOptions, type Ga4BrowserConfig, type LinkedinBrowserConfig, type MetaBrowserConfig, type PosthogBrowserConfig, TrackOptions, createTracking, ga4, linkedin, meta, posthog };
package/dist/browser.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ga4, i as linkedin, n as posthog, r as meta, t as createTracking } from "./browser-DhzO5qPS.js";
1
+ import { a as ga4, i as linkedin, n as posthog, r as meta, t as createTracking } from "./browser-ChcWOQ3N.js";
2
2
  export { createTracking, ga4, linkedin, meta, posthog };
@@ -1,4 +1,4 @@
1
- import { g as ServerProvider, h as ResolvedContext, t as BrowserProvider } from "./browser-34-CHcxK.js";
1
+ import { _ as ServerProvider, g as ResolvedContext, t as BrowserProvider } from "./browser-k4gaaQgF.js";
2
2
  //#region src/conformance.d.ts
3
3
  /**
4
4
  * Runs the shared conformance suite against a browser provider.
@@ -1,6 +1,6 @@
1
1
  import { n as isVendorResponseError } from "./src-DLRGPWMj.js";
2
- import { t as createTracking } from "./browser-DhzO5qPS.js";
3
- import { t as createTracking$1 } from "./server-DnP7RtEn.js";
2
+ import { t as createTracking } from "./browser-ChcWOQ3N.js";
3
+ import { t as createTracking$1 } from "./server-bgUN2MOi.js";
4
4
  import { expect, it } from "vitest";
5
5
  //#region src/conformance.ts
6
6
  /**
@@ -188,6 +188,22 @@ function conformsAsServerProvider(options) {
188
188
  if (with_ !== without) expect(with_).toContain("DEDUP-XYZ");
189
189
  expect(without).not.toContain("DEDUP-XYZ");
190
190
  });
191
+ it("identifies the same way twice, and never merges unless told to", async () => {
192
+ const provider = create();
193
+ if (!provider.identify) return;
194
+ const identity = {
195
+ ...context(),
196
+ user: { userId: "USER-XYZ" },
197
+ traits: { plan: "pro" }
198
+ };
199
+ reset();
200
+ await provider.identify(identity, noop);
201
+ const first = JSON.stringify(captured());
202
+ reset();
203
+ await create().identify?.(identity, noop);
204
+ expect(JSON.stringify(captured())).toBe(first);
205
+ expect(first).not.toContain("anon_distinct_id");
206
+ });
191
207
  it("throws a VendorResponseError when the vendor did not accept the send", async () => {
192
208
  reset();
193
209
  fail();
@@ -1 +1 @@
1
- {"version":3,"file":"conformance.js","names":["createBrowserTracking","createServerTracking"],"sources":["../src/conformance.ts"],"sourcesContent":["import { expect, it } from 'vitest';\nimport { type BrowserProvider, createTracking as createBrowserTracking } from './browser.js';\nimport { isVendorResponseError } from './errors.js';\nimport {\n createTracking as createServerTracking,\n type ResolvedContext,\n type ServerProvider,\n} from './server.js';\nimport type { EventData, ProviderDefault } from './vocabulary.js';\n\n/**\n * The behaviour every provider owes, which the types cannot express.\n *\n * ADR-0004 requires this suite because a provider can satisfy\n * {@link BrowserProvider} or {@link ServerProvider} perfectly and still break\n * a rule the whole library rests on. The interface cannot say \"never invent an\n * identifier\" or \"never expand a consent signal\"; a test can.\n *\n * These are invariants, not vendor behaviour. What GA4 sends and how it spells\n * it belongs in the GA4 tests. What follows must hold for a provider written\n * by anyone, for a vendor nobody here has heard of.\n *\n * **Published as `tracklane/conformance`**, because the claim that a provider\n * written elsewhere uses the same contract as the ones shipped here is only\n * true if it can run the same checks. `vitest` is an optional peer: import this\n * from a test file and call one of the two functions below inside a `describe`.\n *\n * **A wrapper owes the same invariants as a provider.** Wrapping a shipped\n * provider and delegating (see the provider documentation) produces an object\n * that the dispatcher cannot tell apart from any other, so run it through here\n * too. On the server half that catches the expensive mistake: a wrapper whose\n * `track` does not `await` the provider it delegates to resolves before the\n * request settles, which a serverless runtime turns into a conversion that\n * never left. {@link conformsAsServerProvider} fails on it, because a vendor\n * refusal can no longer reach the caller.\n */\n\nconst DEFAULTS: ProviderDefault[] = ['passthrough', 'ignore'];\n\n/** A payload with nothing in it that could excuse a difference between runs. */\nconst STABLE_DATA: EventData = { transaction_id: 'T-1', value: 49.9, currency: 'BRL' };\n\nfunction assertIdentity(provider: { name: string; default: ProviderDefault }): void {\n it('names itself, once and stably', () => {\n // The name is what a duplicate registration is detected by, and what an\n // error report is attributed to.\n expect(provider.name).toBeTruthy();\n expect(provider.name.trim()).toBe(provider.name);\n });\n\n it('declares what happens to an event it has no binding for', () => {\n expect(DEFAULTS).toContain(provider.default);\n });\n}\n\n/**\n * Runs the shared conformance suite against a browser provider.\n *\n * @param create - Builds the provider under test. Called per assertion, so a\n * provider that accumulates state between runs fails rather than hides.\n * @param captured - Everything the provider handed the vendor since the last\n * reset, in order.\n * @param reset - Clears what `captured` returns.\n * @param target - An already-resolved vendor target. Defaults to `purchase`.\n *\n * @example\n * ```ts\n * describe('ga4 browser conformance', () => {\n * conformsAsBrowserProvider({\n * create: () => ga4('G-ABC'),\n * captured: () => calls,\n * reset: () => { calls = []; },\n * });\n * });\n * ```\n */\nexport function conformsAsBrowserProvider(options: {\n create: () => BrowserProvider;\n captured: () => unknown[];\n reset: () => void;\n /** Already-resolved vendor target; conversion-ID providers use their configured ID. */\n target?: string;\n}): void {\n const { create, captured, reset } = options;\n const target = options.target ?? 'purchase';\n\n assertIdentity(create());\n\n it('reports absent mappings on ignore providers and keeps explicit exclusions silent', () => {\n // Mapping assertions do not exercise the host's installation hook.\n const { install: _install, ...provider } = create();\n if (provider.default !== 'ignore') return;\n reset();\n const errors: { severity: string; event: string }[] = [];\n const tracking = createBrowserTracking({\n providers: [{ ...provider, events: { explicit: null } }],\n onError: (error) => errors.push(error),\n });\n tracking.track('missing');\n tracking.track('explicit');\n expect(errors).toMatchObject([{ severity: 'warning', event: 'missing' }]);\n expect(captured()).toEqual([]);\n });\n\n it('sends the same thing twice for the same event', () => {\n // The library never invents an identifier, and this is how that rule is\n // caught rather than trusted: a generated id, a random value or a clock\n // read makes two identical calls differ.\n reset();\n create().track(target, STABLE_DATA, {});\n const first = JSON.stringify(captured());\n\n reset();\n create().track(target, STABLE_DATA, {});\n const second = JSON.stringify(captured());\n\n expect(second).toBe(first);\n });\n\n it('forwards the deduplication id only when it was given one', () => {\n reset();\n create().track(target, STABLE_DATA, {});\n const without = JSON.stringify(captured());\n\n reset();\n create().track(target, STABLE_DATA, { dedupId: 'DEDUP-XYZ' });\n const with_ = JSON.stringify(captured());\n\n // Either the vendor has the concept, and the value appears where it did\n // not before, or it has none and nothing changes. Inventing one to fill\n // the slot is the failure this catches.\n if (with_ !== without) expect(with_).toContain('DEDUP-XYZ');\n expect(without).not.toContain('DEDUP-XYZ');\n });\n\n it('never expands a consent signal it was not given', () => {\n const provider = create();\n if (!provider.consent) return;\n\n reset();\n provider.consent('update', { analytics_storage: 'denied' });\n\n // One denial in must never become a grant out. Collapsing Google's\n // vocabulary for a coarser vendor is translation; widening it invents a\n // permission the visitor never gave.\n expect(JSON.stringify(captured())).not.toContain('granted');\n });\n}\n\n/**\n * Runs the shared conformance suite against a server provider.\n *\n * @param create - Builds the provider under test, per assertion.\n * @param captured - Everything the provider sent since the last reset.\n * @param reset - Clears what `captured` returns, and restores a successful\n * transport.\n * @param fail - Makes the next send fail at the transport, so the suite can\n * check that the provider is loud about it.\n * @param context - A resolved context with a fixed timestamp.\n * @param target - An already-resolved vendor target. Defaults to `purchase`.\n *\n * @example\n * ```ts\n * describe('ga4 server conformance', () => {\n * conformsAsServerProvider({\n * create: () => ga4(credentials),\n * captured: () => fetchMock.mock.calls,\n * reset: () => { fetchMock.mockClear(); },\n * fail: () => { fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); },\n * context,\n * });\n * });\n * ```\n */\nexport function conformsAsServerProvider(options: {\n create: () => ServerProvider;\n captured: () => unknown[];\n reset: () => void;\n fail: () => void;\n context: () => ResolvedContext;\n /** Already-resolved vendor target; conversion-ID providers use their configured ID. */\n target?: string;\n}): void {\n const { create, captured, reset, fail, context } = options;\n const target = options.target ?? 'purchase';\n const noop = (): void => {};\n\n assertIdentity(create());\n\n it('reports absent mappings on ignore providers and keeps explicit exclusions silent', async () => {\n const provider = create();\n if (provider.default !== 'ignore') return;\n reset();\n const errors: { severity: string; event: string }[] = [];\n const tracking = createServerTracking({\n providers: [{ ...provider, events: { explicit: null } }],\n onError: (error) => errors.push(error),\n });\n await tracking.track('missing', {}, context());\n await tracking.track('explicit', {}, context());\n expect(errors).toMatchObject([{ severity: 'warning', event: 'missing' }]);\n expect(captured()).toEqual([]);\n });\n\n it('sends the same thing twice for the same event', async () => {\n // The context carries a fixed timestamp, so any difference between these\n // two is something the provider produced on its own.\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n const first = JSON.stringify(captured());\n\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n\n expect(JSON.stringify(captured())).toBe(first);\n });\n\n it('forwards the deduplication id only when it was given one', async () => {\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n const without = JSON.stringify(captured());\n\n reset();\n await create().track(target, STABLE_DATA, { ...context(), dedupId: 'DEDUP-XYZ' }, noop);\n const with_ = JSON.stringify(captured());\n\n if (with_ !== without) expect(with_).toContain('DEDUP-XYZ');\n expect(without).not.toContain('DEDUP-XYZ');\n });\n\n it('throws a VendorResponseError when the vendor did not accept the send', async () => {\n // Adapters stay honest and loud. Swallowing a non-2xx is how a host ends\n // up believing it is measuring something, and the dispatcher is already\n // there to isolate the throw and report it. ADR-0013 adds the shape: what\n // reaches the host is recognised by `isVendorResponseError`, carrying the\n // status and the body, not just any throw. The body itself is\n // the caller's `fail()` hook to produce; a bodiless response is valid\n // (the documented example does exactly that), so it is not asserted here.\n reset();\n fail();\n\n let caught: unknown;\n try {\n await create().track(target, STABLE_DATA, context(), noop);\n } catch (error) {\n caught = error;\n }\n\n expect(isVendorResponseError(caught)).toBe(true);\n if (isVendorResponseError(caught)) {\n expect(typeof caught.status).toBe('number');\n expect(typeof caught.body).toBe('string');\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"conformance.js","names":["createBrowserTracking","createServerTracking"],"sources":["../src/conformance.ts"],"sourcesContent":["import { expect, it } from 'vitest';\nimport { type BrowserProvider, createTracking as createBrowserTracking } from './browser.js';\nimport { isVendorResponseError } from './errors.js';\nimport {\n createTracking as createServerTracking,\n type ResolvedContext,\n type ServerProvider,\n} from './server.js';\nimport type { EventData, ProviderDefault } from './vocabulary.js';\n\n/**\n * The behaviour every provider owes, which the types cannot express.\n *\n * ADR-0004 requires this suite because a provider can satisfy\n * {@link BrowserProvider} or {@link ServerProvider} perfectly and still break\n * a rule the whole library rests on. The interface cannot say \"never invent an\n * identifier\" or \"never expand a consent signal\"; a test can.\n *\n * These are invariants, not vendor behaviour. What GA4 sends and how it spells\n * it belongs in the GA4 tests. What follows must hold for a provider written\n * by anyone, for a vendor nobody here has heard of.\n *\n * **Published as `tracklane/conformance`**, because the claim that a provider\n * written elsewhere uses the same contract as the ones shipped here is only\n * true if it can run the same checks. `vitest` is an optional peer: import this\n * from a test file and call one of the two functions below inside a `describe`.\n *\n * **A wrapper owes the same invariants as a provider.** Wrapping a shipped\n * provider and delegating (see the provider documentation) produces an object\n * that the dispatcher cannot tell apart from any other, so run it through here\n * too. On the server half that catches the expensive mistake: a wrapper whose\n * `track` does not `await` the provider it delegates to resolves before the\n * request settles, which a serverless runtime turns into a conversion that\n * never left. {@link conformsAsServerProvider} fails on it, because a vendor\n * refusal can no longer reach the caller.\n */\n\nconst DEFAULTS: ProviderDefault[] = ['passthrough', 'ignore'];\n\n/** A payload with nothing in it that could excuse a difference between runs. */\nconst STABLE_DATA: EventData = { transaction_id: 'T-1', value: 49.9, currency: 'BRL' };\n\nfunction assertIdentity(provider: { name: string; default: ProviderDefault }): void {\n it('names itself, once and stably', () => {\n // The name is what a duplicate registration is detected by, and what an\n // error report is attributed to.\n expect(provider.name).toBeTruthy();\n expect(provider.name.trim()).toBe(provider.name);\n });\n\n it('declares what happens to an event it has no binding for', () => {\n expect(DEFAULTS).toContain(provider.default);\n });\n}\n\n/**\n * Runs the shared conformance suite against a browser provider.\n *\n * @param create - Builds the provider under test. Called per assertion, so a\n * provider that accumulates state between runs fails rather than hides.\n * @param captured - Everything the provider handed the vendor since the last\n * reset, in order.\n * @param reset - Clears what `captured` returns.\n * @param target - An already-resolved vendor target. Defaults to `purchase`.\n *\n * @example\n * ```ts\n * describe('ga4 browser conformance', () => {\n * conformsAsBrowserProvider({\n * create: () => ga4('G-ABC'),\n * captured: () => calls,\n * reset: () => { calls = []; },\n * });\n * });\n * ```\n */\nexport function conformsAsBrowserProvider(options: {\n create: () => BrowserProvider;\n captured: () => unknown[];\n reset: () => void;\n /** Already-resolved vendor target; conversion-ID providers use their configured ID. */\n target?: string;\n}): void {\n const { create, captured, reset } = options;\n const target = options.target ?? 'purchase';\n\n assertIdentity(create());\n\n it('reports absent mappings on ignore providers and keeps explicit exclusions silent', () => {\n // Mapping assertions do not exercise the host's installation hook.\n const { install: _install, ...provider } = create();\n if (provider.default !== 'ignore') return;\n reset();\n const errors: { severity: string; event: string }[] = [];\n const tracking = createBrowserTracking({\n providers: [{ ...provider, events: { explicit: null } }],\n onError: (error) => errors.push(error),\n });\n tracking.track('missing');\n tracking.track('explicit');\n expect(errors).toMatchObject([{ severity: 'warning', event: 'missing' }]);\n expect(captured()).toEqual([]);\n });\n\n it('sends the same thing twice for the same event', () => {\n // The library never invents an identifier, and this is how that rule is\n // caught rather than trusted: a generated id, a random value or a clock\n // read makes two identical calls differ.\n reset();\n create().track(target, STABLE_DATA, {});\n const first = JSON.stringify(captured());\n\n reset();\n create().track(target, STABLE_DATA, {});\n const second = JSON.stringify(captured());\n\n expect(second).toBe(first);\n });\n\n it('forwards the deduplication id only when it was given one', () => {\n reset();\n create().track(target, STABLE_DATA, {});\n const without = JSON.stringify(captured());\n\n reset();\n create().track(target, STABLE_DATA, { dedupId: 'DEDUP-XYZ' });\n const with_ = JSON.stringify(captured());\n\n // Either the vendor has the concept, and the value appears where it did\n // not before, or it has none and nothing changes. Inventing one to fill\n // the slot is the failure this catches.\n if (with_ !== without) expect(with_).toContain('DEDUP-XYZ');\n expect(without).not.toContain('DEDUP-XYZ');\n });\n\n it('never expands a consent signal it was not given', () => {\n const provider = create();\n if (!provider.consent) return;\n\n reset();\n provider.consent('update', { analytics_storage: 'denied' });\n\n // One denial in must never become a grant out. Collapsing Google's\n // vocabulary for a coarser vendor is translation; widening it invents a\n // permission the visitor never gave.\n expect(JSON.stringify(captured())).not.toContain('granted');\n });\n}\n\n/**\n * Runs the shared conformance suite against a server provider.\n *\n * @param create - Builds the provider under test, per assertion.\n * @param captured - Everything the provider sent since the last reset.\n * @param reset - Clears what `captured` returns, and restores a successful\n * transport.\n * @param fail - Makes the next send fail at the transport, so the suite can\n * check that the provider is loud about it.\n * @param context - A resolved context with a fixed timestamp.\n * @param target - An already-resolved vendor target. Defaults to `purchase`.\n *\n * @example\n * ```ts\n * describe('ga4 server conformance', () => {\n * conformsAsServerProvider({\n * create: () => ga4(credentials),\n * captured: () => fetchMock.mock.calls,\n * reset: () => { fetchMock.mockClear(); },\n * fail: () => { fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); },\n * context,\n * });\n * });\n * ```\n */\nexport function conformsAsServerProvider(options: {\n create: () => ServerProvider;\n captured: () => unknown[];\n reset: () => void;\n fail: () => void;\n context: () => ResolvedContext;\n /** Already-resolved vendor target; conversion-ID providers use their configured ID. */\n target?: string;\n}): void {\n const { create, captured, reset, fail, context } = options;\n const target = options.target ?? 'purchase';\n const noop = (): void => {};\n\n assertIdentity(create());\n\n it('reports absent mappings on ignore providers and keeps explicit exclusions silent', async () => {\n const provider = create();\n if (provider.default !== 'ignore') return;\n reset();\n const errors: { severity: string; event: string }[] = [];\n const tracking = createServerTracking({\n providers: [{ ...provider, events: { explicit: null } }],\n onError: (error) => errors.push(error),\n });\n await tracking.track('missing', {}, context());\n await tracking.track('explicit', {}, context());\n expect(errors).toMatchObject([{ severity: 'warning', event: 'missing' }]);\n expect(captured()).toEqual([]);\n });\n\n it('sends the same thing twice for the same event', async () => {\n // The context carries a fixed timestamp, so any difference between these\n // two is something the provider produced on its own.\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n const first = JSON.stringify(captured());\n\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n\n expect(JSON.stringify(captured())).toBe(first);\n });\n\n it('forwards the deduplication id only when it was given one', async () => {\n reset();\n await create().track(target, STABLE_DATA, context(), noop);\n const without = JSON.stringify(captured());\n\n reset();\n await create().track(target, STABLE_DATA, { ...context(), dedupId: 'DEDUP-XYZ' }, noop);\n const with_ = JSON.stringify(captured());\n\n if (with_ !== without) expect(with_).toContain('DEDUP-XYZ');\n expect(without).not.toContain('DEDUP-XYZ');\n });\n\n it('identifies the same way twice, and never merges unless told to', async () => {\n const provider = create();\n if (!provider.identify) return;\n\n // Same rule as for events: a generated id or a clock read makes two\n // identical calls differ. And a merge is irreversible, so one must never\n // appear without an anonymousId the host named (ADR-0014).\n const identity = {\n ...context(),\n user: { userId: 'USER-XYZ' },\n traits: { plan: 'pro' },\n };\n\n reset();\n await provider.identify(identity, noop);\n const first = JSON.stringify(captured());\n\n reset();\n await create().identify?.(identity, noop);\n expect(JSON.stringify(captured())).toBe(first);\n expect(first).not.toContain('anon_distinct_id');\n });\n\n it('throws a VendorResponseError when the vendor did not accept the send', async () => {\n // Adapters stay honest and loud. Swallowing a non-2xx is how a host ends\n // up believing it is measuring something, and the dispatcher is already\n // there to isolate the throw and report it. ADR-0013 adds the shape: what\n // reaches the host is recognised by `isVendorResponseError`, carrying the\n // status and the body, not just any throw. The body itself is\n // the caller's `fail()` hook to produce; a bodiless response is valid\n // (the documented example does exactly that), so it is not asserted here.\n reset();\n fail();\n\n let caught: unknown;\n try {\n await create().track(target, STABLE_DATA, context(), noop);\n } catch (error) {\n caught = error;\n }\n\n expect(isVendorResponseError(caught)).toBe(true);\n if (isVendorResponseError(caught)) {\n expect(typeof caught.status).toBe('number');\n expect(typeof caught.body).toBe('string');\n }\n });\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { B as VendorResponseError, C as EventData, D as UserData, E as TrackingError, S as EventBinding, T as ProviderDefault, V as isVendorResponseError, _ as ServerTracking, b as ActionSource, g as ServerProvider, h as ResolvedContext, i as TrackOptions, m as EventContext, n as BrowserTracking, r as BrowserTrackingOptions, t as BrowserProvider, v as ServerTrackingOptions, w as EventItem, x as ConsentState } from "./browser-34-CHcxK.js";
2
- export { type ActionSource, type BrowserProvider, type BrowserTracking, type BrowserTrackingOptions, type ConsentState, type EventBinding, type EventContext, type EventData, type EventItem, type ProviderDefault, type ResolvedContext, type ServerProvider, type ServerTracking, type ServerTrackingOptions, type TrackOptions, type TrackingError, type UserData, VendorResponseError, isVendorResponseError };
1
+ import { C as EventBinding, D as TrackingError, E as ProviderDefault, H as isVendorResponseError, O as UserData, S as ConsentState, T as EventItem, V as VendorResponseError, _ as ServerProvider, g as ResolvedContext, h as IdentifyContext, i as TrackOptions, m as EventContext, n as BrowserTracking, r as BrowserTrackingOptions, t as BrowserProvider, v as ServerTracking, w as EventData, x as ActionSource, y as ServerTrackingOptions } from "./browser-k4gaaQgF.js";
2
+ export { type ActionSource, type BrowserProvider, type BrowserTracking, type BrowserTrackingOptions, type ConsentState, type EventBinding, type EventContext, type EventData, type EventItem, type IdentifyContext, type ProviderDefault, type ResolvedContext, type ServerProvider, type ServerTracking, type ServerTrackingOptions, type TrackOptions, type TrackingError, type UserData, VendorResponseError, isVendorResponseError };