@datalyr/web 1.6.2 → 1.6.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,OAAO,CAAC,UAAU,CAA2E;IAE7F,OAAO,CAAC,SAAS,CAgBf;IAEF,OAAO,CAAC,sBAAsB,CAO5B;gBAEU,OAAO,GAAE;QACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB;IAMN;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;OAEG;IACH,kBAAkB,IAAI,WAAW;IA8DjC;;;;;OAKG;IACH,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAwB/C;;;OAGG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IASnC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAQ9C;;;OAGG;IACH,YAAY,IAAI,WAAW,GAAG,IAAI;IASlC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBhE;;OAEG;IACH,UAAU,IAAI,UAAU,EAAE;IAI1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiDxB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAKlB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;OAEG;IACH,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IA0EzC;;OAEG;IACH,OAAO,CAAC,eAAe;IA6CvB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACH,oBAAoB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAKvD;;OAEG;IACH,uBAAuB,IAAI,IAAI;CAYhC"}
1
+ {"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,OAAO,CAAC,UAAU,CAA2E;IAE7F,OAAO,CAAC,SAAS,CAgBf;IAEF,OAAO,CAAC,sBAAsB,CAO5B;gBAEU,OAAO,GAAE;QACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB;IAMN;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;OAEG;IACH,kBAAkB,IAAI,WAAW;IA8DjC;;;;;OAKG;IACH,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAwB/C;;;OAGG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IASnC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAQ9C;;;OAGG;IACH,YAAY,IAAI,WAAW,GAAG,IAAI;IASlC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBhE;;OAEG;IACH,UAAU,IAAI,UAAU,EAAE;IAI1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAkExB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAKlB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;OAEG;IACH,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IA0EzC;;OAEG;IACH,OAAO,CAAC,eAAe;IA6CvB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACH,oBAAoB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAKvD;;OAEG;IACH,uBAAuB,IAAI,IAAI;CAYhC"}
@@ -2,6 +2,20 @@
2
2
  * Container Script Manager
3
3
  * Loads and manages third-party tracking scripts and pixels
4
4
  */
5
+ /**
6
+ * Identity snapshot read at the moment a third-party pixel initializes.
7
+ * Aligns the browser Pixel's advanced matching with what CAPI sends server-side
8
+ * (meta.js shovels all of {user_id, visitor_id, anonymous_id} into the CAPI
9
+ * external_id array) — so any one of those hashes coincides between surfaces.
10
+ *
11
+ * `externalId` should be the SDK's distinct_id (user_id when identified, else
12
+ * anonymous_id). `email` is only populated after identify(); when present, lets
13
+ * the Pixel match on `em` too.
14
+ */
15
+ export interface PixelIdentity {
16
+ externalId?: string | null;
17
+ email?: string | null;
18
+ }
5
19
  export interface ContainerScript {
6
20
  id: string;
7
21
  name: string;
@@ -50,10 +64,12 @@ export declare class ContainerManager {
50
64
  private sandboxedIframes;
51
65
  private iframeCleanupTimeouts;
52
66
  private messageHandler;
67
+ private getIdentity?;
53
68
  constructor(options: {
54
69
  workspaceId: string;
55
70
  endpoint?: string;
56
71
  debug?: boolean;
72
+ getIdentity?: () => PixelIdentity | undefined;
57
73
  });
58
74
  /**
59
75
  * Initialize container and load scripts
@@ -110,6 +126,12 @@ export declare class ContainerManager {
110
126
  private initializePixels;
111
127
  /**
112
128
  * Initialize Meta (Facebook) Pixel
129
+ *
130
+ * Async because we resolve SHA-256 hashes for advanced matching (Meta's
131
+ * `external_id` / `em`) before calling fbq('init'). Aligning the hashes the
132
+ * browser Pixel sends with what CAPI sends (meta.js shovels user_id /
133
+ * visitor_id / anonymous_id into external_id[], and `em` is sha256 of the
134
+ * lowercased email) is the dedup-quality lift the CAPI side can't fix alone.
113
135
  */
114
136
  private initializeMetaPixel;
115
137
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,QAAQ,CAAC;IAC9D,SAAS,EAAE,QAAQ,GAAG,eAAe,GAAG,kBAAkB,CAAC;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,GAAG,CAAC;KACZ,CAAC,CAAC;IACH,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE;QACL,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,qBAAqB,CAAwC;IACrE,OAAO,CAAC,cAAc,CAAgD;gBAE1D,OAAO,EAAE;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;IA8BD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwD3B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAU5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAkBxB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;OAEG;IACH,OAAO,CAAC,UAAU;IA8BlB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IA+DxB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAyBrB;;;OAGG;IACI,iBAAiB,IAAI,IAAI;IAkBhC;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAqC1B;;OAEG;IACH,OAAO,CAAC,SAAS;IAQjB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiC3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAyB3B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0C7B;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,GAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAqF9E;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAO3C;;OAEG;IACH,gBAAgB,IAAI,MAAM,EAAE;IAI5B;;;;;;;;;;;;;;;;;;;OAmBG;IAEH;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAyB1B;;OAEG;IACH,OAAO,CAAC,aAAa;IA0BrB;;OAEG;IACH,OAAO,CAAC,GAAG;CAKZ"}
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @datalyr/web v1.6.2
2
+ * @datalyr/web v1.6.4
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -586,6 +586,37 @@ const cookies = new CookieStorage();
586
586
  /**
587
587
  * Utility Functions
588
588
  */
589
+ /**
590
+ * SHA-256 hex digest aligned with the worker's CAPI hashing (cloudflare/
591
+ * postback/core/utils.js `sha256()`). Always lowercases + trims the input
592
+ * before hashing — Meta's matching requires byte-for-byte identical hashes
593
+ * on the Pixel side and the CAPI server side, and the worker uniformly
594
+ * normalizes everything it hashes (em, ph, fn, ln, external_id, etc).
595
+ *
596
+ * Pixel-side hashes that don't match CAPI hashes silently fail to dedupe,
597
+ * which is why this helper does the normalization for callers — easy to
598
+ * forget, hard to debug after release. Returns null when Web Crypto isn't
599
+ * available (very old browsers, non-secure contexts) so callers can
600
+ * gracefully skip advanced matching rather than throwing.
601
+ */
602
+ function sha256Hex(input) {
603
+ return __awaiter(this, void 0, void 0, function* () {
604
+ if (typeof crypto === 'undefined' || !crypto.subtle)
605
+ return null;
606
+ if (!input)
607
+ return null;
608
+ try {
609
+ const bytes = new TextEncoder().encode(input.toLowerCase().trim());
610
+ const digest = yield crypto.subtle.digest('SHA-256', bytes);
611
+ return Array.from(new Uint8Array(digest))
612
+ .map((b) => b.toString(16).padStart(2, '0'))
613
+ .join('');
614
+ }
615
+ catch (_a) {
616
+ return null;
617
+ }
618
+ });
619
+ }
589
620
  /**
590
621
  * Generate UUID v4
591
622
  */
@@ -1497,6 +1528,7 @@ class AttributionManager {
1497
1528
  * Capture advertising platform cookies
1498
1529
  */
1499
1530
  captureAdCookies() {
1531
+ var _a, _b;
1500
1532
  const adCookies = {};
1501
1533
  // Facebook/Meta cookies
1502
1534
  adCookies._fbp = cookies.get('_fbp');
@@ -1521,13 +1553,29 @@ class AttributionManager {
1521
1553
  // Optionally set the cookie for future use
1522
1554
  cookies.set('_fbp', adCookies._fbp, 90);
1523
1555
  }
1556
+ // Persist the click time the FIRST time we see fbclid/gclid in URL so we can
1557
+ // rebuild fbc (and stamp real click time on server-side events) later even
1558
+ // when _fbc/_gclid cookies get evicted. Once-only: never overwrite.
1559
+ const fbclid = this.getCurrentFbclid();
1560
+ if (fbclid && !cookies.get('_dl_fbclid_at')) {
1561
+ cookies.set('_dl_fbclid_at', String(Date.now()), 90);
1562
+ }
1563
+ const gclid = this.hasClickId('gclid') ? ((_b = (_a = this.queryParamsCache) === null || _a === void 0 ? void 0 : _a.gclid) !== null && _b !== void 0 ? _b : null) : null;
1564
+ if (gclid && !cookies.get('_dl_gclid_at')) {
1565
+ cookies.set('_dl_gclid_at', String(Date.now()), 90);
1566
+ }
1524
1567
  // Generate _fbc if we have fbclid but no _fbc.
1525
1568
  // Meta's fbc format is `fb.{subdomainIndex}.{creationTime}.{fbclid}` where
1526
1569
  // creationTime is UNIX time in MILLISECONDS (matches the _fbp generation above
1527
1570
  // and the real _fbc cookie the Meta Pixel writes). Do NOT use seconds here.
1528
- const fbclid = this.getCurrentFbclid();
1529
1571
  if (fbclid && !adCookies._fbc) {
1530
- const timestamp = Date.now();
1572
+ // Prefer the persisted click time when valid; fall back to now if the
1573
+ // cookie is missing or corrupted (e.g. user edited it to garbage).
1574
+ // Without this guard, Number("abc") = NaN and Meta would reject
1575
+ // `fb.1.NaN.{fbclid}`.
1576
+ const stored = cookies.get('_dl_fbclid_at');
1577
+ const parsed = stored ? Number(stored) : NaN;
1578
+ const timestamp = Number.isFinite(parsed) && parsed > 0 ? parsed : Date.now();
1531
1579
  adCookies._fbc = `fb.1.${timestamp}.${fbclid}`;
1532
1580
  // Optionally set the cookie for future use
1533
1581
  cookies.set('_fbc', adCookies._fbc, 90);
@@ -2315,6 +2363,7 @@ class ContainerManager {
2315
2363
  // Container scripts use the same endpoint as tracking (ingest)
2316
2364
  this.endpoint = options.endpoint || 'https://ingest.datalyr.com';
2317
2365
  this.debug = options.debug || false;
2366
+ this.getIdentity = options.getIdentity;
2318
2367
  // Load session scripts from storage
2319
2368
  const sessionScripts = storage.get('dl_session_scripts', []);
2320
2369
  this.sessionLoadedScripts = new Set(sessionScripts);
@@ -2358,9 +2407,11 @@ class ContainerManager {
2358
2407
  // Store scripts and pixels
2359
2408
  this.scripts = data.scripts || [];
2360
2409
  this.pixels = data.pixels || null;
2361
- // Initialize pixels if configured
2410
+ // Initialize pixels if configured. Awaited so advanced-matching hashes
2411
+ // are resolved before the first dl.track() flushes through trackToPixels
2412
+ // (otherwise fbq('init') would lag fbq('track') in fast-path tracks).
2362
2413
  if (this.pixels) {
2363
- this.initializePixels();
2414
+ yield this.initializePixels();
2364
2415
  }
2365
2416
  // Load scripts based on trigger
2366
2417
  this.loadScriptsByTrigger('page_load');
@@ -2652,57 +2703,92 @@ class ContainerManager {
2652
2703
  * Initialize third-party pixels (Meta, Google, TikTok)
2653
2704
  */
2654
2705
  initializePixels() {
2655
- var _a, _b, _c;
2656
- if (!this.pixels)
2657
- return;
2658
- // Initialize Meta Pixel
2659
- if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2660
- this.initializeMetaPixel(this.pixels.meta);
2661
- }
2662
- // Initialize Google Tag
2663
- if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2664
- this.initializeGoogleTag(this.pixels.google);
2665
- }
2666
- // Initialize TikTok Pixel
2667
- if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2668
- this.initializeTikTokPixel(this.pixels.tiktok);
2669
- }
2706
+ return __awaiter(this, void 0, void 0, function* () {
2707
+ var _a, _b, _c;
2708
+ if (!this.pixels)
2709
+ return;
2710
+ // Initialize Meta Pixel
2711
+ if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2712
+ yield this.initializeMetaPixel(this.pixels.meta);
2713
+ }
2714
+ // Initialize Google Tag
2715
+ if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2716
+ this.initializeGoogleTag(this.pixels.google);
2717
+ }
2718
+ // Initialize TikTok Pixel
2719
+ if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2720
+ this.initializeTikTokPixel(this.pixels.tiktok);
2721
+ }
2722
+ });
2670
2723
  }
2671
2724
  /**
2672
2725
  * Initialize Meta (Facebook) Pixel
2726
+ *
2727
+ * Async because we resolve SHA-256 hashes for advanced matching (Meta's
2728
+ * `external_id` / `em`) before calling fbq('init'). Aligning the hashes the
2729
+ * browser Pixel sends with what CAPI sends (meta.js shovels user_id /
2730
+ * visitor_id / anonymous_id into external_id[], and `em` is sha256 of the
2731
+ * lowercased email) is the dedup-quality lift the CAPI side can't fix alone.
2673
2732
  */
2674
2733
  initializeMetaPixel(config) {
2675
- try {
2676
- // Load Meta Pixel script
2677
- (function (f, b, e, v, n, t, s) {
2678
- if (f.fbq)
2679
- return;
2680
- n = f.fbq = function () {
2681
- n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2682
- };
2683
- if (!f._fbq)
2684
- f._fbq = n;
2685
- n.push = n;
2686
- n.loaded = !0;
2687
- n.version = '2.0';
2688
- n.queue = [];
2689
- t = b.createElement(e);
2690
- t.async = !0;
2691
- t.src = v;
2692
- s = b.getElementsByTagName(e)[0];
2693
- s.parentNode.insertBefore(t, s);
2694
- })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2695
- // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2696
- // tracking (track('pageview')) routes through trackToPixels and fires a single
2697
- // mapped PageView with a shared eventID. Firing it again here produced TWO
2698
- // PageViews per load (one un-deduped). Note: if the host app disables pageview
2699
- // tracking entirely, no PageView is sent which is the correct outcome.
2700
- window.fbq('init', config.pixel_id);
2701
- this.log('Meta Pixel initialized:', config.pixel_id);
2702
- }
2703
- catch (error) {
2704
- this.log('Error initializing Meta Pixel:', error);
2705
- }
2734
+ return __awaiter(this, void 0, void 0, function* () {
2735
+ var _a;
2736
+ try {
2737
+ // Load Meta Pixel script
2738
+ (function (f, b, e, v, n, t, s) {
2739
+ if (f.fbq)
2740
+ return;
2741
+ n = f.fbq = function () {
2742
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2743
+ };
2744
+ if (!f._fbq)
2745
+ f._fbq = n;
2746
+ n.push = n;
2747
+ n.loaded = !0;
2748
+ n.version = '2.0';
2749
+ n.queue = [];
2750
+ t = b.createElement(e);
2751
+ t.async = !0;
2752
+ t.src = v;
2753
+ s = b.getElementsByTagName(e)[0];
2754
+ s.parentNode.insertBefore(t, s);
2755
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2756
+ // Build advanced-matching object. Skipped silently if Web Crypto isn't
2757
+ // available Pixel still initializes, just without advanced matching.
2758
+ // Anonymous_id is stable across the session and always present, so we
2759
+ // never need to re-init on identify(): CAPI carries both anonymous_id
2760
+ // AND user_id in its external_id[] array, so either side matching one
2761
+ // hash slot is enough to dedupe.
2762
+ const advancedMatching = {};
2763
+ const identity = (_a = this.getIdentity) === null || _a === void 0 ? void 0 : _a.call(this);
2764
+ if (identity === null || identity === void 0 ? void 0 : identity.externalId) {
2765
+ const hash = yield sha256Hex(String(identity.externalId));
2766
+ if (hash)
2767
+ advancedMatching.external_id = hash;
2768
+ }
2769
+ if (identity === null || identity === void 0 ? void 0 : identity.email) {
2770
+ const hash = yield sha256Hex(String(identity.email).toLowerCase().trim());
2771
+ if (hash)
2772
+ advancedMatching.em = hash;
2773
+ }
2774
+ // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2775
+ // tracking (track('pageview')) routes through trackToPixels and fires a single
2776
+ // mapped PageView with a shared eventID. Firing it again here produced TWO
2777
+ // PageViews per load (one un-deduped). Note: if the host app disables pageview
2778
+ // tracking entirely, no PageView is sent — which is the correct outcome.
2779
+ if (Object.keys(advancedMatching).length > 0) {
2780
+ window.fbq('init', config.pixel_id, advancedMatching);
2781
+ this.log('Meta Pixel initialized with advanced matching:', config.pixel_id, Object.keys(advancedMatching));
2782
+ }
2783
+ else {
2784
+ window.fbq('init', config.pixel_id);
2785
+ this.log('Meta Pixel initialized (no advanced matching):', config.pixel_id);
2786
+ }
2787
+ }
2788
+ catch (error) {
2789
+ this.log('Error initializing Meta Pixel:', error);
2790
+ }
2791
+ });
2706
2792
  }
2707
2793
  /**
2708
2794
  * Initialize Google Tag
@@ -3446,6 +3532,14 @@ class Datalyr {
3446
3532
  }
3447
3533
  // Set default config values
3448
3534
  this.config = Object.assign({ endpoint: 'https://ingest.datalyr.com', debug: false, batchSize: 10, flushInterval: 5000, flushAt: 10, criticalEvents: undefined, highPriorityEvents: undefined, sessionTimeout: 60 * 60 * 1000, trackSessions: true, attributionWindow: 90 * 24 * 60 * 60 * 1000, trackedParams: [], respectDoNotTrack: false, respectGlobalPrivacyControl: true, privacyMode: 'standard', cookieDomain: 'auto', cookieExpires: 365, secureCookie: 'auto', sameSite: 'Lax', cookiePrefix: '__dl_', enablePerformanceTracking: true, enableFingerprinting: true, maxRetries: 5, retryDelay: 1000, maxOfflineQueueSize: 100, trackSPA: true, trackPageViews: true, fallbackEndpoints: [], plugins: [] }, config);
3535
+ // platform:'shopify' implies cart-attribute sync: stamp visitor_id + Meta
3536
+ // click signals into note_attributes so server-side order webhooks can
3537
+ // attribute guest checkouts. Lets the Shopify theme app-embed enable it via
3538
+ // data-platform="shopify" alone. `=== undefined` so an explicit
3539
+ // shopifyCartAttributes:false still wins.
3540
+ if (this.config.platform === 'shopify' && this.config.shopifyCartAttributes === undefined) {
3541
+ this.config.shopifyCartAttributes = true;
3542
+ }
3449
3543
  // Initialize cookie storage with config
3450
3544
  this.cookies = new CookieStorage({
3451
3545
  domain: this.config.cookieDomain,
@@ -3457,6 +3551,12 @@ class Datalyr {
3457
3551
  storage.migrateFromLegacyPrefix();
3458
3552
  // Check opt-out AFTER cookies configured (Issue #14)
3459
3553
  this.optedOut = this.cookies.get('__dl_opt_out') === 'true';
3554
+ // CC funnel page entry: restore the _dl_* URL bridge BEFORE IdentityManager
3555
+ // initializes so the storefront's visitor_id is the one used here (instead
3556
+ // of auto-generating a fresh one on this domain).
3557
+ if (this.config.platform === 'checkoutchamp') {
3558
+ this.restoreFromURL();
3559
+ }
3460
3560
  // Initialize modules
3461
3561
  this.identity = new IdentityManager();
3462
3562
  this.session = new SessionManager(this.config.sessionTimeout);
@@ -3522,13 +3622,35 @@ class Datalyr {
3522
3622
  this.container = new ContainerManager({
3523
3623
  workspaceId: this.config.workspaceId,
3524
3624
  endpoint: this.config.endpoint,
3525
- debug: this.config.debug
3625
+ debug: this.config.debug,
3626
+ // Lazy: invoked at the moment a third-party pixel inits, AFTER the
3627
+ // /container-scripts roundtrip resolves — so a pre-init identify()
3628
+ // already updated this.identity / this.userProperties. distinctId
3629
+ // mirrors what CAPI puts in external_id[] (user_id || anonymous_id);
3630
+ // email lets the Pixel match on em with the same hash CAPI sends.
3631
+ getIdentity: () => {
3632
+ var _a, _b;
3633
+ return ({
3634
+ externalId: (_a = this.identity) === null || _a === void 0 ? void 0 : _a.getDistinctId(),
3635
+ email: (_b = this.userProperties) === null || _b === void 0 ? void 0 : _b.email,
3636
+ });
3637
+ }
3526
3638
  });
3527
3639
  // Initialize container asynchronously
3528
3640
  yield this.container.init().catch(error => {
3529
3641
  this.log('Container initialization failed:', error);
3530
3642
  });
3531
3643
  }
3644
+ // autoIdentify default for the CC bridge:
3645
+ // - platform === 'checkoutchamp' (CC funnel pages) OR
3646
+ // - checkoutChampDomains set (Shopify storefronts feeding a CC funnel)
3647
+ // turn autoIdentify on unless the caller explicitly set it to false.
3648
+ // The 95% claim relies on email capture on BOTH ends of the bridge.
3649
+ const ccBridgeActive = this.config.platform === 'checkoutchamp'
3650
+ || (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0);
3651
+ if (ccBridgeActive && this.config.autoIdentify === undefined) {
3652
+ this.config.autoIdentify = true;
3653
+ }
3532
3654
  // Initialize auto-identify if explicitly enabled (opt-in)
3533
3655
  if (this.config.autoIdentify === true) {
3534
3656
  this.autoIdentify = new AutoIdentifyManager({
@@ -3561,6 +3683,12 @@ class Datalyr {
3561
3683
  this.log('Shopify cart attribute sync failed:', error);
3562
3684
  });
3563
3685
  }
3686
+ // Storefront → CC bridge: stamp _dl_* params on outbound links to the
3687
+ // configured CC domains so visitor_id + click signals cross the domain
3688
+ // boundary. Inert unless checkoutChampDomains is set.
3689
+ if (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0) {
3690
+ this.syncOutboundLinkParams(this.config.checkoutChampDomains);
3691
+ }
3564
3692
  // Track initial page view if enabled (AFTER encryption ready)
3565
3693
  if (this.config.trackPageViews) {
3566
3694
  this.page();
@@ -4031,6 +4159,7 @@ class Datalyr {
4031
4159
  const visitorId = this.identity.getAnonymousId();
4032
4160
  const fbc = this.cookies.get("_fbc") || attribution._fbc;
4033
4161
  const fbp = this.cookies.get("_fbp") || attribution._fbp;
4162
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4034
4163
  if (visitorId)
4035
4164
  attributes._datalyr_visitor_id = visitorId;
4036
4165
  if (fbc)
@@ -4039,6 +4168,8 @@ class Datalyr {
4039
4168
  attributes._datalyr_fbp = String(fbp);
4040
4169
  if (fbclid)
4041
4170
  attributes._datalyr_fbclid = String(fbclid);
4171
+ if (fbclidAt)
4172
+ attributes._datalyr_fbclid_at = String(fbclidAt);
4042
4173
  if (Object.keys(attributes).length === 0)
4043
4174
  return;
4044
4175
  try {
@@ -4056,6 +4187,218 @@ class Datalyr {
4056
4187
  }
4057
4188
  });
4058
4189
  }
4190
+ /**
4191
+ * Restore _dl_* URL bridge params on a Checkout Champ funnel page.
4192
+ * Runs BEFORE IdentityManager so the storefront's visitor_id wins over a
4193
+ * freshly auto-generated one. Also restores _fbc / _fbp cookies and the
4194
+ * fbclid click time so server-side rebuilds carry the real click moment.
4195
+ *
4196
+ * Strategy: stamp matching cookies (without clobbering pre-existing values),
4197
+ * then rewrite the URL so `_dl_fbclid` becomes `fbclid` — that way the rest
4198
+ * of the SDK's attribution layer (which reads `params.fbclid`) works
4199
+ * unchanged, and any merchant analytics also see the canonical click ID.
4200
+ */
4201
+ restoreFromURL() {
4202
+ var _a;
4203
+ if (typeof window === "undefined" || typeof document === "undefined")
4204
+ return;
4205
+ try {
4206
+ const params = new URLSearchParams(window.location.search);
4207
+ const get = (k) => params.get(k);
4208
+ const vid = get("_dl_vid");
4209
+ const fbc = get("_dl_fbc");
4210
+ const fbp = get("_dl_fbp");
4211
+ const fbclid = get("_dl_fbclid");
4212
+ const fbclidAt = get("_dl_fbclid_at");
4213
+ const gclid = get("_dl_gclid");
4214
+ const gclidAt = get("_dl_gclid_at");
4215
+ // visitor_id: bridge wins. The whole point of `_dl_vid` is to unify the
4216
+ // storefront's session with the CC funnel session. A pre-existing local
4217
+ // cookie from a prior direct CC visit would silently sink the integration
4218
+ // (CC events stay on the local id; storefront events use the bridged id;
4219
+ // the two never link). Overwrite — orphaned local events are fine.
4220
+ if (vid)
4221
+ this.cookies.set("__dl_visitor_id", vid, 365);
4222
+ // Meta cookies + click-time cookies: existing wins. _fbc / _fbp may have
4223
+ // been written by Meta Pixel on the CC funnel page itself (more recent
4224
+ // than the bridged value); _dl_fbclid_at should record first-touch click
4225
+ // time per device, not get reset by a bridge from a new campaign.
4226
+ const setIfMissing = (name, value) => {
4227
+ if (!value)
4228
+ return;
4229
+ if (this.cookies.get(name))
4230
+ return;
4231
+ this.cookies.set(name, value, 365);
4232
+ };
4233
+ setIfMissing("_fbc", fbc);
4234
+ setIfMissing("_fbp", fbp);
4235
+ setIfMissing("_dl_fbclid_at", fbclidAt);
4236
+ setIfMissing("_dl_gclid_at", gclidAt);
4237
+ // Rewrite URL: _dl_fbclid → fbclid (etc.) so captureAttribution() picks
4238
+ // them up via its existing `params.fbclid` path. Strip the _dl_* params
4239
+ // either way so they don't leak into downstream analytics URLs.
4240
+ let rewrote = false;
4241
+ const mappings = [
4242
+ ["fbclid", fbclid],
4243
+ ["gclid", gclid]
4244
+ ];
4245
+ for (const [canonical, value] of mappings) {
4246
+ if (value && !params.get(canonical)) {
4247
+ params.set(canonical, value);
4248
+ rewrote = true;
4249
+ }
4250
+ }
4251
+ for (const k of ["_dl_vid", "_dl_fbc", "_dl_fbp", "_dl_fbclid", "_dl_fbclid_at", "_dl_gclid", "_dl_gclid_at"]) {
4252
+ if (params.has(k)) {
4253
+ params.delete(k);
4254
+ rewrote = true;
4255
+ }
4256
+ }
4257
+ if (rewrote && typeof ((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState) === "function") {
4258
+ const newSearch = params.toString();
4259
+ const newUrl = window.location.pathname +
4260
+ (newSearch ? "?" + newSearch : "") +
4261
+ window.location.hash;
4262
+ window.history.replaceState(window.history.state, "", newUrl);
4263
+ }
4264
+ this.log("Checkout Champ bridge restored:", {
4265
+ had_vid: !!vid,
4266
+ had_fbc: !!fbc,
4267
+ had_fbp: !!fbp,
4268
+ had_fbclid: !!fbclid,
4269
+ had_gclid: !!gclid
4270
+ });
4271
+ }
4272
+ catch (error) {
4273
+ // Don't let bridge restoration block init — fall through to normal SDK
4274
+ // behavior (a fresh visitor_id, no restored click signals).
4275
+ this.log("restoreFromURL failed:", error);
4276
+ }
4277
+ }
4278
+ /**
4279
+ * Storefront → Checkout Champ link stamping. Finds every `<a href>` whose host
4280
+ * matches the configured CC domain list and appends `?_dl_vid=…&_dl_fbc=…&
4281
+ * _dl_fbp=…&_dl_fbclid=…&_dl_fbclid_at=…&_dl_gclid=…` so the user's
4282
+ * visitor_id + Meta click signals cross the domain. MutationObserver watches
4283
+ * for dynamically-injected links. On click, force-flush the event queue so
4284
+ * any pending track() events land before the browser navigates away.
4285
+ */
4286
+ syncOutboundLinkParams(domains) {
4287
+ if (typeof window === "undefined" || typeof document === "undefined")
4288
+ return;
4289
+ const lowerDomains = domains.map((d) => d.toLowerCase());
4290
+ const matchesCcDomain = (href) => {
4291
+ try {
4292
+ const host = new URL(href, window.location.href).hostname.toLowerCase();
4293
+ return lowerDomains.some((d) => host === d || host.endsWith("." + d));
4294
+ }
4295
+ catch (_a) {
4296
+ return false;
4297
+ }
4298
+ };
4299
+ const buildBridgeParams = () => {
4300
+ const attribution = this.attribution.getAttributionData();
4301
+ const out = {};
4302
+ const vid = this.identity.getAnonymousId();
4303
+ const fbc = this.cookies.get("_fbc") || attribution._fbc;
4304
+ const fbp = this.cookies.get("_fbp") || attribution._fbp;
4305
+ const fbclid = attribution.clickIdType === "fbclid" ? attribution.clickId : null;
4306
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4307
+ const gclid = attribution.clickIdType === "gclid" ? attribution.clickId : null;
4308
+ const gclidAt = this.cookies.get("_dl_gclid_at");
4309
+ if (vid)
4310
+ out._dl_vid = vid;
4311
+ if (fbc)
4312
+ out._dl_fbc = String(fbc);
4313
+ if (fbp)
4314
+ out._dl_fbp = String(fbp);
4315
+ if (fbclid)
4316
+ out._dl_fbclid = String(fbclid);
4317
+ if (fbclidAt)
4318
+ out._dl_fbclid_at = String(fbclidAt);
4319
+ if (gclid)
4320
+ out._dl_gclid = String(gclid);
4321
+ if (gclidAt)
4322
+ out._dl_gclid_at = String(gclidAt);
4323
+ return out;
4324
+ };
4325
+ const stampLink = (anchor) => {
4326
+ if (!anchor.href || !matchesCcDomain(anchor.href))
4327
+ return;
4328
+ try {
4329
+ const u = new URL(anchor.href, window.location.href);
4330
+ const bridge = buildBridgeParams();
4331
+ let mutated = false;
4332
+ for (const [k, v] of Object.entries(bridge)) {
4333
+ if (!u.searchParams.get(k)) {
4334
+ u.searchParams.set(k, v);
4335
+ mutated = true;
4336
+ }
4337
+ }
4338
+ if (mutated)
4339
+ anchor.href = u.toString();
4340
+ }
4341
+ catch (_a) {
4342
+ // Ignore malformed URLs — don't break the page.
4343
+ }
4344
+ };
4345
+ const stampAll = () => {
4346
+ document.querySelectorAll("a[href]").forEach((el) => stampLink(el));
4347
+ };
4348
+ const onClick = (e) => {
4349
+ var _a, _b;
4350
+ const target = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest("a[href]");
4351
+ if (!target)
4352
+ return;
4353
+ const anchor = target;
4354
+ if (!matchesCcDomain(anchor.href))
4355
+ return;
4356
+ stampLink(anchor); // re-stamp in case attribution changed since DOMReady
4357
+ try {
4358
+ (_b = this.queue) === null || _b === void 0 ? void 0 : _b.flush();
4359
+ }
4360
+ catch (_c) {
4361
+ // Best-effort — never block navigation.
4362
+ }
4363
+ };
4364
+ stampAll();
4365
+ document.addEventListener("click", onClick, true);
4366
+ try {
4367
+ // Debounce re-stamping. A busy SPA (infinite scroll, animations,
4368
+ // React/Vue updates) can fire thousands of mutations per second; a naive
4369
+ // re-stamp on every notification would burn CPU pointlessly when
4370
+ // outbound link sets only change occasionally. 150ms is small enough to
4371
+ // catch links before the user can click them, large enough to coalesce
4372
+ // bursts.
4373
+ let restampTimer = null;
4374
+ const scheduleRestamp = () => {
4375
+ if (restampTimer != null)
4376
+ return;
4377
+ restampTimer = setTimeout(() => {
4378
+ restampTimer = null;
4379
+ stampAll();
4380
+ }, 150);
4381
+ };
4382
+ const observer = new MutationObserver(scheduleRestamp);
4383
+ observer.observe(document.documentElement, { childList: true, subtree: true });
4384
+ // Observer + click listener live for the session; pagehide cleans up on
4385
+ // full-page unload. SPA route changes are fine — we WANT stamping to keep
4386
+ // working across virtual navigations.
4387
+ window.addEventListener("pagehide", () => {
4388
+ try {
4389
+ observer.disconnect();
4390
+ }
4391
+ catch ( /* idempotent */_a) { /* idempotent */ }
4392
+ if (restampTimer != null)
4393
+ clearTimeout(restampTimer);
4394
+ document.removeEventListener("click", onClick, true);
4395
+ }, { once: true });
4396
+ }
4397
+ catch (error) {
4398
+ this.log("MutationObserver setup failed (CC link sync):", error);
4399
+ }
4400
+ this.log("Checkout Champ outbound-link sync active for:", lowerDomains);
4401
+ }
4059
4402
  /**
4060
4403
  * Create event payload
4061
4404
  */
@@ -4116,7 +4459,7 @@ class Datalyr {
4116
4459
  resolution_method: 'browser_sdk',
4117
4460
  resolution_confidence: 1.0,
4118
4461
  // SDK metadata (keep in sync with package.json version)
4119
- sdk_version: '1.6.0',
4462
+ sdk_version: '1.6.4',
4120
4463
  sdk_name: 'datalyr-web-sdk'
4121
4464
  };
4122
4465
  return payload;