@traffical/js-client 0.17.0 → 0.19.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.
package/README.md CHANGED
@@ -29,7 +29,7 @@ const traffical = await createTrafficalClient({
29
29
  orgId: 'org_xxx',
30
30
  projectId: 'proj_xxx',
31
31
  env: 'production',
32
- apiKey: 'pk_xxx',
32
+ apiKey: 'traffical_pk_…',
33
33
  });
34
34
 
35
35
  // Get parameters
@@ -67,7 +67,7 @@ traffical.track('checkout_complete', { value: 1 }, { decisionId: decision.decisi
67
67
  orgId: 'org_xxx',
68
68
  projectId: 'proj_xxx',
69
69
  env: 'production',
70
- apiKey: 'pk_xxx',
70
+ apiKey: 'traffical_pk_…',
71
71
  }).then(function(traffical) {
72
72
  var params = traffical.getParams({
73
73
  context: { userId: 'user_123' },
@@ -167,7 +167,7 @@ The Traffical DevTools debug plugin uses these methods to let developers force p
167
167
  ## Features
168
168
 
169
169
  - **Error Boundary** - SDK errors never crash your app
170
- - **Exposure Deduplication** - Same user/variant = 1 exposure per session
170
+ - **Exposure Deduplication** - Same user/allocation = 1 exposure event per session
171
171
  - **Smart Batching** - Events batched and flushed efficiently
172
172
  - **Beacon on Unload** - Events sent reliably on page close
173
173
  - **Auto Stable ID** - Anonymous user identification via localStorage/cookie
@@ -187,7 +187,7 @@ const traffical = await createTrafficalClient({
187
187
  orgId: 'org_xxx',
188
188
  projectId: 'proj_xxx',
189
189
  env: 'production',
190
- apiKey: 'pk_xxx',
190
+ apiKey: 'traffical_pk_…',
191
191
  plugins: [
192
192
  createDOMBindingPlugin({
193
193
  observeMutations: true, // Watch for DOM changes (SPA support)
@@ -218,7 +218,7 @@ The plugin:
218
218
 
219
219
  ### Redirect Plugin
220
220
 
221
- Run URL split tests (redirect experiments) where visitors are redirected to different landing page variants. The redirect plugin automatically triggers a decision on init, performs the redirect, and sets an attribution cookie. The attribution plugin ensures conversions on the variant page are attributed back to the experiment.
221
+ Run URL split tests (redirect policies) where visitors are redirected to different landing page allocations. The redirect plugin automatically triggers a decision on init, performs the redirect, and sets an attribution cookie. The attribution plugin ensures conversions on the destination page are attributed back to the policy.
222
222
 
223
223
  ```typescript
224
224
  import {
@@ -231,7 +231,7 @@ const traffical = await createTrafficalClient({
231
231
  orgId: 'org_xxx',
232
232
  projectId: 'proj_xxx',
233
233
  env: 'production',
234
- apiKey: 'pk_xxx',
234
+ apiKey: 'traffical_pk_…',
235
235
  plugins: [
236
236
  createRedirectPlugin(),
237
237
  createRedirectAttributionPlugin(),
@@ -242,7 +242,7 @@ const traffical = await createTrafficalClient({
242
242
  // On entry pages, it redirects. On other pages, it's a no-op.
243
243
 
244
244
  // Track goals as usual — the attribution plugin injects
245
- // redirect experiment metadata into every track() call.
245
+ // redirect policy metadata into every track() call.
246
246
  traffical.track('add_to_cart', { value: 29.99 });
247
247
  ```
248
248
 
@@ -281,9 +281,9 @@ Track goal events from a separate GTM tag (e.g., triggered on "Add to Cart" clic
281
281
 
282
282
  1. **Init** — The redirect plugin's `onInitialize` hook receives the client and calls `decide()` automatically.
283
283
 
284
- 2. **Entry page** — `onBeforeDecision` injects `url.pathname` into the context. The policy condition (e.g., `url.pathname startsWith /products/pillow`) matches, `redirect.url` resolves to the variant URL. `onDecision` writes an attribution cookie (`traffical_rdr`) and calls `window.location.replace()`.
284
+ 2. **Entry page** — `onBeforeDecision` injects `url.pathname` into the context. The policy condition (e.g., `url.pathname startsWith /products/pillow`) matches, `redirect.url` resolves to the allocation's URL. `onDecision` writes an attribution cookie (`traffical_rdr`) and calls `window.location.replace()`.
285
285
 
286
- 3. **Variant page** — The SDK loads again, `decide()` runs, but the policy condition doesn't match the new URL, so `redirect.url` stays empty and no redirect happens. The redirect-attribution plugin reads the `traffical_rdr` cookie and injects the experiment metadata into every `track()` call.
286
+ 3. **Destination page** — The SDK loads again, `decide()` runs, but the policy condition doesn't match the new URL, so `redirect.url` stays empty and no redirect happens. The redirect-attribution plugin reads the `traffical_rdr` cookie and injects the policy metadata into every `track()` call.
287
287
 
288
288
  #### Configuration
289
289
 
@@ -310,6 +310,62 @@ The redirect plugin automatically adds these context fields:
310
310
 
311
311
  Use `url.pathname` in policy conditions to target specific pages.
312
312
 
313
+ ### Auto Attributes Plugin
314
+
315
+ Opt-in plugin that derives browser, page, UTM and locale attributes and injects them into every decision context via `onBeforeDecision` (the same hook the redirect plugin uses). All keys live in the reserved `$` namespace so they never collide with your own context keys. The dashboard registers them as **system attributes** (`managedBy: system`, source `plugin:web`), so they appear in the condition editor's key picker as soon as the plugin is in use.
316
+
317
+ ```typescript
318
+ import { createTrafficalClient, autoAttributesPlugin } from '@traffical/js-client';
319
+
320
+ const traffical = await createTrafficalClient({
321
+ orgId: 'org_xxx',
322
+ projectId: 'proj_xxx',
323
+ env: 'production',
324
+ apiKey: 'traffical_pk_…',
325
+ plugins: [autoAttributesPlugin()],
326
+ });
327
+ ```
328
+
329
+ Via the CDN build: `Traffical.autoAttributesPlugin()` (`Traffical.createAutoAttributesPlugin` is an alias).
330
+
331
+ #### Keys
332
+
333
+ | Key | Type | Values / source |
334
+ |-----|------|-----------------|
335
+ | `$browser` | enum | `chrome`, `edge`, `firefox`, `safari`, `other` — UA class only, no versions |
336
+ | `$os` | enum | `ios`, `android`, `macos`, `windows`, `linux`, `other` — UA class; iPadOS desktop UAs are detected via touch points |
337
+ | `$device_type` | enum | `mobile`, `tablet`, `desktop` — UA class first; desktop-class UAs with a coarse pointer fall back to width (`< 768` mobile, `< 1024` tablet) |
338
+ | `$url` | string (url) | `location.href` |
339
+ | `$host` | string | `location.host` |
340
+ | `$path` | string | `location.pathname` |
341
+ | `$query` | string | `location.search` without the leading `?`; omitted when empty |
342
+ | `$referrer` | string | `document.referrer`; omitted when empty |
343
+ | `$page_title` | string | `document.title`; omitted when empty |
344
+ | `$utm_source`, `$utm_medium`, `$utm_campaign`, `$utm_term`, `$utm_content` | string | From the query string; persisted in `sessionStorage` (`traffical:utm`) and re-read on later pages of the session whose URL has none |
345
+ | `$locale` | string | `navigator.language` (BCP 47, e.g. `en-GB`) |
346
+ | `$timezone` | string | `Intl.DateTimeFormat().resolvedOptions().timeZone` (IANA, e.g. `Europe/Berlin`) |
347
+
348
+ Rules:
349
+
350
+ - **Caller wins.** Context you pass to `decide()` / `getParams()` overrides any derived key of the same name.
351
+ - **Nothing empty.** A key that cannot be derived is omitted, never sent as `""`.
352
+ - **Re-derived on every decision**, so SPA navigation is covered without patching `history`.
353
+ - **SSR-safe.** Without a `window` the context passes through untouched.
354
+
355
+ #### Configuration
356
+
357
+ ```typescript
358
+ autoAttributesPlugin({
359
+ include?: AutoAttributeKey[], // only derive these keys (default: all)
360
+ exclude?: AutoAttributeKey[], // never derive these keys (applied after include)
361
+ persistUtm?: boolean, // default true — sessionStorage-backed utm persistence
362
+ });
363
+ ```
364
+
365
+ `AUTO_ATTRIBUTE_KEYS` (the full key list) and the `AutoAttributeKey` type are exported for building `include` / `exclude` lists.
366
+
367
+ Mobile SDKs emit the same `$` keys from their device-info providers (`$os`, `$os_version`, `$app_version`, `$locale`, `$timezone`, `$device_model`, `$device_type`): `DefaultDeviceInfoProvider` on iOS and `defaultDeviceInfoProvider` in `@traffical/react-native`.
368
+
313
369
  ## Type-Safe Event Tracking
314
370
 
315
371
  Use `@traffical/cli generate-types` to generate TypeScript interfaces for your event schemas. This lets you create a strictly typed `track` function that catches invalid event names and properties at compile time.
package/dist/client.d.ts CHANGED
@@ -151,7 +151,7 @@ export interface TrafficalClientOptions {
151
151
  disableCloudEvents?: boolean;
152
152
  /**
153
153
  * When true, assignment logger calls are deduplicated per session
154
- * (same unit+policy+variant won't fire again). Default: true.
154
+ * (same unit+policy+allocation won't fire again). Default: true.
155
155
  */
156
156
  deduplicateAssignmentLogger?: boolean;
157
157
  /**
@@ -275,7 +275,7 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
275
275
  decide<T extends Record<string, ParameterValue>>(options: DecideOptions<T>): DecisionResult;
276
276
  /**
277
277
  * Tracks an exposure event.
278
- * Automatically deduplicates exposures for the same user/variant.
278
+ * Automatically deduplicates exposure events for the same user/allocation.
279
279
  *
280
280
  * Skips layers marked `attributionOnly` — those were resolved for
281
281
  * attribution/assignment purposes only (no parameters were requested
@@ -419,7 +419,7 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
419
419
  * - "cumulative": Collects layers from ALL cached decisions for this unit,
420
420
  * deduplicated by layerId:policyId (last-write-wins). This ensures cross-page
421
421
  * funnels (e.g., catalog -> PDP -> checkout) attribute correctly to all
422
- * experiments the user is exposed to. For per-entity dynamic allocation
422
+ * policies the user is exposed to. For per-entity dynamic allocation
423
423
  * policies, only the most recent allocation is kept to avoid attributing
424
424
  * rewards to allocations from other entities (e.g., different products).
425
425
  *
package/dist/client.js CHANGED
@@ -464,7 +464,7 @@ export class TrafficalClient {
464
464
  // ===========================================================================
465
465
  /**
466
466
  * Tracks an exposure event.
467
- * Automatically deduplicates exposures for the same user/variant.
467
+ * Automatically deduplicates exposure events for the same user/allocation.
468
468
  *
469
469
  * Skips layers marked `attributionOnly` — those were resolved for
470
470
  * attribution/assignment purposes only (no parameters were requested
@@ -1065,7 +1065,7 @@ export class TrafficalClient {
1065
1065
  * - "cumulative": Collects layers from ALL cached decisions for this unit,
1066
1066
  * deduplicated by layerId:policyId (last-write-wins). This ensures cross-page
1067
1067
  * funnels (e.g., catalog -> PDP -> checkout) attribute correctly to all
1068
- * experiments the user is exposed to. For per-entity dynamic allocation
1068
+ * policies the user is exposed to. For per-entity dynamic allocation
1069
1069
  * policies, only the most recent allocation is kept to avoid attributing
1070
1070
  * rewards to allocations from other entities (e.g., different products).
1071
1071
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ExposureDeduplicator - Prevents duplicate exposure events.
3
3
  *
4
- * Same user seeing same variant should only count as 1 exposure.
4
+ * Same user seeing the same allocation should only count as 1 exposure.
5
5
  * Uses session-based deduplication with localStorage persistence.
6
6
  */
7
7
  import type { StorageProvider } from "./storage.js";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ExposureDeduplicator - Prevents duplicate exposure events.
3
3
  *
4
- * Same user seeing same variant should only count as 1 exposure.
4
+ * Same user seeing the same allocation should only count as 1 exposure.
5
5
  * Uses session-based deduplication with localStorage persistence.
6
6
  */
7
7
  const STORAGE_KEY = "exposure_dedup";
package/dist/global.d.ts CHANGED
@@ -18,6 +18,7 @@ import { createDOMBindingPlugin, type DOMBindingPlugin, type DOMBindingPluginOpt
18
18
  import { createRedirectPlugin, type RedirectPluginOptions } from "./plugins/redirect.js";
19
19
  import { createRedirectAttributionPlugin, type RedirectAttributionPluginOptions } from "./plugins/redirect-attribution.js";
20
20
  import { createDebugPlugin, type DebugPluginOptions } from "./plugins/debug.js";
21
+ import { autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, type AutoAttributeKey, type AutoAttributesPluginOptions } from "./plugins/auto-attributes.js";
21
22
  /**
22
23
  * Initialize the Traffical client (async).
23
24
  * Returns the client instance.
@@ -37,5 +38,5 @@ declare function instance(): TrafficalClient | null;
37
38
  * Destroy the singleton instance.
38
39
  */
39
40
  declare function destroy(): void;
40
- export { init, initSync, instance, destroy, TrafficalClient, type TrafficalClientOptions, type TrafficalPlugin, createDOMBindingPlugin, type DOMBindingPlugin, type DOMBindingPluginOptions, createRedirectPlugin, type RedirectPluginOptions, createRedirectAttributionPlugin, type RedirectAttributionPluginOptions, createDebugPlugin, type DebugPluginOptions, };
41
+ export { init, initSync, instance, destroy, TrafficalClient, type TrafficalClientOptions, type TrafficalPlugin, createDOMBindingPlugin, type DOMBindingPlugin, type DOMBindingPluginOptions, createRedirectPlugin, type RedirectPluginOptions, createRedirectAttributionPlugin, type RedirectAttributionPluginOptions, createDebugPlugin, type DebugPluginOptions, autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, type AutoAttributeKey, type AutoAttributesPluginOptions, };
41
42
  //# sourceMappingURL=global.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"global.d.ts","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EAGf,KAAK,sBAAsB,EAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,EAC/B,KAAK,gCAAgC,EACtC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAK5B;;;GAGG;AACH,iBAAe,IAAI,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAQ7E;AAED;;;GAGG;AACH,iBAAS,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAclE;AAED;;;GAGG;AACH,iBAAS,QAAQ,IAAI,eAAe,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,iBAAS,OAAO,IAAI,IAAI,CAKvB;AAGD,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EAEpB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,+BAA+B,EAC/B,KAAK,gCAAgC,EAErC,iBAAiB,EACjB,KAAK,kBAAkB,GACxB,CAAC"}
1
+ {"version":3,"file":"global.d.ts","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EAGf,KAAK,sBAAsB,EAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,EAC/B,KAAK,gCAAgC,EACtC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,2BAA2B,EACjC,MAAM,8BAA8B,CAAC;AAKtC;;;GAGG;AACH,iBAAe,IAAI,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAQ7E;AAED;;;GAGG;AACH,iBAAS,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAclE;AAED;;;GAGG;AACH,iBAAS,QAAQ,IAAI,eAAe,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,iBAAS,OAAO,IAAI,IAAI,CAKvB;AAGD,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EAEpB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,+BAA+B,EAC/B,KAAK,gCAAgC,EAErC,iBAAiB,EACjB,KAAK,kBAAkB,EAEvB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,2BAA2B,GACjC,CAAC"}
package/dist/global.js CHANGED
@@ -17,6 +17,7 @@ import { createDOMBindingPlugin, } from "./plugins/dom-binding.js";
17
17
  import { createRedirectPlugin, } from "./plugins/redirect.js";
18
18
  import { createRedirectAttributionPlugin, } from "./plugins/redirect-attribution.js";
19
19
  import { createDebugPlugin, } from "./plugins/debug.js";
20
+ import { autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, } from "./plugins/auto-attributes.js";
20
21
  // Global state for singleton pattern
21
22
  let _instance = null;
22
23
  /**
@@ -70,5 +71,7 @@ createDOMBindingPlugin,
70
71
  // Redirect plugins
71
72
  createRedirectPlugin, createRedirectAttributionPlugin,
72
73
  // Debug plugin
73
- createDebugPlugin, };
74
+ createDebugPlugin,
75
+ // Auto attributes plugin
76
+ autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, };
74
77
  //# sourceMappingURL=global.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"global.js","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,GAE1B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,GAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,GAEhC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAE5B,qCAAqC;AACrC,IAAI,SAAS,GAA2B,IAAI,CAAC;AAE7C;;;GAGG;AACH,KAAK,UAAU,IAAI,CAAC,OAA+B;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,SAAS,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,OAA+B;IAC/C,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,SAAS,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAE/C,2CAA2C;IAC3C,SAAS,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QACrC,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ;IACf,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAS,OAAO;IACd,IAAI,SAAS,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qCAAqC;AACrC,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,eAAe;AAGf,qBAAqB;AACrB,sBAAsB;AAGtB,mBAAmB;AACnB,oBAAoB,EAEpB,+BAA+B;AAE/B,eAAe;AACf,iBAAiB,GAElB,CAAC"}
1
+ {"version":3,"file":"global.js","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,GAE1B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,GAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,GAEhC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,GAGpB,MAAM,8BAA8B,CAAC;AAEtC,qCAAqC;AACrC,IAAI,SAAS,GAA2B,IAAI,CAAC;AAE7C;;;GAGG;AACH,KAAK,UAAU,IAAI,CAAC,OAA+B;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,SAAS,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,OAA+B;IAC/C,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,SAAS,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAE/C,2CAA2C;IAC3C,SAAS,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QACrC,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ;IACf,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAS,OAAO;IACd,IAAI,SAAS,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qCAAqC;AACrC,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,eAAe;AAGf,qBAAqB;AACrB,sBAAsB;AAGtB,mBAAmB;AACnB,oBAAoB,EAEpB,+BAA+B;AAE/B,eAAe;AACf,iBAAiB;AAEjB,yBAAyB;AACzB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,GAGpB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Features:
7
7
  * - Error boundary wrapping (P0) - SDK errors never crash your app
8
- * - Exposure deduplication (P0) - Same user/variant = 1 exposure
8
+ * - Exposure deduplication (P0) - Same user/allocation = 1 exposure event
9
9
  * - Smart event batching (P1) - Batches events, uses sendBeacon on unload
10
10
  * - Plugin system (P2) - Extensible via plugins
11
11
  * - DOM binding plugin - Auto-apply parameters to DOM elements
@@ -46,5 +46,6 @@ export { type LifecycleProvider, type VisibilityState, type VisibilityCallback,
46
46
  export { PluginManager, type TrafficalPlugin, type PluginOptions } from "./plugins/index.js";
47
47
  export { createDOMBindingPlugin, type DOMBindingPlugin, type DOMBindingPluginOptions, } from "./plugins/dom-binding.js";
48
48
  export { createWarehouseNativeLoggerPlugin, createWarehouseNativeLogger, type WarehouseNativeLoggerOptions, type JitsuDestination, type AnalyticsLike, } from "./plugins/warehouse-native-logger.js";
49
+ export { autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, type AutoAttributeKey, type AutoAttributesPluginOptions, type AutoBrowser, type AutoOS, type AutoDeviceType, } from "./plugins/auto-attributes.js";
49
50
  export { createDebugPlugin, type DebugPluginOptions, type TrafficalDebugRegistry, type TrafficalDebugInstance, type DebugState, type DebugEvent, type RegistryEvent, } from "./plugins/debug.js";
50
51
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,cAAc,iBAAiB,CAAC;AAGhC,cAAc,oBAAoB,CAAC;AAGnC,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AAGrB;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAG7F,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,iCAAiC,EACjC,2BAA2B,EAC3B,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,cAAc,iBAAiB,CAAC;AAGhC,cAAc,oBAAoB,CAAC;AAGnC,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AAGrB;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAG7F,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,iCAAiC,EACjC,2BAA2B,EAC3B,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,2BAA2B,EAChC,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,KAAK,cAAc,GACpB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Features:
7
7
  * - Error boundary wrapping (P0) - SDK errors never crash your app
8
- * - Exposure deduplication (P0) - Same user/variant = 1 exposure
8
+ * - Exposure deduplication (P0) - Same user/allocation = 1 exposure event
9
9
  * - Smart event batching (P1) - Batches events, uses sendBeacon on unload
10
10
  * - Plugin system (P2) - Extensible via plugins
11
11
  * - DOM binding plugin - Auto-apply parameters to DOM elements
@@ -54,6 +54,8 @@ export { PluginManager } from "./plugins/index.js";
54
54
  export { createDOMBindingPlugin, } from "./plugins/dom-binding.js";
55
55
  // Export warehouse-native logger
56
56
  export { createWarehouseNativeLoggerPlugin, createWarehouseNativeLogger, } from "./plugins/warehouse-native-logger.js";
57
+ // Export auto-attributes plugin ($-prefixed web context keys)
58
+ export { autoAttributesPlugin, createAutoAttributesPlugin, AUTO_ATTRIBUTE_KEYS, } from "./plugins/auto-attributes.js";
57
59
  // Export debug plugin
58
60
  export { createDebugPlugin, } from "./plugins/debug.js";
59
61
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,iCAAiC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,6CAA6C;AAC7C,cAAc,oBAAoB,CAAC;AAEnC,gBAAgB;AAChB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,GAE1B,MAAM,aAAa,CAAC;AAErB,uCAAuC;AACvC;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAoC,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAgC,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,cAAc,CAAC;AAEtB,+BAA+B;AAC/B,OAAO,EAIL,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AAExB,uBAAuB;AACvB,OAAO,EAAE,aAAa,EAA4C,MAAM,oBAAoB,CAAC;AAE7F,4BAA4B;AAC5B,OAAO,EACL,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAElC,iCAAiC;AACjC,OAAO,EACL,iCAAiC,EACjC,2BAA2B,GAI5B,MAAM,sCAAsC,CAAC;AAE9C,sBAAsB;AACtB,OAAO,EACL,iBAAiB,GAOlB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,iCAAiC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,6CAA6C;AAC7C,cAAc,oBAAoB,CAAC;AAEnC,gBAAgB;AAChB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,yBAAyB,GAE1B,MAAM,aAAa,CAAC;AAErB,uCAAuC;AACvC;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAoC,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAgC,MAAM,gBAAgB,CAAC;AAChF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,cAAc,CAAC;AAEtB,+BAA+B;AAC/B,OAAO,EAIL,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AAExB,uBAAuB;AACvB,OAAO,EAAE,aAAa,EAA4C,MAAM,oBAAoB,CAAC;AAE7F,4BAA4B;AAC5B,OAAO,EACL,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAElC,iCAAiC;AACjC,OAAO,EACL,iCAAiC,EACjC,2BAA2B,GAI5B,MAAM,sCAAsC,CAAC;AAE9C,8DAA8D;AAC9D,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,GAMpB,MAAM,8BAA8B,CAAC;AAEtC,sBAAsB;AACtB,OAAO,EACL,iBAAiB,GAOlB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { TrafficalPlugin } from "./types.js";
2
+ /**
3
+ * Every context key the auto-attributes plugin can derive. All keys live in the
4
+ * reserved `$` namespace so they never collide with customer keys; the
5
+ * dashboard registers them as system attributes (`managedBy: system`,
6
+ * `source: plugin:web`).
7
+ */
8
+ export declare const AUTO_ATTRIBUTE_KEYS: readonly ["$browser", "$os", "$device_type", "$url", "$host", "$path", "$query", "$referrer", "$page_title", "$utm_source", "$utm_medium", "$utm_campaign", "$utm_term", "$utm_content", "$locale", "$timezone"];
9
+ export type AutoAttributeKey = (typeof AUTO_ATTRIBUTE_KEYS)[number];
10
+ export type AutoBrowser = "chrome" | "edge" | "firefox" | "safari" | "other";
11
+ export type AutoOS = "ios" | "android" | "macos" | "windows" | "linux" | "other";
12
+ export type AutoDeviceType = "mobile" | "tablet" | "desktop";
13
+ export interface AutoAttributesPluginOptions {
14
+ /** Only derive these keys. Default: every key in `AUTO_ATTRIBUTE_KEYS`. */
15
+ include?: AutoAttributeKey[];
16
+ /** Never derive these keys. Applied after `include`. */
17
+ exclude?: AutoAttributeKey[];
18
+ /**
19
+ * Persist `$utm_*` values in `sessionStorage` (key `traffical:utm`) when the
20
+ * landing URL carries them, and re-read them on later pages of the same
21
+ * session whose URL has none. Default: true.
22
+ */
23
+ persistUtm?: boolean;
24
+ }
25
+ export declare function classifyBrowser(ua: string): AutoBrowser;
26
+ export declare function classifyOS(ua: string, maxTouchPoints?: number): AutoOS;
27
+ export declare function classifyDeviceType(ua: string, opts: {
28
+ coarsePointer: boolean;
29
+ width: number;
30
+ maxTouchPoints?: number;
31
+ }): AutoDeviceType;
32
+ /**
33
+ * Auto-collected web attributes. Injects `$`-prefixed browser/page/UTM/locale
34
+ * keys into every decision context via `onBeforeDecision` (same hook as the
35
+ * redirect plugin). Caller-supplied context always wins; keys that cannot be
36
+ * derived are omitted rather than sent empty. Re-derived on every decision so
37
+ * SPA navigation is covered without patching `history`. SSR-safe: without a
38
+ * `window` the context passes through untouched.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const client = await createTrafficalClient({
43
+ * …,
44
+ * plugins: [autoAttributesPlugin({ exclude: ["$page_title"] })],
45
+ * });
46
+ * ```
47
+ */
48
+ export declare function autoAttributesPlugin(options?: AutoAttributesPluginOptions): TrafficalPlugin;
49
+ /** Alias with the `create*Plugin` naming used by the other bundled plugins. */
50
+ export declare const createAutoAttributesPlugin: typeof autoAttributesPlugin;
51
+ //# sourceMappingURL=auto-attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-attributes.d.ts","sourceRoot":"","sources":["../../src/plugins/auto-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,kNAiBtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC7E,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AACjF,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,2BAA2B;IAC1C,2EAA2E;IAC3E,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,wDAAwD;IACxD,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAiBD,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAOvD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,SAAI,GAAG,MAAM,CASjE;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,cAAc,CAWhB;AA0GD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,2BAAgC,GAAG,eAAe,CA2B/F;AAED,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,6BAAuB,CAAC"}
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Every context key the auto-attributes plugin can derive. All keys live in the
3
+ * reserved `$` namespace so they never collide with customer keys; the
4
+ * dashboard registers them as system attributes (`managedBy: system`,
5
+ * `source: plugin:web`).
6
+ */
7
+ export const AUTO_ATTRIBUTE_KEYS = [
8
+ "$browser",
9
+ "$os",
10
+ "$device_type",
11
+ "$url",
12
+ "$host",
13
+ "$path",
14
+ "$query",
15
+ "$referrer",
16
+ "$page_title",
17
+ "$utm_source",
18
+ "$utm_medium",
19
+ "$utm_campaign",
20
+ "$utm_term",
21
+ "$utm_content",
22
+ "$locale",
23
+ "$timezone",
24
+ ];
25
+ const UTM_STORAGE_KEY = "traffical:utm";
26
+ const UTM_KEYS = [
27
+ "$utm_source",
28
+ "$utm_medium",
29
+ "$utm_campaign",
30
+ "$utm_term",
31
+ "$utm_content",
32
+ ];
33
+ /* ------------------------------------------------------------------ */
34
+ /* UA classification — deliberately coarse; UA-class only, no versions */
35
+ /* ------------------------------------------------------------------ */
36
+ export function classifyBrowser(ua) {
37
+ if (/\bEdg(?:e|A|iOS)?\//.test(ua))
38
+ return "edge";
39
+ if (/\bOPR\/|\bOpera\b/.test(ua))
40
+ return "other";
41
+ if (/\bFirefox\/|\bFxiOS\//.test(ua))
42
+ return "firefox";
43
+ if (/\bChrome\/|\bCriOS\/|\bChromium\//.test(ua))
44
+ return "chrome";
45
+ if (/\bSafari\//.test(ua) && /\bVersion\/|\bMobile\//.test(ua))
46
+ return "safari";
47
+ return "other";
48
+ }
49
+ export function classifyOS(ua, maxTouchPoints = 0) {
50
+ if (/\b(?:iPhone|iPad|iPod)\b/.test(ua))
51
+ return "ios";
52
+ // iPadOS 13+ reports a desktop Macintosh UA but is the only "Mac" with touch.
53
+ if (/\bMacintosh\b/.test(ua) && maxTouchPoints > 1)
54
+ return "ios";
55
+ if (/\bAndroid\b/.test(ua))
56
+ return "android";
57
+ if (/\bWindows\b/.test(ua))
58
+ return "windows";
59
+ if (/\bMac OS X\b|\bMacintosh\b/.test(ua))
60
+ return "macos";
61
+ if (/\bCrOS\b|\bLinux\b|\bX11\b/.test(ua))
62
+ return "linux";
63
+ return "other";
64
+ }
65
+ export function classifyDeviceType(ua, opts) {
66
+ if (/\biPad\b/.test(ua))
67
+ return "tablet";
68
+ if (/\bMacintosh\b/.test(ua) && (opts.maxTouchPoints ?? 0) > 1)
69
+ return "tablet";
70
+ if (/\biPhone\b|\biPod\b/.test(ua))
71
+ return "mobile";
72
+ if (/\bAndroid\b/.test(ua))
73
+ return /\bMobile\b/.test(ua) ? "mobile" : "tablet";
74
+ if (/\bMobi\b/.test(ua))
75
+ return "mobile";
76
+ if (opts.coarsePointer) {
77
+ if (opts.width < 768)
78
+ return "mobile";
79
+ if (opts.width < 1024)
80
+ return "tablet";
81
+ }
82
+ return "desktop";
83
+ }
84
+ /* ------------------------------------------------------------------ */
85
+ function readStoredUtm(win) {
86
+ try {
87
+ const raw = win.sessionStorage?.getItem(UTM_STORAGE_KEY);
88
+ if (!raw)
89
+ return null;
90
+ const parsed = JSON.parse(raw);
91
+ if (!parsed || typeof parsed !== "object")
92
+ return null;
93
+ const out = {};
94
+ for (const k of UTM_KEYS) {
95
+ const v = parsed[k];
96
+ if (typeof v === "string" && v)
97
+ out[k] = v;
98
+ }
99
+ return Object.keys(out).length ? out : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ function writeStoredUtm(win, utm) {
106
+ try {
107
+ win.sessionStorage?.setItem(UTM_STORAGE_KEY, JSON.stringify(utm));
108
+ }
109
+ catch {
110
+ // storage may be unavailable (private mode, quota, disabled)
111
+ }
112
+ }
113
+ function utmFromSearch(search) {
114
+ const out = {};
115
+ let params;
116
+ try {
117
+ params = new URLSearchParams(search);
118
+ }
119
+ catch {
120
+ return out;
121
+ }
122
+ for (const k of UTM_KEYS) {
123
+ const v = params.get(k.slice(1)); // "$utm_source" → "utm_source"
124
+ if (v)
125
+ out[k] = v;
126
+ }
127
+ return out;
128
+ }
129
+ function deriveAll(win, persistUtm) {
130
+ const nav = win.navigator;
131
+ const loc = win.location;
132
+ const doc = win.document;
133
+ const ua = nav?.userAgent ?? "";
134
+ const maxTouchPoints = typeof nav?.maxTouchPoints === "number" ? nav.maxTouchPoints : 0;
135
+ let coarsePointer = false;
136
+ try {
137
+ coarsePointer = typeof win.matchMedia === "function" && win.matchMedia("(pointer: coarse)").matches;
138
+ }
139
+ catch {
140
+ // matchMedia may throw in exotic embeddings
141
+ }
142
+ const width = typeof win.innerWidth === "number" && win.innerWidth > 0 ? win.innerWidth : 1024;
143
+ const search = loc?.search ?? "";
144
+ let utm = utmFromSearch(search);
145
+ if (persistUtm) {
146
+ if (Object.keys(utm).length) {
147
+ writeStoredUtm(win, utm);
148
+ }
149
+ else {
150
+ utm = readStoredUtm(win) ?? {};
151
+ }
152
+ }
153
+ let locale = nav?.language || undefined;
154
+ let timezone;
155
+ try {
156
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
157
+ }
158
+ catch {
159
+ timezone = undefined;
160
+ }
161
+ if (locale === undefined) {
162
+ try {
163
+ locale = Intl.DateTimeFormat().resolvedOptions().locale || undefined;
164
+ }
165
+ catch {
166
+ locale = undefined;
167
+ }
168
+ }
169
+ const query = search.startsWith("?") ? search.slice(1) : search;
170
+ return {
171
+ $browser: classifyBrowser(ua),
172
+ $os: classifyOS(ua, maxTouchPoints),
173
+ $device_type: classifyDeviceType(ua, { coarsePointer, width, maxTouchPoints }),
174
+ $url: loc?.href || undefined,
175
+ $host: loc?.host || undefined,
176
+ $path: loc?.pathname || undefined,
177
+ $query: query || undefined,
178
+ $referrer: doc?.referrer || undefined,
179
+ $page_title: doc?.title || undefined,
180
+ $utm_source: utm.$utm_source,
181
+ $utm_medium: utm.$utm_medium,
182
+ $utm_campaign: utm.$utm_campaign,
183
+ $utm_term: utm.$utm_term,
184
+ $utm_content: utm.$utm_content,
185
+ $locale: locale,
186
+ $timezone: timezone,
187
+ };
188
+ }
189
+ /**
190
+ * Auto-collected web attributes. Injects `$`-prefixed browser/page/UTM/locale
191
+ * keys into every decision context via `onBeforeDecision` (same hook as the
192
+ * redirect plugin). Caller-supplied context always wins; keys that cannot be
193
+ * derived are omitted rather than sent empty. Re-derived on every decision so
194
+ * SPA navigation is covered without patching `history`. SSR-safe: without a
195
+ * `window` the context passes through untouched.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * const client = await createTrafficalClient({
200
+ * …,
201
+ * plugins: [autoAttributesPlugin({ exclude: ["$page_title"] })],
202
+ * });
203
+ * ```
204
+ */
205
+ export function autoAttributesPlugin(options = {}) {
206
+ const persistUtm = options.persistUtm ?? true;
207
+ const active = new Set(options.include ? options.include.filter((k) => AUTO_ATTRIBUTE_KEYS.includes(k)) : AUTO_ATTRIBUTE_KEYS);
208
+ for (const k of options.exclude ?? [])
209
+ active.delete(k);
210
+ return {
211
+ name: "auto-attributes",
212
+ onBeforeDecision(context) {
213
+ if (typeof window === "undefined")
214
+ return context;
215
+ let derived;
216
+ try {
217
+ derived = deriveAll(window, persistUtm);
218
+ }
219
+ catch {
220
+ return context;
221
+ }
222
+ const out = {};
223
+ for (const k of active) {
224
+ const v = derived[k];
225
+ if (v !== undefined)
226
+ out[k] = v;
227
+ }
228
+ return { ...out, ...context };
229
+ },
230
+ };
231
+ }
232
+ /** Alias with the `create*Plugin` naming used by the other bundled plugins. */
233
+ export const createAutoAttributesPlugin = autoAttributesPlugin;
234
+ //# sourceMappingURL=auto-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-attributes.js","sourceRoot":"","sources":["../../src/plugins/auto-attributes.ts"],"names":[],"mappings":"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"}
@@ -53,6 +53,18 @@ export interface DebugState {
53
53
  lastDecisionId: string | null;
54
54
  /** Parameter overrides currently applied by the debug plugin. */
55
55
  overrides: Record<string, unknown>;
56
+ /**
57
+ * Context attributes the debug plugin merges into every decision's context
58
+ * (on top of whatever the host app passes). Lets an inspector satisfy
59
+ * targeting conditions the app itself never sets, e.g. `testMode exists`.
60
+ */
61
+ contextOverrides: Record<string, unknown>;
62
+ /**
63
+ * The context of the last real decision, as the app passed it (after the
64
+ * client's unit-key enrichment, before context overrides). `null` until the
65
+ * app has decided at least once.
66
+ */
67
+ lastContext: Record<string, unknown> | null;
56
68
  }
57
69
  export interface TrafficalDebugInstance {
58
70
  readonly id: string;
@@ -72,6 +84,18 @@ export interface TrafficalDebugInstance {
72
84
  clearOverride(key: string): void;
73
85
  clearAllOverrides(): void;
74
86
  getOverrides(): Record<string, unknown>;
87
+ /**
88
+ * Replace the context attributes merged into every decision. Re-decides
89
+ * immediately. Pass `{}` to clear.
90
+ */
91
+ setContextOverrides(context: Record<string, unknown>): void;
92
+ getContextOverrides(): Record<string, unknown>;
93
+ /**
94
+ * Re-run the app's last decision (same context and parameter set) so the
95
+ * inspector shows what the app would resolve right now. If the page uses
96
+ * the OpenFeature provider, the OpenFeature context is re-set so hooks
97
+ * bound to `PROVIDER_CONTEXT_CHANGED` re-render too.
98
+ */
75
99
  reDecide(): void;
76
100
  refresh(): Promise<void>;
77
101
  }