@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,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
@@ -582,6 +582,37 @@ const cookies = new CookieStorage();
582
582
  /**
583
583
  * Utility Functions
584
584
  */
585
+ /**
586
+ * SHA-256 hex digest aligned with the worker's CAPI hashing (cloudflare/
587
+ * postback/core/utils.js `sha256()`). Always lowercases + trims the input
588
+ * before hashing — Meta's matching requires byte-for-byte identical hashes
589
+ * on the Pixel side and the CAPI server side, and the worker uniformly
590
+ * normalizes everything it hashes (em, ph, fn, ln, external_id, etc).
591
+ *
592
+ * Pixel-side hashes that don't match CAPI hashes silently fail to dedupe,
593
+ * which is why this helper does the normalization for callers — easy to
594
+ * forget, hard to debug after release. Returns null when Web Crypto isn't
595
+ * available (very old browsers, non-secure contexts) so callers can
596
+ * gracefully skip advanced matching rather than throwing.
597
+ */
598
+ function sha256Hex(input) {
599
+ return __awaiter(this, void 0, void 0, function* () {
600
+ if (typeof crypto === 'undefined' || !crypto.subtle)
601
+ return null;
602
+ if (!input)
603
+ return null;
604
+ try {
605
+ const bytes = new TextEncoder().encode(input.toLowerCase().trim());
606
+ const digest = yield crypto.subtle.digest('SHA-256', bytes);
607
+ return Array.from(new Uint8Array(digest))
608
+ .map((b) => b.toString(16).padStart(2, '0'))
609
+ .join('');
610
+ }
611
+ catch (_a) {
612
+ return null;
613
+ }
614
+ });
615
+ }
585
616
  /**
586
617
  * Generate UUID v4
587
618
  */
@@ -1493,6 +1524,7 @@ class AttributionManager {
1493
1524
  * Capture advertising platform cookies
1494
1525
  */
1495
1526
  captureAdCookies() {
1527
+ var _a, _b;
1496
1528
  const adCookies = {};
1497
1529
  // Facebook/Meta cookies
1498
1530
  adCookies._fbp = cookies.get('_fbp');
@@ -1517,13 +1549,29 @@ class AttributionManager {
1517
1549
  // Optionally set the cookie for future use
1518
1550
  cookies.set('_fbp', adCookies._fbp, 90);
1519
1551
  }
1552
+ // Persist the click time the FIRST time we see fbclid/gclid in URL so we can
1553
+ // rebuild fbc (and stamp real click time on server-side events) later even
1554
+ // when _fbc/_gclid cookies get evicted. Once-only: never overwrite.
1555
+ const fbclid = this.getCurrentFbclid();
1556
+ if (fbclid && !cookies.get('_dl_fbclid_at')) {
1557
+ cookies.set('_dl_fbclid_at', String(Date.now()), 90);
1558
+ }
1559
+ const gclid = this.hasClickId('gclid') ? ((_b = (_a = this.queryParamsCache) === null || _a === void 0 ? void 0 : _a.gclid) !== null && _b !== void 0 ? _b : null) : null;
1560
+ if (gclid && !cookies.get('_dl_gclid_at')) {
1561
+ cookies.set('_dl_gclid_at', String(Date.now()), 90);
1562
+ }
1520
1563
  // Generate _fbc if we have fbclid but no _fbc.
1521
1564
  // Meta's fbc format is `fb.{subdomainIndex}.{creationTime}.{fbclid}` where
1522
1565
  // creationTime is UNIX time in MILLISECONDS (matches the _fbp generation above
1523
1566
  // and the real _fbc cookie the Meta Pixel writes). Do NOT use seconds here.
1524
- const fbclid = this.getCurrentFbclid();
1525
1567
  if (fbclid && !adCookies._fbc) {
1526
- const timestamp = Date.now();
1568
+ // Prefer the persisted click time when valid; fall back to now if the
1569
+ // cookie is missing or corrupted (e.g. user edited it to garbage).
1570
+ // Without this guard, Number("abc") = NaN and Meta would reject
1571
+ // `fb.1.NaN.{fbclid}`.
1572
+ const stored = cookies.get('_dl_fbclid_at');
1573
+ const parsed = stored ? Number(stored) : NaN;
1574
+ const timestamp = Number.isFinite(parsed) && parsed > 0 ? parsed : Date.now();
1527
1575
  adCookies._fbc = `fb.1.${timestamp}.${fbclid}`;
1528
1576
  // Optionally set the cookie for future use
1529
1577
  cookies.set('_fbc', adCookies._fbc, 90);
@@ -2311,6 +2359,7 @@ class ContainerManager {
2311
2359
  // Container scripts use the same endpoint as tracking (ingest)
2312
2360
  this.endpoint = options.endpoint || 'https://ingest.datalyr.com';
2313
2361
  this.debug = options.debug || false;
2362
+ this.getIdentity = options.getIdentity;
2314
2363
  // Load session scripts from storage
2315
2364
  const sessionScripts = storage.get('dl_session_scripts', []);
2316
2365
  this.sessionLoadedScripts = new Set(sessionScripts);
@@ -2354,9 +2403,11 @@ class ContainerManager {
2354
2403
  // Store scripts and pixels
2355
2404
  this.scripts = data.scripts || [];
2356
2405
  this.pixels = data.pixels || null;
2357
- // Initialize pixels if configured
2406
+ // Initialize pixels if configured. Awaited so advanced-matching hashes
2407
+ // are resolved before the first dl.track() flushes through trackToPixels
2408
+ // (otherwise fbq('init') would lag fbq('track') in fast-path tracks).
2358
2409
  if (this.pixels) {
2359
- this.initializePixels();
2410
+ yield this.initializePixels();
2360
2411
  }
2361
2412
  // Load scripts based on trigger
2362
2413
  this.loadScriptsByTrigger('page_load');
@@ -2648,57 +2699,92 @@ class ContainerManager {
2648
2699
  * Initialize third-party pixels (Meta, Google, TikTok)
2649
2700
  */
2650
2701
  initializePixels() {
2651
- var _a, _b, _c;
2652
- if (!this.pixels)
2653
- return;
2654
- // Initialize Meta Pixel
2655
- if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2656
- this.initializeMetaPixel(this.pixels.meta);
2657
- }
2658
- // Initialize Google Tag
2659
- if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2660
- this.initializeGoogleTag(this.pixels.google);
2661
- }
2662
- // Initialize TikTok Pixel
2663
- if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2664
- this.initializeTikTokPixel(this.pixels.tiktok);
2665
- }
2702
+ return __awaiter(this, void 0, void 0, function* () {
2703
+ var _a, _b, _c;
2704
+ if (!this.pixels)
2705
+ return;
2706
+ // Initialize Meta Pixel
2707
+ if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2708
+ yield this.initializeMetaPixel(this.pixels.meta);
2709
+ }
2710
+ // Initialize Google Tag
2711
+ if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2712
+ this.initializeGoogleTag(this.pixels.google);
2713
+ }
2714
+ // Initialize TikTok Pixel
2715
+ if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2716
+ this.initializeTikTokPixel(this.pixels.tiktok);
2717
+ }
2718
+ });
2666
2719
  }
2667
2720
  /**
2668
2721
  * Initialize Meta (Facebook) Pixel
2722
+ *
2723
+ * Async because we resolve SHA-256 hashes for advanced matching (Meta's
2724
+ * `external_id` / `em`) before calling fbq('init'). Aligning the hashes the
2725
+ * browser Pixel sends with what CAPI sends (meta.js shovels user_id /
2726
+ * visitor_id / anonymous_id into external_id[], and `em` is sha256 of the
2727
+ * lowercased email) is the dedup-quality lift the CAPI side can't fix alone.
2669
2728
  */
2670
2729
  initializeMetaPixel(config) {
2671
- try {
2672
- // Load Meta Pixel script
2673
- (function (f, b, e, v, n, t, s) {
2674
- if (f.fbq)
2675
- return;
2676
- n = f.fbq = function () {
2677
- n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2678
- };
2679
- if (!f._fbq)
2680
- f._fbq = n;
2681
- n.push = n;
2682
- n.loaded = !0;
2683
- n.version = '2.0';
2684
- n.queue = [];
2685
- t = b.createElement(e);
2686
- t.async = !0;
2687
- t.src = v;
2688
- s = b.getElementsByTagName(e)[0];
2689
- s.parentNode.insertBefore(t, s);
2690
- })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2691
- // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2692
- // tracking (track('pageview')) routes through trackToPixels and fires a single
2693
- // mapped PageView with a shared eventID. Firing it again here produced TWO
2694
- // PageViews per load (one un-deduped). Note: if the host app disables pageview
2695
- // tracking entirely, no PageView is sent which is the correct outcome.
2696
- window.fbq('init', config.pixel_id);
2697
- this.log('Meta Pixel initialized:', config.pixel_id);
2698
- }
2699
- catch (error) {
2700
- this.log('Error initializing Meta Pixel:', error);
2701
- }
2730
+ return __awaiter(this, void 0, void 0, function* () {
2731
+ var _a;
2732
+ try {
2733
+ // Load Meta Pixel script
2734
+ (function (f, b, e, v, n, t, s) {
2735
+ if (f.fbq)
2736
+ return;
2737
+ n = f.fbq = function () {
2738
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2739
+ };
2740
+ if (!f._fbq)
2741
+ f._fbq = n;
2742
+ n.push = n;
2743
+ n.loaded = !0;
2744
+ n.version = '2.0';
2745
+ n.queue = [];
2746
+ t = b.createElement(e);
2747
+ t.async = !0;
2748
+ t.src = v;
2749
+ s = b.getElementsByTagName(e)[0];
2750
+ s.parentNode.insertBefore(t, s);
2751
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2752
+ // Build advanced-matching object. Skipped silently if Web Crypto isn't
2753
+ // available Pixel still initializes, just without advanced matching.
2754
+ // Anonymous_id is stable across the session and always present, so we
2755
+ // never need to re-init on identify(): CAPI carries both anonymous_id
2756
+ // AND user_id in its external_id[] array, so either side matching one
2757
+ // hash slot is enough to dedupe.
2758
+ const advancedMatching = {};
2759
+ const identity = (_a = this.getIdentity) === null || _a === void 0 ? void 0 : _a.call(this);
2760
+ if (identity === null || identity === void 0 ? void 0 : identity.externalId) {
2761
+ const hash = yield sha256Hex(String(identity.externalId));
2762
+ if (hash)
2763
+ advancedMatching.external_id = hash;
2764
+ }
2765
+ if (identity === null || identity === void 0 ? void 0 : identity.email) {
2766
+ const hash = yield sha256Hex(String(identity.email).toLowerCase().trim());
2767
+ if (hash)
2768
+ advancedMatching.em = hash;
2769
+ }
2770
+ // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2771
+ // tracking (track('pageview')) routes through trackToPixels and fires a single
2772
+ // mapped PageView with a shared eventID. Firing it again here produced TWO
2773
+ // PageViews per load (one un-deduped). Note: if the host app disables pageview
2774
+ // tracking entirely, no PageView is sent — which is the correct outcome.
2775
+ if (Object.keys(advancedMatching).length > 0) {
2776
+ window.fbq('init', config.pixel_id, advancedMatching);
2777
+ this.log('Meta Pixel initialized with advanced matching:', config.pixel_id, Object.keys(advancedMatching));
2778
+ }
2779
+ else {
2780
+ window.fbq('init', config.pixel_id);
2781
+ this.log('Meta Pixel initialized (no advanced matching):', config.pixel_id);
2782
+ }
2783
+ }
2784
+ catch (error) {
2785
+ this.log('Error initializing Meta Pixel:', error);
2786
+ }
2787
+ });
2702
2788
  }
2703
2789
  /**
2704
2790
  * Initialize Google Tag
@@ -3442,6 +3528,14 @@ class Datalyr {
3442
3528
  }
3443
3529
  // Set default config values
3444
3530
  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);
3531
+ // platform:'shopify' implies cart-attribute sync: stamp visitor_id + Meta
3532
+ // click signals into note_attributes so server-side order webhooks can
3533
+ // attribute guest checkouts. Lets the Shopify theme app-embed enable it via
3534
+ // data-platform="shopify" alone. `=== undefined` so an explicit
3535
+ // shopifyCartAttributes:false still wins.
3536
+ if (this.config.platform === 'shopify' && this.config.shopifyCartAttributes === undefined) {
3537
+ this.config.shopifyCartAttributes = true;
3538
+ }
3445
3539
  // Initialize cookie storage with config
3446
3540
  this.cookies = new CookieStorage({
3447
3541
  domain: this.config.cookieDomain,
@@ -3453,6 +3547,12 @@ class Datalyr {
3453
3547
  storage.migrateFromLegacyPrefix();
3454
3548
  // Check opt-out AFTER cookies configured (Issue #14)
3455
3549
  this.optedOut = this.cookies.get('__dl_opt_out') === 'true';
3550
+ // CC funnel page entry: restore the _dl_* URL bridge BEFORE IdentityManager
3551
+ // initializes so the storefront's visitor_id is the one used here (instead
3552
+ // of auto-generating a fresh one on this domain).
3553
+ if (this.config.platform === 'checkoutchamp') {
3554
+ this.restoreFromURL();
3555
+ }
3456
3556
  // Initialize modules
3457
3557
  this.identity = new IdentityManager();
3458
3558
  this.session = new SessionManager(this.config.sessionTimeout);
@@ -3518,13 +3618,35 @@ class Datalyr {
3518
3618
  this.container = new ContainerManager({
3519
3619
  workspaceId: this.config.workspaceId,
3520
3620
  endpoint: this.config.endpoint,
3521
- debug: this.config.debug
3621
+ debug: this.config.debug,
3622
+ // Lazy: invoked at the moment a third-party pixel inits, AFTER the
3623
+ // /container-scripts roundtrip resolves — so a pre-init identify()
3624
+ // already updated this.identity / this.userProperties. distinctId
3625
+ // mirrors what CAPI puts in external_id[] (user_id || anonymous_id);
3626
+ // email lets the Pixel match on em with the same hash CAPI sends.
3627
+ getIdentity: () => {
3628
+ var _a, _b;
3629
+ return ({
3630
+ externalId: (_a = this.identity) === null || _a === void 0 ? void 0 : _a.getDistinctId(),
3631
+ email: (_b = this.userProperties) === null || _b === void 0 ? void 0 : _b.email,
3632
+ });
3633
+ }
3522
3634
  });
3523
3635
  // Initialize container asynchronously
3524
3636
  yield this.container.init().catch(error => {
3525
3637
  this.log('Container initialization failed:', error);
3526
3638
  });
3527
3639
  }
3640
+ // autoIdentify default for the CC bridge:
3641
+ // - platform === 'checkoutchamp' (CC funnel pages) OR
3642
+ // - checkoutChampDomains set (Shopify storefronts feeding a CC funnel)
3643
+ // turn autoIdentify on unless the caller explicitly set it to false.
3644
+ // The 95% claim relies on email capture on BOTH ends of the bridge.
3645
+ const ccBridgeActive = this.config.platform === 'checkoutchamp'
3646
+ || (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0);
3647
+ if (ccBridgeActive && this.config.autoIdentify === undefined) {
3648
+ this.config.autoIdentify = true;
3649
+ }
3528
3650
  // Initialize auto-identify if explicitly enabled (opt-in)
3529
3651
  if (this.config.autoIdentify === true) {
3530
3652
  this.autoIdentify = new AutoIdentifyManager({
@@ -3557,6 +3679,12 @@ class Datalyr {
3557
3679
  this.log('Shopify cart attribute sync failed:', error);
3558
3680
  });
3559
3681
  }
3682
+ // Storefront → CC bridge: stamp _dl_* params on outbound links to the
3683
+ // configured CC domains so visitor_id + click signals cross the domain
3684
+ // boundary. Inert unless checkoutChampDomains is set.
3685
+ if (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0) {
3686
+ this.syncOutboundLinkParams(this.config.checkoutChampDomains);
3687
+ }
3560
3688
  // Track initial page view if enabled (AFTER encryption ready)
3561
3689
  if (this.config.trackPageViews) {
3562
3690
  this.page();
@@ -4027,6 +4155,7 @@ class Datalyr {
4027
4155
  const visitorId = this.identity.getAnonymousId();
4028
4156
  const fbc = this.cookies.get("_fbc") || attribution._fbc;
4029
4157
  const fbp = this.cookies.get("_fbp") || attribution._fbp;
4158
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4030
4159
  if (visitorId)
4031
4160
  attributes._datalyr_visitor_id = visitorId;
4032
4161
  if (fbc)
@@ -4035,6 +4164,8 @@ class Datalyr {
4035
4164
  attributes._datalyr_fbp = String(fbp);
4036
4165
  if (fbclid)
4037
4166
  attributes._datalyr_fbclid = String(fbclid);
4167
+ if (fbclidAt)
4168
+ attributes._datalyr_fbclid_at = String(fbclidAt);
4038
4169
  if (Object.keys(attributes).length === 0)
4039
4170
  return;
4040
4171
  try {
@@ -4052,6 +4183,218 @@ class Datalyr {
4052
4183
  }
4053
4184
  });
4054
4185
  }
4186
+ /**
4187
+ * Restore _dl_* URL bridge params on a Checkout Champ funnel page.
4188
+ * Runs BEFORE IdentityManager so the storefront's visitor_id wins over a
4189
+ * freshly auto-generated one. Also restores _fbc / _fbp cookies and the
4190
+ * fbclid click time so server-side rebuilds carry the real click moment.
4191
+ *
4192
+ * Strategy: stamp matching cookies (without clobbering pre-existing values),
4193
+ * then rewrite the URL so `_dl_fbclid` becomes `fbclid` — that way the rest
4194
+ * of the SDK's attribution layer (which reads `params.fbclid`) works
4195
+ * unchanged, and any merchant analytics also see the canonical click ID.
4196
+ */
4197
+ restoreFromURL() {
4198
+ var _a;
4199
+ if (typeof window === "undefined" || typeof document === "undefined")
4200
+ return;
4201
+ try {
4202
+ const params = new URLSearchParams(window.location.search);
4203
+ const get = (k) => params.get(k);
4204
+ const vid = get("_dl_vid");
4205
+ const fbc = get("_dl_fbc");
4206
+ const fbp = get("_dl_fbp");
4207
+ const fbclid = get("_dl_fbclid");
4208
+ const fbclidAt = get("_dl_fbclid_at");
4209
+ const gclid = get("_dl_gclid");
4210
+ const gclidAt = get("_dl_gclid_at");
4211
+ // visitor_id: bridge wins. The whole point of `_dl_vid` is to unify the
4212
+ // storefront's session with the CC funnel session. A pre-existing local
4213
+ // cookie from a prior direct CC visit would silently sink the integration
4214
+ // (CC events stay on the local id; storefront events use the bridged id;
4215
+ // the two never link). Overwrite — orphaned local events are fine.
4216
+ if (vid)
4217
+ this.cookies.set("__dl_visitor_id", vid, 365);
4218
+ // Meta cookies + click-time cookies: existing wins. _fbc / _fbp may have
4219
+ // been written by Meta Pixel on the CC funnel page itself (more recent
4220
+ // than the bridged value); _dl_fbclid_at should record first-touch click
4221
+ // time per device, not get reset by a bridge from a new campaign.
4222
+ const setIfMissing = (name, value) => {
4223
+ if (!value)
4224
+ return;
4225
+ if (this.cookies.get(name))
4226
+ return;
4227
+ this.cookies.set(name, value, 365);
4228
+ };
4229
+ setIfMissing("_fbc", fbc);
4230
+ setIfMissing("_fbp", fbp);
4231
+ setIfMissing("_dl_fbclid_at", fbclidAt);
4232
+ setIfMissing("_dl_gclid_at", gclidAt);
4233
+ // Rewrite URL: _dl_fbclid → fbclid (etc.) so captureAttribution() picks
4234
+ // them up via its existing `params.fbclid` path. Strip the _dl_* params
4235
+ // either way so they don't leak into downstream analytics URLs.
4236
+ let rewrote = false;
4237
+ const mappings = [
4238
+ ["fbclid", fbclid],
4239
+ ["gclid", gclid]
4240
+ ];
4241
+ for (const [canonical, value] of mappings) {
4242
+ if (value && !params.get(canonical)) {
4243
+ params.set(canonical, value);
4244
+ rewrote = true;
4245
+ }
4246
+ }
4247
+ for (const k of ["_dl_vid", "_dl_fbc", "_dl_fbp", "_dl_fbclid", "_dl_fbclid_at", "_dl_gclid", "_dl_gclid_at"]) {
4248
+ if (params.has(k)) {
4249
+ params.delete(k);
4250
+ rewrote = true;
4251
+ }
4252
+ }
4253
+ if (rewrote && typeof ((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState) === "function") {
4254
+ const newSearch = params.toString();
4255
+ const newUrl = window.location.pathname +
4256
+ (newSearch ? "?" + newSearch : "") +
4257
+ window.location.hash;
4258
+ window.history.replaceState(window.history.state, "", newUrl);
4259
+ }
4260
+ this.log("Checkout Champ bridge restored:", {
4261
+ had_vid: !!vid,
4262
+ had_fbc: !!fbc,
4263
+ had_fbp: !!fbp,
4264
+ had_fbclid: !!fbclid,
4265
+ had_gclid: !!gclid
4266
+ });
4267
+ }
4268
+ catch (error) {
4269
+ // Don't let bridge restoration block init — fall through to normal SDK
4270
+ // behavior (a fresh visitor_id, no restored click signals).
4271
+ this.log("restoreFromURL failed:", error);
4272
+ }
4273
+ }
4274
+ /**
4275
+ * Storefront → Checkout Champ link stamping. Finds every `<a href>` whose host
4276
+ * matches the configured CC domain list and appends `?_dl_vid=…&_dl_fbc=…&
4277
+ * _dl_fbp=…&_dl_fbclid=…&_dl_fbclid_at=…&_dl_gclid=…` so the user's
4278
+ * visitor_id + Meta click signals cross the domain. MutationObserver watches
4279
+ * for dynamically-injected links. On click, force-flush the event queue so
4280
+ * any pending track() events land before the browser navigates away.
4281
+ */
4282
+ syncOutboundLinkParams(domains) {
4283
+ if (typeof window === "undefined" || typeof document === "undefined")
4284
+ return;
4285
+ const lowerDomains = domains.map((d) => d.toLowerCase());
4286
+ const matchesCcDomain = (href) => {
4287
+ try {
4288
+ const host = new URL(href, window.location.href).hostname.toLowerCase();
4289
+ return lowerDomains.some((d) => host === d || host.endsWith("." + d));
4290
+ }
4291
+ catch (_a) {
4292
+ return false;
4293
+ }
4294
+ };
4295
+ const buildBridgeParams = () => {
4296
+ const attribution = this.attribution.getAttributionData();
4297
+ const out = {};
4298
+ const vid = this.identity.getAnonymousId();
4299
+ const fbc = this.cookies.get("_fbc") || attribution._fbc;
4300
+ const fbp = this.cookies.get("_fbp") || attribution._fbp;
4301
+ const fbclid = attribution.clickIdType === "fbclid" ? attribution.clickId : null;
4302
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4303
+ const gclid = attribution.clickIdType === "gclid" ? attribution.clickId : null;
4304
+ const gclidAt = this.cookies.get("_dl_gclid_at");
4305
+ if (vid)
4306
+ out._dl_vid = vid;
4307
+ if (fbc)
4308
+ out._dl_fbc = String(fbc);
4309
+ if (fbp)
4310
+ out._dl_fbp = String(fbp);
4311
+ if (fbclid)
4312
+ out._dl_fbclid = String(fbclid);
4313
+ if (fbclidAt)
4314
+ out._dl_fbclid_at = String(fbclidAt);
4315
+ if (gclid)
4316
+ out._dl_gclid = String(gclid);
4317
+ if (gclidAt)
4318
+ out._dl_gclid_at = String(gclidAt);
4319
+ return out;
4320
+ };
4321
+ const stampLink = (anchor) => {
4322
+ if (!anchor.href || !matchesCcDomain(anchor.href))
4323
+ return;
4324
+ try {
4325
+ const u = new URL(anchor.href, window.location.href);
4326
+ const bridge = buildBridgeParams();
4327
+ let mutated = false;
4328
+ for (const [k, v] of Object.entries(bridge)) {
4329
+ if (!u.searchParams.get(k)) {
4330
+ u.searchParams.set(k, v);
4331
+ mutated = true;
4332
+ }
4333
+ }
4334
+ if (mutated)
4335
+ anchor.href = u.toString();
4336
+ }
4337
+ catch (_a) {
4338
+ // Ignore malformed URLs — don't break the page.
4339
+ }
4340
+ };
4341
+ const stampAll = () => {
4342
+ document.querySelectorAll("a[href]").forEach((el) => stampLink(el));
4343
+ };
4344
+ const onClick = (e) => {
4345
+ var _a, _b;
4346
+ const target = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest("a[href]");
4347
+ if (!target)
4348
+ return;
4349
+ const anchor = target;
4350
+ if (!matchesCcDomain(anchor.href))
4351
+ return;
4352
+ stampLink(anchor); // re-stamp in case attribution changed since DOMReady
4353
+ try {
4354
+ (_b = this.queue) === null || _b === void 0 ? void 0 : _b.flush();
4355
+ }
4356
+ catch (_c) {
4357
+ // Best-effort — never block navigation.
4358
+ }
4359
+ };
4360
+ stampAll();
4361
+ document.addEventListener("click", onClick, true);
4362
+ try {
4363
+ // Debounce re-stamping. A busy SPA (infinite scroll, animations,
4364
+ // React/Vue updates) can fire thousands of mutations per second; a naive
4365
+ // re-stamp on every notification would burn CPU pointlessly when
4366
+ // outbound link sets only change occasionally. 150ms is small enough to
4367
+ // catch links before the user can click them, large enough to coalesce
4368
+ // bursts.
4369
+ let restampTimer = null;
4370
+ const scheduleRestamp = () => {
4371
+ if (restampTimer != null)
4372
+ return;
4373
+ restampTimer = setTimeout(() => {
4374
+ restampTimer = null;
4375
+ stampAll();
4376
+ }, 150);
4377
+ };
4378
+ const observer = new MutationObserver(scheduleRestamp);
4379
+ observer.observe(document.documentElement, { childList: true, subtree: true });
4380
+ // Observer + click listener live for the session; pagehide cleans up on
4381
+ // full-page unload. SPA route changes are fine — we WANT stamping to keep
4382
+ // working across virtual navigations.
4383
+ window.addEventListener("pagehide", () => {
4384
+ try {
4385
+ observer.disconnect();
4386
+ }
4387
+ catch ( /* idempotent */_a) { /* idempotent */ }
4388
+ if (restampTimer != null)
4389
+ clearTimeout(restampTimer);
4390
+ document.removeEventListener("click", onClick, true);
4391
+ }, { once: true });
4392
+ }
4393
+ catch (error) {
4394
+ this.log("MutationObserver setup failed (CC link sync):", error);
4395
+ }
4396
+ this.log("Checkout Champ outbound-link sync active for:", lowerDomains);
4397
+ }
4055
4398
  /**
4056
4399
  * Create event payload
4057
4400
  */
@@ -4112,7 +4455,7 @@ class Datalyr {
4112
4455
  resolution_method: 'browser_sdk',
4113
4456
  resolution_confidence: 1.0,
4114
4457
  // SDK metadata (keep in sync with package.json version)
4115
- sdk_version: '1.6.0',
4458
+ sdk_version: '1.6.4',
4116
4459
  sdk_name: 'datalyr-web-sdk'
4117
4460
  };
4118
4461
  return payload;