@datalyr/web 1.7.9 → 1.7.12

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.7.9
2
+ * @datalyr/web v1.7.12
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -818,7 +818,11 @@ var Datalyr = (function (exports) {
818
818
  'token', 'access_token', 'refresh_token', 'id_token', 'auth', 'authorization',
819
819
  'password', 'pass', 'pwd', 'secret', 'api_key', 'apikey', 'key',
820
820
  'session', 'session_id', 'sid', 'email', 'e', 'phone', 'tel',
821
- 'signature', 'sig', 'otp', 'reset', 'hash'
821
+ 'signature', 'sig', 'otp', 'reset', 'hash',
822
+ // Klaviyo profile IDs are deterministic customer identifiers. Capture them
823
+ // through the dedicated identity envelope, but never retain them in URLs,
824
+ // referrers, first/last-touch records, or generic event properties.
825
+ 'dl_kprofile_id'
822
826
  ]);
823
827
  // BATCH-2(e): `code` is CONDITIONAL, not in the always-redact set above. An OAuth
824
828
  // authorization code (account-takeover-grade if it leaks to ad platforms) ALWAYS travels
@@ -1763,7 +1767,8 @@ var Datalyr = (function (exports) {
1763
1767
  class AttributionManager {
1764
1768
  constructor(options = {}) {
1765
1769
  this.queryParamsCache = null;
1766
- this.UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
1770
+ this.pendingKlaviyoProfileId = null;
1771
+ this.UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id'];
1767
1772
  // Updated to match dl.js - includes ALL ad platform click IDs
1768
1773
  this.CLICK_IDS = [
1769
1774
  'fbclid', // Facebook/Meta
@@ -1808,7 +1813,11 @@ var Datalyr = (function (exports) {
1808
1813
  'source', // Generic source (non-UTM)
1809
1814
  'campaign', // Generic campaign (non-UTM)
1810
1815
  'medium', // Generic medium (non-UTM)
1811
- 'gad_source' // Google Ads source parameter
1816
+ 'gad_source', // Google Ads source parameter
1817
+ // Datalyr-owned Klaviyo parameters. These live beside merchant UTMs so
1818
+ // account-level setup never replaces customer values.
1819
+ 'dl_ksource',
1820
+ 'dl_kmessage_id'
1812
1821
  ];
1813
1822
  // TR-03: ad-cookie fields (Meta/Google Ads/TikTok/Snap) that are MARKETING-scoped. When
1814
1823
  // marketing consent is declined these are neither synthesized/written nor shipped. Google
@@ -1818,6 +1827,69 @@ var Datalyr = (function (exports) {
1818
1827
  // Merge default tracked params with user-provided ones
1819
1828
  this.trackedParams = [...this.DEFAULT_TRACKED_PARAMS, ...(options.trackedParams || [])];
1820
1829
  this.marketingAllowedFn = options.marketingAllowed;
1830
+ this.replaceVisibleUrl = options.replaceVisibleUrl;
1831
+ }
1832
+ validKlaviyoProfileId(value) {
1833
+ const id = typeof value === 'string' ? value.trim() : '';
1834
+ if (!id || id.length > 512 || /[\u0000-\u001f\u007f]/.test(id))
1835
+ return null;
1836
+ return id;
1837
+ }
1838
+ /** Capture the dedicated Klaviyo identity parameter before URL redaction.
1839
+ * It deliberately never enters the Attribution object or touch storage. */
1840
+ captureKlaviyoProfileBinding(params) {
1841
+ if (!Object.prototype.hasOwnProperty.call(params, 'dl_kprofile_id'))
1842
+ return;
1843
+ const profileId = this.validKlaviyoProfileId(params.dl_kprofile_id);
1844
+ delete params.dl_kprofile_id;
1845
+ if (profileId && this.isMarketingAllowed())
1846
+ this.pendingKlaviyoProfileId = profileId;
1847
+ try {
1848
+ const url = new URL(window.location.href);
1849
+ if (url.searchParams.has('dl_kprofile_id')) {
1850
+ url.searchParams.delete('dl_kprofile_id');
1851
+ const clean = `${url.pathname}${url.search}${url.hash}`;
1852
+ if (this.replaceVisibleUrl)
1853
+ this.replaceVisibleUrl(clean);
1854
+ else
1855
+ window.history.replaceState(window.history.state, '', clean);
1856
+ }
1857
+ }
1858
+ catch (_a) {
1859
+ // Capture still succeeds when a synthetic test/browser URL cannot parse.
1860
+ }
1861
+ }
1862
+ /** Called once encryption is ready. The short TTL only bridges async init or
1863
+ * a reload before the landing event is queued; it is not a profile cache. */
1864
+ hydrateKlaviyoProfileBinding() {
1865
+ return __awaiter(this, void 0, void 0, function* () {
1866
+ const now = Date.now();
1867
+ if (!this.pendingKlaviyoProfileId) {
1868
+ const stored = yield storage.getEncrypted(AttributionManager.KLAVIYO_BINDING_KEY, null);
1869
+ const restored = this.validKlaviyoProfileId(stored === null || stored === void 0 ? void 0 : stored.profileId);
1870
+ if (restored && Number(stored === null || stored === void 0 ? void 0 : stored.expiresAt) > now && this.isMarketingAllowed()) {
1871
+ this.pendingKlaviyoProfileId = restored;
1872
+ }
1873
+ else {
1874
+ storage.remove(AttributionManager.KLAVIYO_BINDING_KEY);
1875
+ }
1876
+ }
1877
+ if (this.pendingKlaviyoProfileId) {
1878
+ yield storage.setEncrypted(AttributionManager.KLAVIYO_BINDING_KEY, {
1879
+ profileId: this.pendingKlaviyoProfileId,
1880
+ expiresAt: now + AttributionManager.KLAVIYO_BINDING_TTL_MS,
1881
+ });
1882
+ }
1883
+ });
1884
+ }
1885
+ /** One-shot binding for the next queued event. */
1886
+ consumeKlaviyoProfileBinding() {
1887
+ if (!this.isMarketingAllowed())
1888
+ this.pendingKlaviyoProfileId = null;
1889
+ const profileId = this.pendingKlaviyoProfileId;
1890
+ this.pendingKlaviyoProfileId = null;
1891
+ storage.remove(AttributionManager.KLAVIYO_BINDING_KEY);
1892
+ return profileId;
1821
1893
  }
1822
1894
  // TR-03: default (no predicate / no signal) = allowed → byte-identical to prior behavior.
1823
1895
  isMarketingAllowed() {
@@ -1840,6 +1912,7 @@ var Datalyr = (function (exports) {
1840
1912
  if (!this.queryParamsCache) {
1841
1913
  this.queryParamsCache = params;
1842
1914
  }
1915
+ this.captureKlaviyoProfileBinding(params);
1843
1916
  const attribution = {
1844
1917
  timestamp: Date.now()
1845
1918
  };
@@ -1854,8 +1927,13 @@ var Datalyr = (function (exports) {
1854
1927
  const value = params[utm];
1855
1928
  if (value) {
1856
1929
  attribution[utm] = value; // canonical: utm_source, utm_campaign, ...
1857
- const key = utm.replace('utm_', '');
1858
- attribution[key] = value; // alias: source, campaign, ...
1930
+ // The five original UTM dimensions keep their legacy stripped aliases.
1931
+ // `utm_id` is intentionally canonical-only: writing it to a generic `id`
1932
+ // property would collide with event/customer identifiers downstream.
1933
+ if (utm !== 'utm_id') {
1934
+ const key = utm.replace('utm_', '');
1935
+ attribution[key] = value; // alias: source, campaign, ...
1936
+ }
1859
1937
  }
1860
1938
  }
1861
1939
  // Capture click IDs. The first present (by CLICK_IDS priority) is the primary
@@ -1892,6 +1970,12 @@ var Datalyr = (function (exports) {
1892
1970
  attribution[param] = value;
1893
1971
  }
1894
1972
  }
1973
+ // The namespaced source is Datalyr's routing authority. Keep any
1974
+ // merchant-owned utm_source intact while still classifying this visit as
1975
+ // Klaviyo everywhere source-level attribution is displayed.
1976
+ if (attribution.dl_ksource === 'klaviyo') {
1977
+ attribution.source = 'klaviyo';
1978
+ }
1895
1979
  // Capture referrer. 9.A.4: redact secret/PII query-param values (reset tokens,
1896
1980
  // OAuth codes, emails) — this attribution object is stamped onto every event AND
1897
1981
  // persisted into dl_first_touch/dl_last_touch, so it must be clean at capture.
@@ -2155,6 +2239,21 @@ var Datalyr = (function (exports) {
2155
2239
  current = Object.assign(Object.assign({}, fallback), { referrer: current.referrer, referrerHost: current.referrerHost, landingPage: current.landingPage, landingPath: current.landingPath });
2156
2240
  }
2157
2241
  }
2242
+ // A real signal WITHOUT its own link tag must not erase the stored one. The
2243
+ // creator/bio-link journey is click-link-today, return-from-instagram-next-week:
2244
+ // that organic revisit scores hasRealAttribution (external referrer) with no
2245
+ // `lyr`, and before this carry-forward storeLastTouch below replaced the tagged
2246
+ // touch wholesale — every later event (and the Track page's conversion count,
2247
+ // which keys on the event's own lyr) silently lost the link. Semantics = "last
2248
+ // non-empty lyr within the window", the same answer the server's resolveLyr
2249
+ // argMax gives webhook conversions; a NEW ?lyr= still wins because current.lyr
2250
+ // is already set. source/medium/campaign still update — lyr is an independent
2251
+ // dimension (which link), not a channel claim.
2252
+ if (hasRealAttribution && !current.lyr) {
2253
+ const storedLyr = (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.lyr) || (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.lyr);
2254
+ if (storedLyr)
2255
+ current.lyr = storedLyr;
2256
+ }
2158
2257
  // Capture advertising cookies automatically
2159
2258
  const adCookies = this.captureAdCookies();
2160
2259
  // Only (re)write first/last touch when this pageview carried a REAL signal — never
@@ -2331,6 +2430,8 @@ var Datalyr = (function (exports) {
2331
2430
  }
2332
2431
  }
2333
2432
  }
2433
+ AttributionManager.KLAVIYO_BINDING_KEY = 'dl_klaviyo_profile_binding';
2434
+ AttributionManager.KLAVIYO_BINDING_TTL_MS = 10 * 60 * 1000;
2334
2435
 
2335
2436
  /**
2336
2437
  * Event Queue and Batching Module
@@ -4991,6 +5092,7 @@ var Datalyr = (function (exports) {
4991
5092
  this.initialized = false;
4992
5093
  this.errors = [];
4993
5094
  this.MAX_ERRORS = 50;
5095
+ this.shopifyConsentUnresolvable = false; // loadFeatures failed or never yielded customerPrivacy → stop fail-closing
4994
5096
  this.lastSpaPath = null; // dedups SPA pageviews (replaceState-on-mount double-fire)
4995
5097
  // Shopify loads Customer Privacy asynchronously. Keep the initial pageview
4996
5098
  // pending until initialization is complete and analytics consent is known,
@@ -5070,7 +5172,15 @@ var Datalyr = (function (exports) {
5070
5172
  // TR-03: gate ad-signal synthesis + shipping on LIVE marketing consent. Returns true by
5071
5173
  // default (no consent signal) so behavior is unchanged for the common case; false only on
5072
5174
  // an explicit decline (Shopify marketing:false / setConsent marketing|sale=false).
5073
- marketingAllowed: () => this.consentAllowsMarketing()
5175
+ marketingAllowed: () => this.consentAllowsMarketing(),
5176
+ // Bypass our SPA wrapper when removing the one-time profile parameter,
5177
+ // otherwise the privacy cleanup itself would manufacture a pageview.
5178
+ replaceVisibleUrl: (url) => {
5179
+ var _a;
5180
+ const replace = (_a = this.originalReplaceState) !== null && _a !== void 0 ? _a : history.replaceState;
5181
+ replace.call(history, history.state, '', url);
5182
+ this.lastSpaPath = window.location.pathname + window.location.search + window.location.hash;
5183
+ },
5074
5184
  });
5075
5185
  this.queue = new EventQueue(this.config);
5076
5186
  this.fingerprint = new FingerprintCollector({
@@ -5160,6 +5270,7 @@ var Datalyr = (function (exports) {
5160
5270
  // in the IdentityManager constructor. It never overwrites an id already
5161
5271
  // set by an explicit identify() earlier in this page load.
5162
5272
  yield this.identity.hydrateEncryptedUserId();
5273
+ yield this.attribution.hydrateKlaviyoProfileBinding();
5163
5274
  this.log('Encryption initialized, user properties loaded');
5164
5275
  }
5165
5276
  catch (encErr) {
@@ -5423,6 +5534,14 @@ var Datalyr = (function (exports) {
5423
5534
  referrerParams.set('utm_content', lastTouch.content);
5424
5535
  if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.term)
5425
5536
  referrerParams.set('utm_term', lastTouch.term);
5537
+ if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.utm_id)
5538
+ referrerParams.set('utm_id', lastTouch.utm_id);
5539
+ // The trackable-link tag rides the referrer too — the edge worker's own
5540
+ // server-side appendPlayReferrer packs it, and ingest parses it back out
5541
+ // of install_referrer_url. Without it, interstitial-mode Android installs
5542
+ // lose the deterministic link tag that a bare 302 would have carried.
5543
+ if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.lyr)
5544
+ referrerParams.set('lyr', lastTouch.lyr);
5426
5545
  try {
5427
5546
  const url = new URL(options.appStoreUrl);
5428
5547
  url.searchParams.set('referrer', referrerParams.toString());
@@ -6423,6 +6542,16 @@ var Datalyr = (function (exports) {
6423
6542
  // Add attribution data (caller wins on collisions)
6424
6543
  const attributionData = this.attribution.getAttributionData();
6425
6544
  assignMissing(eventData, attributionData);
6545
+ // A Klaviyo landing URL may carry the profile ID as a dedicated custom
6546
+ // parameter. Emit it once through the deterministic external-id envelope;
6547
+ // never copy it into generic attribution or user-visible URL fields.
6548
+ const klaviyoProfileId = this.attribution.consumeKlaviyoProfileBinding();
6549
+ if (klaviyoProfileId) {
6550
+ const existingExternalIds = eventData.external_ids;
6551
+ eventData.external_ids = Object.assign(Object.assign({}, (existingExternalIds && typeof existingExternalIds === 'object' && !Array.isArray(existingExternalIds)
6552
+ ? existingExternalIds
6553
+ : {})), { klaviyo: klaviyoProfileId });
6554
+ }
6426
6555
  // Add session metrics (SDK-authoritative — session_id et al. must not be overridden)
6427
6556
  const sessionMetrics = this.session.getMetrics();
6428
6557
  Object.assign(eventData, sessionMetrics);
@@ -6490,7 +6619,7 @@ var Datalyr = (function (exports) {
6490
6619
  // drift from the package again (it sat at 1.7.4 across the 1.7.5 release). The
6491
6620
  // build guard (scripts/check-bundle.js, run by build:check) still verifies the
6492
6621
  // deployable bundles carry the package.json version. (FSR-103)
6493
- sdk_version: '1.7.9',
6622
+ sdk_version: '1.7.12',
6494
6623
  sdk_name: 'datalyr-web-sdk',
6495
6624
  // A3-25: versioned-envelope stamp. Every SDK emits schema_version so the ingest/contract
6496
6625
  // layer can key on ONE canonical envelope version (snake_case fields, event_name/event_data
@@ -6579,6 +6708,27 @@ var Datalyr = (function (exports) {
6579
6708
  return false;
6580
6709
  }
6581
6710
  }
6711
+ /**
6712
+ * TR-15 refinement: fail closed ONLY while the Customer Privacy API can still
6713
+ * arrive (Shopify.loadFeatures exists and hasn't failed). A storefront with no
6714
+ * loadFeatures (headless installs, `platform:'shopify'` on a non-Shopify page)
6715
+ * has NO path to a consent answer — blocking there is not a pre-load window,
6716
+ * it is zero events forever, silently. No signal possible → null (fail open);
6717
+ * false is reserved for an explicit decline from the API.
6718
+ */
6719
+ shopifyConsentPendingBlock() {
6720
+ var _a;
6721
+ if (!this.isShopifyStorefront())
6722
+ return null;
6723
+ if (this.shopifyConsentUnresolvable)
6724
+ return null;
6725
+ try {
6726
+ return typeof ((_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.loadFeatures) === 'function' ? false : null;
6727
+ }
6728
+ catch (_b) {
6729
+ return null;
6730
+ }
6731
+ }
6582
6732
  /**
6583
6733
  * Shopify's analytics-consent signal: true/false when the Customer Privacy API is
6584
6734
  * present and answers, null when there's no signal (API absent / unexpected shape)
@@ -6588,7 +6738,7 @@ var Datalyr = (function (exports) {
6588
6738
  try {
6589
6739
  const cp = this.getShopifyCustomerPrivacy();
6590
6740
  if (!cp)
6591
- return this.isShopifyStorefront() ? false : null;
6741
+ return this.shopifyConsentPendingBlock();
6592
6742
  if (typeof cp.analyticsProcessingAllowed === 'function') {
6593
6743
  const allowed = cp.analyticsProcessingAllowed();
6594
6744
  return typeof allowed === 'boolean' ? allowed : null;
@@ -6598,10 +6748,10 @@ var Datalyr = (function (exports) {
6598
6748
  const allowed = cp.userCanBeTracked();
6599
6749
  return typeof allowed === 'boolean' ? allowed : null;
6600
6750
  }
6601
- return this.isShopifyStorefront() ? false : null;
6751
+ return this.shopifyConsentPendingBlock();
6602
6752
  }
6603
6753
  catch (_a) {
6604
- return this.isShopifyStorefront() ? false : null;
6754
+ return this.shopifyConsentPendingBlock();
6605
6755
  }
6606
6756
  }
6607
6757
  /**
@@ -6613,15 +6763,15 @@ var Datalyr = (function (exports) {
6613
6763
  try {
6614
6764
  const cp = this.getShopifyCustomerPrivacy();
6615
6765
  if (!cp || typeof cp.marketingAllowed !== 'function') {
6616
- return this.isShopifyStorefront() ? false : null;
6766
+ return this.shopifyConsentPendingBlock();
6617
6767
  }
6618
6768
  const allowed = cp.marketingAllowed();
6619
6769
  return typeof allowed === 'boolean'
6620
6770
  ? allowed
6621
- : (this.isShopifyStorefront() ? false : null);
6771
+ : (this.shopifyConsentPendingBlock());
6622
6772
  }
6623
6773
  catch (_a) {
6624
- return this.isShopifyStorefront() ? false : null;
6774
+ return this.shopifyConsentPendingBlock();
6625
6775
  }
6626
6776
  }
6627
6777
  /**
@@ -6657,18 +6807,31 @@ var Datalyr = (function (exports) {
6657
6807
  // banner interaction. Guarded — a missing/changed loadFeatures must never break tracking.
6658
6808
  const shopify = window.Shopify;
6659
6809
  if (shopify && typeof shopify.loadFeatures === 'function') {
6660
- shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], (error) => {
6661
- if (error) {
6662
- this.log('Shopify loadFeatures(consent-tracking-api) failed:', error);
6663
- return;
6664
- }
6665
- try {
6666
- this.onShopifyConsentChanged();
6667
- }
6668
- catch (e) {
6669
- this.log('Shopify post-load consent eval failed:', e);
6670
- }
6671
- });
6810
+ // One retry before declaring the API unreachable: a TRANSIENT loadFeatures
6811
+ // failure must not fail-open the whole session for a previously-declined
6812
+ // shopper. Only after the retry (or a success that still yields no
6813
+ // customerPrivacy) is the pre-load window declared over — then the
6814
+ // fail-closed gate releases and consent re-evaluates (a decline gates).
6815
+ const attempt = (retriesLeft) => {
6816
+ shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], (error) => {
6817
+ if (error && retriesLeft > 0) {
6818
+ this.log('Shopify loadFeatures(consent-tracking-api) failed, retrying:', error);
6819
+ setTimeout(() => attempt(retriesLeft - 1), 1000);
6820
+ return;
6821
+ }
6822
+ if (error)
6823
+ this.log('Shopify loadFeatures(consent-tracking-api) failed:', error);
6824
+ if (error || !this.getShopifyCustomerPrivacy())
6825
+ this.shopifyConsentUnresolvable = true;
6826
+ try {
6827
+ this.onShopifyConsentChanged();
6828
+ }
6829
+ catch (e) {
6830
+ this.log('Shopify post-load consent eval failed:', e);
6831
+ }
6832
+ });
6833
+ };
6834
+ attempt(1);
6672
6835
  }
6673
6836
  }
6674
6837
  catch (error) {