@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.
package/dist/datalyr.js CHANGED
@@ -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
@@ -246,15 +246,21 @@ var Datalyr = (function (exports) {
246
246
  this.prefix = 'dl_';
247
247
  // Test if storage is available
248
248
  try {
249
+ if (!storage)
250
+ throw new Error('no storage'); // server / SSR
249
251
  const testKey = 'dl_test__' + Math.random();
250
252
  storage.setItem(testKey, '1');
251
253
  storage.removeItem(testKey);
252
254
  this.storage = storage;
253
255
  }
254
256
  catch (_a) {
255
- // Storage not available (Safari private mode, etc.)
257
+ // Storage not available (server-side render, Safari private mode, etc.)
256
258
  this.storage = null;
257
- console.warn('[Datalyr] Storage not available, using memory fallback');
259
+ // Only warn in the browser on the server there's intentionally no
260
+ // storage and the SDK does no real work until init() runs client-side.
261
+ if (typeof window !== 'undefined') {
262
+ console.warn('[Datalyr] Storage not available, using memory fallback');
263
+ }
258
264
  }
259
265
  }
260
266
  get(key, defaultValue = null) {
@@ -563,15 +569,53 @@ var Datalyr = (function (exports) {
563
569
  return '';
564
570
  }
565
571
  }
566
- // Export singleton instances for storage
567
- const storage = new SafeStorage(window.localStorage);
568
- new SafeStorage(window.sessionStorage);
572
+ // Export singleton instances for storage.
573
+ // Guard the browser globals so importing the SDK on the server (SSR / Node)
574
+ // doesn't throw `window is not defined`. On the server these fall back to
575
+ // in-memory storage; real storage is wired when the module re-evaluates in the
576
+ // browser. (Without this, any static `import` of the SDK in a Next.js client
577
+ // component crashes server rendering.)
578
+ const browserLocalStorage = typeof window !== 'undefined' ? window.localStorage : undefined;
579
+ const browserSessionStorage = typeof window !== 'undefined' ? window.sessionStorage : undefined;
580
+ const storage = new SafeStorage(browserLocalStorage);
581
+ new SafeStorage(browserSessionStorage);
569
582
  // Default cookie instance for backwards compatibility
570
583
  const cookies = new CookieStorage();
571
584
 
572
585
  /**
573
586
  * Utility Functions
574
587
  */
588
+ /**
589
+ * SHA-256 hex digest aligned with the worker's CAPI hashing (cloudflare/
590
+ * postback/core/utils.js `sha256()`). Always lowercases + trims the input
591
+ * before hashing — Meta's matching requires byte-for-byte identical hashes
592
+ * on the Pixel side and the CAPI server side, and the worker uniformly
593
+ * normalizes everything it hashes (em, ph, fn, ln, external_id, etc).
594
+ *
595
+ * Pixel-side hashes that don't match CAPI hashes silently fail to dedupe,
596
+ * which is why this helper does the normalization for callers — easy to
597
+ * forget, hard to debug after release. Returns null when Web Crypto isn't
598
+ * available (very old browsers, non-secure contexts) so callers can
599
+ * gracefully skip advanced matching rather than throwing.
600
+ */
601
+ function sha256Hex(input) {
602
+ return __awaiter(this, void 0, void 0, function* () {
603
+ if (typeof crypto === 'undefined' || !crypto.subtle)
604
+ return null;
605
+ if (!input)
606
+ return null;
607
+ try {
608
+ const bytes = new TextEncoder().encode(input.toLowerCase().trim());
609
+ const digest = yield crypto.subtle.digest('SHA-256', bytes);
610
+ return Array.from(new Uint8Array(digest))
611
+ .map((b) => b.toString(16).padStart(2, '0'))
612
+ .join('');
613
+ }
614
+ catch (_a) {
615
+ return null;
616
+ }
617
+ });
618
+ }
575
619
  /**
576
620
  * Generate UUID v4
577
621
  */
@@ -2301,6 +2345,7 @@ var Datalyr = (function (exports) {
2301
2345
  // Container scripts use the same endpoint as tracking (ingest)
2302
2346
  this.endpoint = options.endpoint || 'https://ingest.datalyr.com';
2303
2347
  this.debug = options.debug || false;
2348
+ this.getIdentity = options.getIdentity;
2304
2349
  // Load session scripts from storage
2305
2350
  const sessionScripts = storage.get('dl_session_scripts', []);
2306
2351
  this.sessionLoadedScripts = new Set(sessionScripts);
@@ -2344,9 +2389,11 @@ var Datalyr = (function (exports) {
2344
2389
  // Store scripts and pixels
2345
2390
  this.scripts = data.scripts || [];
2346
2391
  this.pixels = data.pixels || null;
2347
- // Initialize pixels if configured
2392
+ // Initialize pixels if configured. Awaited so advanced-matching hashes
2393
+ // are resolved before the first dl.track() flushes through trackToPixels
2394
+ // (otherwise fbq('init') would lag fbq('track') in fast-path tracks).
2348
2395
  if (this.pixels) {
2349
- this.initializePixels();
2396
+ yield this.initializePixels();
2350
2397
  }
2351
2398
  // Load scripts based on trigger
2352
2399
  this.loadScriptsByTrigger('page_load');
@@ -2638,57 +2685,92 @@ var Datalyr = (function (exports) {
2638
2685
  * Initialize third-party pixels (Meta, Google, TikTok)
2639
2686
  */
2640
2687
  initializePixels() {
2641
- var _a, _b, _c;
2642
- if (!this.pixels)
2643
- return;
2644
- // Initialize Meta Pixel
2645
- if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2646
- this.initializeMetaPixel(this.pixels.meta);
2647
- }
2648
- // Initialize Google Tag
2649
- if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2650
- this.initializeGoogleTag(this.pixels.google);
2651
- }
2652
- // Initialize TikTok Pixel
2653
- if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2654
- this.initializeTikTokPixel(this.pixels.tiktok);
2655
- }
2688
+ return __awaiter(this, void 0, void 0, function* () {
2689
+ var _a, _b, _c;
2690
+ if (!this.pixels)
2691
+ return;
2692
+ // Initialize Meta Pixel
2693
+ if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2694
+ yield this.initializeMetaPixel(this.pixels.meta);
2695
+ }
2696
+ // Initialize Google Tag
2697
+ if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2698
+ this.initializeGoogleTag(this.pixels.google);
2699
+ }
2700
+ // Initialize TikTok Pixel
2701
+ if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2702
+ this.initializeTikTokPixel(this.pixels.tiktok);
2703
+ }
2704
+ });
2656
2705
  }
2657
2706
  /**
2658
2707
  * Initialize Meta (Facebook) Pixel
2708
+ *
2709
+ * Async because we resolve SHA-256 hashes for advanced matching (Meta's
2710
+ * `external_id` / `em`) before calling fbq('init'). Aligning the hashes the
2711
+ * browser Pixel sends with what CAPI sends (meta.js shovels user_id /
2712
+ * visitor_id / anonymous_id into external_id[], and `em` is sha256 of the
2713
+ * lowercased email) is the dedup-quality lift the CAPI side can't fix alone.
2659
2714
  */
2660
2715
  initializeMetaPixel(config) {
2661
- try {
2662
- // Load Meta Pixel script
2663
- (function (f, b, e, v, n, t, s) {
2664
- if (f.fbq)
2665
- return;
2666
- n = f.fbq = function () {
2667
- n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2668
- };
2669
- if (!f._fbq)
2670
- f._fbq = n;
2671
- n.push = n;
2672
- n.loaded = !0;
2673
- n.version = '2.0';
2674
- n.queue = [];
2675
- t = b.createElement(e);
2676
- t.async = !0;
2677
- t.src = v;
2678
- s = b.getElementsByTagName(e)[0];
2679
- s.parentNode.insertBefore(t, s);
2680
- })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2681
- // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2682
- // tracking (track('pageview')) routes through trackToPixels and fires a single
2683
- // mapped PageView with a shared eventID. Firing it again here produced TWO
2684
- // PageViews per load (one un-deduped). Note: if the host app disables pageview
2685
- // tracking entirely, no PageView is sent which is the correct outcome.
2686
- window.fbq('init', config.pixel_id);
2687
- this.log('Meta Pixel initialized:', config.pixel_id);
2688
- }
2689
- catch (error) {
2690
- this.log('Error initializing Meta Pixel:', error);
2691
- }
2716
+ return __awaiter(this, void 0, void 0, function* () {
2717
+ var _a;
2718
+ try {
2719
+ // Load Meta Pixel script
2720
+ (function (f, b, e, v, n, t, s) {
2721
+ if (f.fbq)
2722
+ return;
2723
+ n = f.fbq = function () {
2724
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2725
+ };
2726
+ if (!f._fbq)
2727
+ f._fbq = n;
2728
+ n.push = n;
2729
+ n.loaded = !0;
2730
+ n.version = '2.0';
2731
+ n.queue = [];
2732
+ t = b.createElement(e);
2733
+ t.async = !0;
2734
+ t.src = v;
2735
+ s = b.getElementsByTagName(e)[0];
2736
+ s.parentNode.insertBefore(t, s);
2737
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2738
+ // Build advanced-matching object. Skipped silently if Web Crypto isn't
2739
+ // available Pixel still initializes, just without advanced matching.
2740
+ // Anonymous_id is stable across the session and always present, so we
2741
+ // never need to re-init on identify(): CAPI carries both anonymous_id
2742
+ // AND user_id in its external_id[] array, so either side matching one
2743
+ // hash slot is enough to dedupe.
2744
+ const advancedMatching = {};
2745
+ const identity = (_a = this.getIdentity) === null || _a === void 0 ? void 0 : _a.call(this);
2746
+ if (identity === null || identity === void 0 ? void 0 : identity.externalId) {
2747
+ const hash = yield sha256Hex(String(identity.externalId));
2748
+ if (hash)
2749
+ advancedMatching.external_id = hash;
2750
+ }
2751
+ if (identity === null || identity === void 0 ? void 0 : identity.email) {
2752
+ const hash = yield sha256Hex(String(identity.email).toLowerCase().trim());
2753
+ if (hash)
2754
+ advancedMatching.em = hash;
2755
+ }
2756
+ // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2757
+ // tracking (track('pageview')) routes through trackToPixels and fires a single
2758
+ // mapped PageView with a shared eventID. Firing it again here produced TWO
2759
+ // PageViews per load (one un-deduped). Note: if the host app disables pageview
2760
+ // tracking entirely, no PageView is sent — which is the correct outcome.
2761
+ if (Object.keys(advancedMatching).length > 0) {
2762
+ window.fbq('init', config.pixel_id, advancedMatching);
2763
+ this.log('Meta Pixel initialized with advanced matching:', config.pixel_id, Object.keys(advancedMatching));
2764
+ }
2765
+ else {
2766
+ window.fbq('init', config.pixel_id);
2767
+ this.log('Meta Pixel initialized (no advanced matching):', config.pixel_id);
2768
+ }
2769
+ }
2770
+ catch (error) {
2771
+ this.log('Error initializing Meta Pixel:', error);
2772
+ }
2773
+ });
2692
2774
  }
2693
2775
  /**
2694
2776
  * Initialize Google Tag
@@ -3508,7 +3590,19 @@ var Datalyr = (function (exports) {
3508
3590
  this.container = new ContainerManager({
3509
3591
  workspaceId: this.config.workspaceId,
3510
3592
  endpoint: this.config.endpoint,
3511
- debug: this.config.debug
3593
+ debug: this.config.debug,
3594
+ // Lazy: invoked at the moment a third-party pixel inits, AFTER the
3595
+ // /container-scripts roundtrip resolves — so a pre-init identify()
3596
+ // already updated this.identity / this.userProperties. distinctId
3597
+ // mirrors what CAPI puts in external_id[] (user_id || anonymous_id);
3598
+ // email lets the Pixel match on em with the same hash CAPI sends.
3599
+ getIdentity: () => {
3600
+ var _a, _b;
3601
+ return ({
3602
+ externalId: (_a = this.identity) === null || _a === void 0 ? void 0 : _a.getDistinctId(),
3603
+ email: (_b = this.userProperties) === null || _b === void 0 ? void 0 : _b.email,
3604
+ });
3605
+ }
3512
3606
  });
3513
3607
  // Initialize container asynchronously
3514
3608
  yield this.container.init().catch(error => {
@@ -4102,7 +4196,7 @@ var Datalyr = (function (exports) {
4102
4196
  resolution_method: 'browser_sdk',
4103
4197
  resolution_confidence: 1.0,
4104
4198
  // SDK metadata (keep in sync with package.json version)
4105
- sdk_version: '1.6.0',
4199
+ sdk_version: '1.6.3',
4106
4200
  sdk_name: 'datalyr-web-sdk'
4107
4201
  };
4108
4202
  return payload;