@datalyr/web 1.6.1 → 1.6.3

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.1
2
+ * @datalyr/web v1.6.3
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -243,15 +243,21 @@ class SafeStorage {
243
243
  this.prefix = 'dl_';
244
244
  // Test if storage is available
245
245
  try {
246
+ if (!storage)
247
+ throw new Error('no storage'); // server / SSR
246
248
  const testKey = 'dl_test__' + Math.random();
247
249
  storage.setItem(testKey, '1');
248
250
  storage.removeItem(testKey);
249
251
  this.storage = storage;
250
252
  }
251
253
  catch (_a) {
252
- // Storage not available (Safari private mode, etc.)
254
+ // Storage not available (server-side render, Safari private mode, etc.)
253
255
  this.storage = null;
254
- console.warn('[Datalyr] Storage not available, using memory fallback');
256
+ // Only warn in the browser on the server there's intentionally no
257
+ // storage and the SDK does no real work until init() runs client-side.
258
+ if (typeof window !== 'undefined') {
259
+ console.warn('[Datalyr] Storage not available, using memory fallback');
260
+ }
255
261
  }
256
262
  }
257
263
  get(key, defaultValue = null) {
@@ -560,15 +566,53 @@ class CookieStorage {
560
566
  return '';
561
567
  }
562
568
  }
563
- // Export singleton instances for storage
564
- const storage = new SafeStorage(window.localStorage);
565
- new SafeStorage(window.sessionStorage);
569
+ // Export singleton instances for storage.
570
+ // Guard the browser globals so importing the SDK on the server (SSR / Node)
571
+ // doesn't throw `window is not defined`. On the server these fall back to
572
+ // in-memory storage; real storage is wired when the module re-evaluates in the
573
+ // browser. (Without this, any static `import` of the SDK in a Next.js client
574
+ // component crashes server rendering.)
575
+ const browserLocalStorage = typeof window !== 'undefined' ? window.localStorage : undefined;
576
+ const browserSessionStorage = typeof window !== 'undefined' ? window.sessionStorage : undefined;
577
+ const storage = new SafeStorage(browserLocalStorage);
578
+ new SafeStorage(browserSessionStorage);
566
579
  // Default cookie instance for backwards compatibility
567
580
  const cookies = new CookieStorage();
568
581
 
569
582
  /**
570
583
  * Utility Functions
571
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
+ }
572
616
  /**
573
617
  * Generate UUID v4
574
618
  */
@@ -2298,6 +2342,7 @@ class ContainerManager {
2298
2342
  // Container scripts use the same endpoint as tracking (ingest)
2299
2343
  this.endpoint = options.endpoint || 'https://ingest.datalyr.com';
2300
2344
  this.debug = options.debug || false;
2345
+ this.getIdentity = options.getIdentity;
2301
2346
  // Load session scripts from storage
2302
2347
  const sessionScripts = storage.get('dl_session_scripts', []);
2303
2348
  this.sessionLoadedScripts = new Set(sessionScripts);
@@ -2341,9 +2386,11 @@ class ContainerManager {
2341
2386
  // Store scripts and pixels
2342
2387
  this.scripts = data.scripts || [];
2343
2388
  this.pixels = data.pixels || null;
2344
- // Initialize pixels if configured
2389
+ // Initialize pixels if configured. Awaited so advanced-matching hashes
2390
+ // are resolved before the first dl.track() flushes through trackToPixels
2391
+ // (otherwise fbq('init') would lag fbq('track') in fast-path tracks).
2345
2392
  if (this.pixels) {
2346
- this.initializePixels();
2393
+ yield this.initializePixels();
2347
2394
  }
2348
2395
  // Load scripts based on trigger
2349
2396
  this.loadScriptsByTrigger('page_load');
@@ -2635,57 +2682,92 @@ class ContainerManager {
2635
2682
  * Initialize third-party pixels (Meta, Google, TikTok)
2636
2683
  */
2637
2684
  initializePixels() {
2638
- var _a, _b, _c;
2639
- if (!this.pixels)
2640
- return;
2641
- // Initialize Meta Pixel
2642
- if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2643
- this.initializeMetaPixel(this.pixels.meta);
2644
- }
2645
- // Initialize Google Tag
2646
- if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2647
- this.initializeGoogleTag(this.pixels.google);
2648
- }
2649
- // Initialize TikTok Pixel
2650
- if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2651
- this.initializeTikTokPixel(this.pixels.tiktok);
2652
- }
2685
+ return __awaiter(this, void 0, void 0, function* () {
2686
+ var _a, _b, _c;
2687
+ if (!this.pixels)
2688
+ return;
2689
+ // Initialize Meta Pixel
2690
+ if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2691
+ yield this.initializeMetaPixel(this.pixels.meta);
2692
+ }
2693
+ // Initialize Google Tag
2694
+ if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2695
+ this.initializeGoogleTag(this.pixels.google);
2696
+ }
2697
+ // Initialize TikTok Pixel
2698
+ if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2699
+ this.initializeTikTokPixel(this.pixels.tiktok);
2700
+ }
2701
+ });
2653
2702
  }
2654
2703
  /**
2655
2704
  * Initialize Meta (Facebook) Pixel
2705
+ *
2706
+ * Async because we resolve SHA-256 hashes for advanced matching (Meta's
2707
+ * `external_id` / `em`) before calling fbq('init'). Aligning the hashes the
2708
+ * browser Pixel sends with what CAPI sends (meta.js shovels user_id /
2709
+ * visitor_id / anonymous_id into external_id[], and `em` is sha256 of the
2710
+ * lowercased email) is the dedup-quality lift the CAPI side can't fix alone.
2656
2711
  */
2657
2712
  initializeMetaPixel(config) {
2658
- try {
2659
- // Load Meta Pixel script
2660
- (function (f, b, e, v, n, t, s) {
2661
- if (f.fbq)
2662
- return;
2663
- n = f.fbq = function () {
2664
- n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2665
- };
2666
- if (!f._fbq)
2667
- f._fbq = n;
2668
- n.push = n;
2669
- n.loaded = !0;
2670
- n.version = '2.0';
2671
- n.queue = [];
2672
- t = b.createElement(e);
2673
- t.async = !0;
2674
- t.src = v;
2675
- s = b.getElementsByTagName(e)[0];
2676
- s.parentNode.insertBefore(t, s);
2677
- })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2678
- // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2679
- // tracking (track('pageview')) routes through trackToPixels and fires a single
2680
- // mapped PageView with a shared eventID. Firing it again here produced TWO
2681
- // PageViews per load (one un-deduped). Note: if the host app disables pageview
2682
- // tracking entirely, no PageView is sent which is the correct outcome.
2683
- window.fbq('init', config.pixel_id);
2684
- this.log('Meta Pixel initialized:', config.pixel_id);
2685
- }
2686
- catch (error) {
2687
- this.log('Error initializing Meta Pixel:', error);
2688
- }
2713
+ return __awaiter(this, void 0, void 0, function* () {
2714
+ var _a;
2715
+ try {
2716
+ // Load Meta Pixel script
2717
+ (function (f, b, e, v, n, t, s) {
2718
+ if (f.fbq)
2719
+ return;
2720
+ n = f.fbq = function () {
2721
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2722
+ };
2723
+ if (!f._fbq)
2724
+ f._fbq = n;
2725
+ n.push = n;
2726
+ n.loaded = !0;
2727
+ n.version = '2.0';
2728
+ n.queue = [];
2729
+ t = b.createElement(e);
2730
+ t.async = !0;
2731
+ t.src = v;
2732
+ s = b.getElementsByTagName(e)[0];
2733
+ s.parentNode.insertBefore(t, s);
2734
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2735
+ // Build advanced-matching object. Skipped silently if Web Crypto isn't
2736
+ // available Pixel still initializes, just without advanced matching.
2737
+ // Anonymous_id is stable across the session and always present, so we
2738
+ // never need to re-init on identify(): CAPI carries both anonymous_id
2739
+ // AND user_id in its external_id[] array, so either side matching one
2740
+ // hash slot is enough to dedupe.
2741
+ const advancedMatching = {};
2742
+ const identity = (_a = this.getIdentity) === null || _a === void 0 ? void 0 : _a.call(this);
2743
+ if (identity === null || identity === void 0 ? void 0 : identity.externalId) {
2744
+ const hash = yield sha256Hex(String(identity.externalId));
2745
+ if (hash)
2746
+ advancedMatching.external_id = hash;
2747
+ }
2748
+ if (identity === null || identity === void 0 ? void 0 : identity.email) {
2749
+ const hash = yield sha256Hex(String(identity.email).toLowerCase().trim());
2750
+ if (hash)
2751
+ advancedMatching.em = hash;
2752
+ }
2753
+ // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2754
+ // tracking (track('pageview')) routes through trackToPixels and fires a single
2755
+ // mapped PageView with a shared eventID. Firing it again here produced TWO
2756
+ // PageViews per load (one un-deduped). Note: if the host app disables pageview
2757
+ // tracking entirely, no PageView is sent — which is the correct outcome.
2758
+ if (Object.keys(advancedMatching).length > 0) {
2759
+ window.fbq('init', config.pixel_id, advancedMatching);
2760
+ this.log('Meta Pixel initialized with advanced matching:', config.pixel_id, Object.keys(advancedMatching));
2761
+ }
2762
+ else {
2763
+ window.fbq('init', config.pixel_id);
2764
+ this.log('Meta Pixel initialized (no advanced matching):', config.pixel_id);
2765
+ }
2766
+ }
2767
+ catch (error) {
2768
+ this.log('Error initializing Meta Pixel:', error);
2769
+ }
2770
+ });
2689
2771
  }
2690
2772
  /**
2691
2773
  * Initialize Google Tag
@@ -3505,7 +3587,19 @@ class Datalyr {
3505
3587
  this.container = new ContainerManager({
3506
3588
  workspaceId: this.config.workspaceId,
3507
3589
  endpoint: this.config.endpoint,
3508
- debug: this.config.debug
3590
+ debug: this.config.debug,
3591
+ // Lazy: invoked at the moment a third-party pixel inits, AFTER the
3592
+ // /container-scripts roundtrip resolves — so a pre-init identify()
3593
+ // already updated this.identity / this.userProperties. distinctId
3594
+ // mirrors what CAPI puts in external_id[] (user_id || anonymous_id);
3595
+ // email lets the Pixel match on em with the same hash CAPI sends.
3596
+ getIdentity: () => {
3597
+ var _a, _b;
3598
+ return ({
3599
+ externalId: (_a = this.identity) === null || _a === void 0 ? void 0 : _a.getDistinctId(),
3600
+ email: (_b = this.userProperties) === null || _b === void 0 ? void 0 : _b.email,
3601
+ });
3602
+ }
3509
3603
  });
3510
3604
  // Initialize container asynchronously
3511
3605
  yield this.container.init().catch(error => {
@@ -4099,7 +4193,7 @@ class Datalyr {
4099
4193
  resolution_method: 'browser_sdk',
4100
4194
  resolution_confidence: 1.0,
4101
4195
  // SDK metadata (keep in sync with package.json version)
4102
- sdk_version: '1.6.0',
4196
+ sdk_version: '1.6.3',
4103
4197
  sdk_name: 'datalyr-web-sdk'
4104
4198
  };
4105
4199
  return payload;