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