@datalyr/web 1.7.8 → 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/container.d.ts +7 -3
- package/dist/container.d.ts.map +1 -1
- package/dist/container.test.d.ts +2 -0
- package/dist/container.test.d.ts.map +1 -0
- package/dist/datalyr.cjs.js +254 -31
- package/dist/datalyr.cjs.js.map +1 -1
- package/dist/datalyr.esm.js +254 -31
- 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 +254 -31
- 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/datalyr.esm.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
|
|
@@ -815,7 +815,11 @@ const REDACTED_URL_PARAMS = new Set([
|
|
|
815
815
|
'token', 'access_token', 'refresh_token', 'id_token', 'auth', 'authorization',
|
|
816
816
|
'password', 'pass', 'pwd', 'secret', 'api_key', 'apikey', 'key',
|
|
817
817
|
'session', 'session_id', 'sid', 'email', 'e', 'phone', 'tel',
|
|
818
|
-
'signature', 'sig', 'otp', 'reset', 'hash'
|
|
818
|
+
'signature', 'sig', 'otp', 'reset', 'hash',
|
|
819
|
+
// Klaviyo profile IDs are deterministic customer identifiers. Capture them
|
|
820
|
+
// through the dedicated identity envelope, but never retain them in URLs,
|
|
821
|
+
// referrers, first/last-touch records, or generic event properties.
|
|
822
|
+
'dl_kprofile_id'
|
|
819
823
|
]);
|
|
820
824
|
// BATCH-2(e): `code` is CONDITIONAL, not in the always-redact set above. An OAuth
|
|
821
825
|
// authorization code (account-takeover-grade if it leaks to ad platforms) ALWAYS travels
|
|
@@ -1760,7 +1764,8 @@ class SessionManager {
|
|
|
1760
1764
|
class AttributionManager {
|
|
1761
1765
|
constructor(options = {}) {
|
|
1762
1766
|
this.queryParamsCache = null;
|
|
1763
|
-
this.
|
|
1767
|
+
this.pendingKlaviyoProfileId = null;
|
|
1768
|
+
this.UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id'];
|
|
1764
1769
|
// Updated to match dl.js - includes ALL ad platform click IDs
|
|
1765
1770
|
this.CLICK_IDS = [
|
|
1766
1771
|
'fbclid', // Facebook/Meta
|
|
@@ -1805,7 +1810,11 @@ class AttributionManager {
|
|
|
1805
1810
|
'source', // Generic source (non-UTM)
|
|
1806
1811
|
'campaign', // Generic campaign (non-UTM)
|
|
1807
1812
|
'medium', // Generic medium (non-UTM)
|
|
1808
|
-
'gad_source' // Google Ads source parameter
|
|
1813
|
+
'gad_source', // Google Ads source parameter
|
|
1814
|
+
// Datalyr-owned Klaviyo parameters. These live beside merchant UTMs so
|
|
1815
|
+
// account-level setup never replaces customer values.
|
|
1816
|
+
'dl_ksource',
|
|
1817
|
+
'dl_kmessage_id'
|
|
1809
1818
|
];
|
|
1810
1819
|
// TR-03: ad-cookie fields (Meta/Google Ads/TikTok/Snap) that are MARKETING-scoped. When
|
|
1811
1820
|
// marketing consent is declined these are neither synthesized/written nor shipped. Google
|
|
@@ -1815,6 +1824,69 @@ class AttributionManager {
|
|
|
1815
1824
|
// Merge default tracked params with user-provided ones
|
|
1816
1825
|
this.trackedParams = [...this.DEFAULT_TRACKED_PARAMS, ...(options.trackedParams || [])];
|
|
1817
1826
|
this.marketingAllowedFn = options.marketingAllowed;
|
|
1827
|
+
this.replaceVisibleUrl = options.replaceVisibleUrl;
|
|
1828
|
+
}
|
|
1829
|
+
validKlaviyoProfileId(value) {
|
|
1830
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
1831
|
+
if (!id || id.length > 512 || /[\u0000-\u001f\u007f]/.test(id))
|
|
1832
|
+
return null;
|
|
1833
|
+
return id;
|
|
1834
|
+
}
|
|
1835
|
+
/** Capture the dedicated Klaviyo identity parameter before URL redaction.
|
|
1836
|
+
* It deliberately never enters the Attribution object or touch storage. */
|
|
1837
|
+
captureKlaviyoProfileBinding(params) {
|
|
1838
|
+
if (!Object.prototype.hasOwnProperty.call(params, 'dl_kprofile_id'))
|
|
1839
|
+
return;
|
|
1840
|
+
const profileId = this.validKlaviyoProfileId(params.dl_kprofile_id);
|
|
1841
|
+
delete params.dl_kprofile_id;
|
|
1842
|
+
if (profileId && this.isMarketingAllowed())
|
|
1843
|
+
this.pendingKlaviyoProfileId = profileId;
|
|
1844
|
+
try {
|
|
1845
|
+
const url = new URL(window.location.href);
|
|
1846
|
+
if (url.searchParams.has('dl_kprofile_id')) {
|
|
1847
|
+
url.searchParams.delete('dl_kprofile_id');
|
|
1848
|
+
const clean = `${url.pathname}${url.search}${url.hash}`;
|
|
1849
|
+
if (this.replaceVisibleUrl)
|
|
1850
|
+
this.replaceVisibleUrl(clean);
|
|
1851
|
+
else
|
|
1852
|
+
window.history.replaceState(window.history.state, '', clean);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
catch (_a) {
|
|
1856
|
+
// Capture still succeeds when a synthetic test/browser URL cannot parse.
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
/** Called once encryption is ready. The short TTL only bridges async init or
|
|
1860
|
+
* a reload before the landing event is queued; it is not a profile cache. */
|
|
1861
|
+
hydrateKlaviyoProfileBinding() {
|
|
1862
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1863
|
+
const now = Date.now();
|
|
1864
|
+
if (!this.pendingKlaviyoProfileId) {
|
|
1865
|
+
const stored = yield storage.getEncrypted(AttributionManager.KLAVIYO_BINDING_KEY, null);
|
|
1866
|
+
const restored = this.validKlaviyoProfileId(stored === null || stored === void 0 ? void 0 : stored.profileId);
|
|
1867
|
+
if (restored && Number(stored === null || stored === void 0 ? void 0 : stored.expiresAt) > now && this.isMarketingAllowed()) {
|
|
1868
|
+
this.pendingKlaviyoProfileId = restored;
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
storage.remove(AttributionManager.KLAVIYO_BINDING_KEY);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (this.pendingKlaviyoProfileId) {
|
|
1875
|
+
yield storage.setEncrypted(AttributionManager.KLAVIYO_BINDING_KEY, {
|
|
1876
|
+
profileId: this.pendingKlaviyoProfileId,
|
|
1877
|
+
expiresAt: now + AttributionManager.KLAVIYO_BINDING_TTL_MS,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
/** One-shot binding for the next queued event. */
|
|
1883
|
+
consumeKlaviyoProfileBinding() {
|
|
1884
|
+
if (!this.isMarketingAllowed())
|
|
1885
|
+
this.pendingKlaviyoProfileId = null;
|
|
1886
|
+
const profileId = this.pendingKlaviyoProfileId;
|
|
1887
|
+
this.pendingKlaviyoProfileId = null;
|
|
1888
|
+
storage.remove(AttributionManager.KLAVIYO_BINDING_KEY);
|
|
1889
|
+
return profileId;
|
|
1818
1890
|
}
|
|
1819
1891
|
// TR-03: default (no predicate / no signal) = allowed → byte-identical to prior behavior.
|
|
1820
1892
|
isMarketingAllowed() {
|
|
@@ -1837,6 +1909,7 @@ class AttributionManager {
|
|
|
1837
1909
|
if (!this.queryParamsCache) {
|
|
1838
1910
|
this.queryParamsCache = params;
|
|
1839
1911
|
}
|
|
1912
|
+
this.captureKlaviyoProfileBinding(params);
|
|
1840
1913
|
const attribution = {
|
|
1841
1914
|
timestamp: Date.now()
|
|
1842
1915
|
};
|
|
@@ -1851,8 +1924,13 @@ class AttributionManager {
|
|
|
1851
1924
|
const value = params[utm];
|
|
1852
1925
|
if (value) {
|
|
1853
1926
|
attribution[utm] = value; // canonical: utm_source, utm_campaign, ...
|
|
1854
|
-
|
|
1855
|
-
|
|
1927
|
+
// The five original UTM dimensions keep their legacy stripped aliases.
|
|
1928
|
+
// `utm_id` is intentionally canonical-only: writing it to a generic `id`
|
|
1929
|
+
// property would collide with event/customer identifiers downstream.
|
|
1930
|
+
if (utm !== 'utm_id') {
|
|
1931
|
+
const key = utm.replace('utm_', '');
|
|
1932
|
+
attribution[key] = value; // alias: source, campaign, ...
|
|
1933
|
+
}
|
|
1856
1934
|
}
|
|
1857
1935
|
}
|
|
1858
1936
|
// Capture click IDs. The first present (by CLICK_IDS priority) is the primary
|
|
@@ -1889,6 +1967,12 @@ class AttributionManager {
|
|
|
1889
1967
|
attribution[param] = value;
|
|
1890
1968
|
}
|
|
1891
1969
|
}
|
|
1970
|
+
// The namespaced source is Datalyr's routing authority. Keep any
|
|
1971
|
+
// merchant-owned utm_source intact while still classifying this visit as
|
|
1972
|
+
// Klaviyo everywhere source-level attribution is displayed.
|
|
1973
|
+
if (attribution.dl_ksource === 'klaviyo') {
|
|
1974
|
+
attribution.source = 'klaviyo';
|
|
1975
|
+
}
|
|
1892
1976
|
// Capture referrer. 9.A.4: redact secret/PII query-param values (reset tokens,
|
|
1893
1977
|
// OAuth codes, emails) — this attribution object is stamped onto every event AND
|
|
1894
1978
|
// persisted into dl_first_touch/dl_last_touch, so it must be clean at capture.
|
|
@@ -2152,6 +2236,21 @@ class AttributionManager {
|
|
|
2152
2236
|
current = Object.assign(Object.assign({}, fallback), { referrer: current.referrer, referrerHost: current.referrerHost, landingPage: current.landingPage, landingPath: current.landingPath });
|
|
2153
2237
|
}
|
|
2154
2238
|
}
|
|
2239
|
+
// A real signal WITHOUT its own link tag must not erase the stored one. The
|
|
2240
|
+
// creator/bio-link journey is click-link-today, return-from-instagram-next-week:
|
|
2241
|
+
// that organic revisit scores hasRealAttribution (external referrer) with no
|
|
2242
|
+
// `lyr`, and before this carry-forward storeLastTouch below replaced the tagged
|
|
2243
|
+
// touch wholesale — every later event (and the Track page's conversion count,
|
|
2244
|
+
// which keys on the event's own lyr) silently lost the link. Semantics = "last
|
|
2245
|
+
// non-empty lyr within the window", the same answer the server's resolveLyr
|
|
2246
|
+
// argMax gives webhook conversions; a NEW ?lyr= still wins because current.lyr
|
|
2247
|
+
// is already set. source/medium/campaign still update — lyr is an independent
|
|
2248
|
+
// dimension (which link), not a channel claim.
|
|
2249
|
+
if (hasRealAttribution && !current.lyr) {
|
|
2250
|
+
const storedLyr = (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.lyr) || (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.lyr);
|
|
2251
|
+
if (storedLyr)
|
|
2252
|
+
current.lyr = storedLyr;
|
|
2253
|
+
}
|
|
2155
2254
|
// Capture advertising cookies automatically
|
|
2156
2255
|
const adCookies = this.captureAdCookies();
|
|
2157
2256
|
// Only (re)write first/last touch when this pageview carried a REAL signal — never
|
|
@@ -2328,6 +2427,8 @@ class AttributionManager {
|
|
|
2328
2427
|
}
|
|
2329
2428
|
}
|
|
2330
2429
|
}
|
|
2430
|
+
AttributionManager.KLAVIYO_BINDING_KEY = 'dl_klaviyo_profile_binding';
|
|
2431
|
+
AttributionManager.KLAVIYO_BINDING_TTL_MS = 10 * 60 * 1000;
|
|
2331
2432
|
|
|
2332
2433
|
/**
|
|
2333
2434
|
* Event Queue and Batching Module
|
|
@@ -3687,12 +3788,10 @@ class ContainerManager {
|
|
|
3687
3788
|
img.dataset.datalyrPixel = script.id;
|
|
3688
3789
|
document.body.appendChild(img);
|
|
3689
3790
|
}
|
|
3690
|
-
/**
|
|
3691
|
-
* Initialize third-party pixels (Meta, Google, TikTok)
|
|
3692
|
-
*/
|
|
3791
|
+
/** Initialize configured third-party pixels in the merchant's page context. */
|
|
3693
3792
|
initializePixels() {
|
|
3694
3793
|
return __awaiter(this, void 0, void 0, function* () {
|
|
3695
|
-
var _a, _b, _c;
|
|
3794
|
+
var _a, _b, _c, _e;
|
|
3696
3795
|
if (!this.pixels)
|
|
3697
3796
|
return;
|
|
3698
3797
|
// Initialize Meta Pixel
|
|
@@ -3707,8 +3806,58 @@ class ContainerManager {
|
|
|
3707
3806
|
if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
|
|
3708
3807
|
this.initializeTikTokPixel(this.pixels.tiktok);
|
|
3709
3808
|
}
|
|
3809
|
+
// The Whop Pixel needs only the merchant's public company ID. Loading it
|
|
3810
|
+
// here lets Whop link this external-site visitor to a later hosted or
|
|
3811
|
+
// embedded Whop checkout without a Datalyr visitor_id in checkout metadata.
|
|
3812
|
+
if (((_e = this.pixels.whop) === null || _e === void 0 ? void 0 : _e.enabled) && this.pixels.whop.company_id) {
|
|
3813
|
+
this.initializeWhopPixel(this.pixels.whop);
|
|
3814
|
+
}
|
|
3710
3815
|
});
|
|
3711
3816
|
}
|
|
3817
|
+
/** Initialize Whop's official first-party attribution pixel. */
|
|
3818
|
+
initializeWhopPixel(config) {
|
|
3819
|
+
var _a, _b;
|
|
3820
|
+
try {
|
|
3821
|
+
const host = window;
|
|
3822
|
+
if (!host.whop) {
|
|
3823
|
+
const queue = host.whop = {
|
|
3824
|
+
q: [],
|
|
3825
|
+
t: Date.now(),
|
|
3826
|
+
s: [],
|
|
3827
|
+
o: 'https://t.whop.tw',
|
|
3828
|
+
track: function (...args) {
|
|
3829
|
+
queue.q.push([Date.now(), ...args]);
|
|
3830
|
+
},
|
|
3831
|
+
setScope: function (...args) {
|
|
3832
|
+
queue.s = args.filter((value) => typeof value === 'string');
|
|
3833
|
+
queue.q.push([Date.now(), 'setScope', ...queue.s]);
|
|
3834
|
+
},
|
|
3835
|
+
scope: function (...scope) {
|
|
3836
|
+
return {
|
|
3837
|
+
track: function (...args) {
|
|
3838
|
+
queue.q.push([Date.now(), ...args, { __scope: scope }]);
|
|
3839
|
+
},
|
|
3840
|
+
};
|
|
3841
|
+
},
|
|
3842
|
+
};
|
|
3843
|
+
const script = document.createElement('script');
|
|
3844
|
+
script.async = true;
|
|
3845
|
+
script.src = 'https://t.whop.tw/s.js';
|
|
3846
|
+
const firstScript = document.getElementsByTagName('script')[0];
|
|
3847
|
+
(_a = firstScript === null || firstScript === void 0 ? void 0 : firstScript.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(script, firstScript);
|
|
3848
|
+
if (!firstScript)
|
|
3849
|
+
document.head.appendChild(script);
|
|
3850
|
+
}
|
|
3851
|
+
if (typeof ((_b = host.whop) === null || _b === void 0 ? void 0 : _b.setScope) !== 'function') {
|
|
3852
|
+
throw new Error('Existing window.whop does not expose setScope');
|
|
3853
|
+
}
|
|
3854
|
+
host.whop.setScope(config.company_id);
|
|
3855
|
+
this.log('Whop Pixel initialized:', config.company_id);
|
|
3856
|
+
}
|
|
3857
|
+
catch (error) {
|
|
3858
|
+
this.log('Error initializing Whop Pixel:', error);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3712
3861
|
/**
|
|
3713
3862
|
* Initialize Meta (Facebook) Pixel
|
|
3714
3863
|
*
|
|
@@ -3864,7 +4013,7 @@ class ContainerManager {
|
|
|
3864
4013
|
* Track event to all initialized pixels
|
|
3865
4014
|
*/
|
|
3866
4015
|
trackToPixels(eventName, properties = {}, eventId) {
|
|
3867
|
-
var _a, _b, _c, _e, _f, _g, _h, _j, _k, _l;
|
|
4016
|
+
var _a, _b, _c, _e, _f, _g, _h, _j, _k, _l, _m, _p;
|
|
3868
4017
|
// Datalyr-internal events ($identify, $group, $alias, $auto_identify,
|
|
3869
4018
|
// $app_download_click, …) are not conversions — never forward them to ad
|
|
3870
4019
|
// pixels. (Previously they fired as noise custom events with the $ stripped.)
|
|
@@ -3961,6 +4110,18 @@ class ContainerManager {
|
|
|
3961
4110
|
this.log('Error tracking TikTok Pixel event:', error);
|
|
3962
4111
|
}
|
|
3963
4112
|
}
|
|
4113
|
+
// Whop records checkout and payment events server-side. Datalyr only sends
|
|
4114
|
+
// page views here, including SPA navigations, so purchases are never doubled.
|
|
4115
|
+
if (((_p = (_m = this.pixels) === null || _m === void 0 ? void 0 : _m.whop) === null || _p === void 0 ? void 0 : _p.enabled) &&
|
|
4116
|
+
window.whop &&
|
|
4117
|
+
(eventName === 'pageview' || eventName === 'page_view')) {
|
|
4118
|
+
try {
|
|
4119
|
+
window.whop.track('page');
|
|
4120
|
+
}
|
|
4121
|
+
catch (error) {
|
|
4122
|
+
this.log('Error tracking Whop Pixel page:', error);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
3964
4125
|
}
|
|
3965
4126
|
/**
|
|
3966
4127
|
* Manually trigger a custom script
|
|
@@ -4928,6 +5089,7 @@ class Datalyr {
|
|
|
4928
5089
|
this.initialized = false;
|
|
4929
5090
|
this.errors = [];
|
|
4930
5091
|
this.MAX_ERRORS = 50;
|
|
5092
|
+
this.shopifyConsentUnresolvable = false; // loadFeatures failed or never yielded customerPrivacy → stop fail-closing
|
|
4931
5093
|
this.lastSpaPath = null; // dedups SPA pageviews (replaceState-on-mount double-fire)
|
|
4932
5094
|
// Shopify loads Customer Privacy asynchronously. Keep the initial pageview
|
|
4933
5095
|
// pending until initialization is complete and analytics consent is known,
|
|
@@ -5007,7 +5169,15 @@ class Datalyr {
|
|
|
5007
5169
|
// TR-03: gate ad-signal synthesis + shipping on LIVE marketing consent. Returns true by
|
|
5008
5170
|
// default (no consent signal) so behavior is unchanged for the common case; false only on
|
|
5009
5171
|
// an explicit decline (Shopify marketing:false / setConsent marketing|sale=false).
|
|
5010
|
-
marketingAllowed: () => this.consentAllowsMarketing()
|
|
5172
|
+
marketingAllowed: () => this.consentAllowsMarketing(),
|
|
5173
|
+
// Bypass our SPA wrapper when removing the one-time profile parameter,
|
|
5174
|
+
// otherwise the privacy cleanup itself would manufacture a pageview.
|
|
5175
|
+
replaceVisibleUrl: (url) => {
|
|
5176
|
+
var _a;
|
|
5177
|
+
const replace = (_a = this.originalReplaceState) !== null && _a !== void 0 ? _a : history.replaceState;
|
|
5178
|
+
replace.call(history, history.state, '', url);
|
|
5179
|
+
this.lastSpaPath = window.location.pathname + window.location.search + window.location.hash;
|
|
5180
|
+
},
|
|
5011
5181
|
});
|
|
5012
5182
|
this.queue = new EventQueue(this.config);
|
|
5013
5183
|
this.fingerprint = new FingerprintCollector({
|
|
@@ -5097,6 +5267,7 @@ class Datalyr {
|
|
|
5097
5267
|
// in the IdentityManager constructor. It never overwrites an id already
|
|
5098
5268
|
// set by an explicit identify() earlier in this page load.
|
|
5099
5269
|
yield this.identity.hydrateEncryptedUserId();
|
|
5270
|
+
yield this.attribution.hydrateKlaviyoProfileBinding();
|
|
5100
5271
|
this.log('Encryption initialized, user properties loaded');
|
|
5101
5272
|
}
|
|
5102
5273
|
catch (encErr) {
|
|
@@ -5360,6 +5531,14 @@ class Datalyr {
|
|
|
5360
5531
|
referrerParams.set('utm_content', lastTouch.content);
|
|
5361
5532
|
if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.term)
|
|
5362
5533
|
referrerParams.set('utm_term', lastTouch.term);
|
|
5534
|
+
if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.utm_id)
|
|
5535
|
+
referrerParams.set('utm_id', lastTouch.utm_id);
|
|
5536
|
+
// The trackable-link tag rides the referrer too — the edge worker's own
|
|
5537
|
+
// server-side appendPlayReferrer packs it, and ingest parses it back out
|
|
5538
|
+
// of install_referrer_url. Without it, interstitial-mode Android installs
|
|
5539
|
+
// lose the deterministic link tag that a bare 302 would have carried.
|
|
5540
|
+
if (lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.lyr)
|
|
5541
|
+
referrerParams.set('lyr', lastTouch.lyr);
|
|
5363
5542
|
try {
|
|
5364
5543
|
const url = new URL(options.appStoreUrl);
|
|
5365
5544
|
url.searchParams.set('referrer', referrerParams.toString());
|
|
@@ -6360,6 +6539,16 @@ class Datalyr {
|
|
|
6360
6539
|
// Add attribution data (caller wins on collisions)
|
|
6361
6540
|
const attributionData = this.attribution.getAttributionData();
|
|
6362
6541
|
assignMissing(eventData, attributionData);
|
|
6542
|
+
// A Klaviyo landing URL may carry the profile ID as a dedicated custom
|
|
6543
|
+
// parameter. Emit it once through the deterministic external-id envelope;
|
|
6544
|
+
// never copy it into generic attribution or user-visible URL fields.
|
|
6545
|
+
const klaviyoProfileId = this.attribution.consumeKlaviyoProfileBinding();
|
|
6546
|
+
if (klaviyoProfileId) {
|
|
6547
|
+
const existingExternalIds = eventData.external_ids;
|
|
6548
|
+
eventData.external_ids = Object.assign(Object.assign({}, (existingExternalIds && typeof existingExternalIds === 'object' && !Array.isArray(existingExternalIds)
|
|
6549
|
+
? existingExternalIds
|
|
6550
|
+
: {})), { klaviyo: klaviyoProfileId });
|
|
6551
|
+
}
|
|
6363
6552
|
// Add session metrics (SDK-authoritative — session_id et al. must not be overridden)
|
|
6364
6553
|
const sessionMetrics = this.session.getMetrics();
|
|
6365
6554
|
Object.assign(eventData, sessionMetrics);
|
|
@@ -6427,7 +6616,7 @@ class Datalyr {
|
|
|
6427
6616
|
// drift from the package again (it sat at 1.7.4 across the 1.7.5 release). The
|
|
6428
6617
|
// build guard (scripts/check-bundle.js, run by build:check) still verifies the
|
|
6429
6618
|
// deployable bundles carry the package.json version. (FSR-103)
|
|
6430
|
-
sdk_version: '1.7.
|
|
6619
|
+
sdk_version: '1.7.12',
|
|
6431
6620
|
sdk_name: 'datalyr-web-sdk',
|
|
6432
6621
|
// A3-25: versioned-envelope stamp. Every SDK emits schema_version so the ingest/contract
|
|
6433
6622
|
// layer can key on ONE canonical envelope version (snake_case fields, event_name/event_data
|
|
@@ -6516,6 +6705,27 @@ class Datalyr {
|
|
|
6516
6705
|
return false;
|
|
6517
6706
|
}
|
|
6518
6707
|
}
|
|
6708
|
+
/**
|
|
6709
|
+
* TR-15 refinement: fail closed ONLY while the Customer Privacy API can still
|
|
6710
|
+
* arrive (Shopify.loadFeatures exists and hasn't failed). A storefront with no
|
|
6711
|
+
* loadFeatures (headless installs, `platform:'shopify'` on a non-Shopify page)
|
|
6712
|
+
* has NO path to a consent answer — blocking there is not a pre-load window,
|
|
6713
|
+
* it is zero events forever, silently. No signal possible → null (fail open);
|
|
6714
|
+
* false is reserved for an explicit decline from the API.
|
|
6715
|
+
*/
|
|
6716
|
+
shopifyConsentPendingBlock() {
|
|
6717
|
+
var _a;
|
|
6718
|
+
if (!this.isShopifyStorefront())
|
|
6719
|
+
return null;
|
|
6720
|
+
if (this.shopifyConsentUnresolvable)
|
|
6721
|
+
return null;
|
|
6722
|
+
try {
|
|
6723
|
+
return typeof ((_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.loadFeatures) === 'function' ? false : null;
|
|
6724
|
+
}
|
|
6725
|
+
catch (_b) {
|
|
6726
|
+
return null;
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6519
6729
|
/**
|
|
6520
6730
|
* Shopify's analytics-consent signal: true/false when the Customer Privacy API is
|
|
6521
6731
|
* present and answers, null when there's no signal (API absent / unexpected shape)
|
|
@@ -6525,7 +6735,7 @@ class Datalyr {
|
|
|
6525
6735
|
try {
|
|
6526
6736
|
const cp = this.getShopifyCustomerPrivacy();
|
|
6527
6737
|
if (!cp)
|
|
6528
|
-
return this.
|
|
6738
|
+
return this.shopifyConsentPendingBlock();
|
|
6529
6739
|
if (typeof cp.analyticsProcessingAllowed === 'function') {
|
|
6530
6740
|
const allowed = cp.analyticsProcessingAllowed();
|
|
6531
6741
|
return typeof allowed === 'boolean' ? allowed : null;
|
|
@@ -6535,10 +6745,10 @@ class Datalyr {
|
|
|
6535
6745
|
const allowed = cp.userCanBeTracked();
|
|
6536
6746
|
return typeof allowed === 'boolean' ? allowed : null;
|
|
6537
6747
|
}
|
|
6538
|
-
return this.
|
|
6748
|
+
return this.shopifyConsentPendingBlock();
|
|
6539
6749
|
}
|
|
6540
6750
|
catch (_a) {
|
|
6541
|
-
return this.
|
|
6751
|
+
return this.shopifyConsentPendingBlock();
|
|
6542
6752
|
}
|
|
6543
6753
|
}
|
|
6544
6754
|
/**
|
|
@@ -6550,15 +6760,15 @@ class Datalyr {
|
|
|
6550
6760
|
try {
|
|
6551
6761
|
const cp = this.getShopifyCustomerPrivacy();
|
|
6552
6762
|
if (!cp || typeof cp.marketingAllowed !== 'function') {
|
|
6553
|
-
return this.
|
|
6763
|
+
return this.shopifyConsentPendingBlock();
|
|
6554
6764
|
}
|
|
6555
6765
|
const allowed = cp.marketingAllowed();
|
|
6556
6766
|
return typeof allowed === 'boolean'
|
|
6557
6767
|
? allowed
|
|
6558
|
-
: (this.
|
|
6768
|
+
: (this.shopifyConsentPendingBlock());
|
|
6559
6769
|
}
|
|
6560
6770
|
catch (_a) {
|
|
6561
|
-
return this.
|
|
6771
|
+
return this.shopifyConsentPendingBlock();
|
|
6562
6772
|
}
|
|
6563
6773
|
}
|
|
6564
6774
|
/**
|
|
@@ -6594,18 +6804,31 @@ class Datalyr {
|
|
|
6594
6804
|
// banner interaction. Guarded — a missing/changed loadFeatures must never break tracking.
|
|
6595
6805
|
const shopify = window.Shopify;
|
|
6596
6806
|
if (shopify && typeof shopify.loadFeatures === 'function') {
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6807
|
+
// One retry before declaring the API unreachable: a TRANSIENT loadFeatures
|
|
6808
|
+
// failure must not fail-open the whole session for a previously-declined
|
|
6809
|
+
// shopper. Only after the retry (or a success that still yields no
|
|
6810
|
+
// customerPrivacy) is the pre-load window declared over — then the
|
|
6811
|
+
// fail-closed gate releases and consent re-evaluates (a decline gates).
|
|
6812
|
+
const attempt = (retriesLeft) => {
|
|
6813
|
+
shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], (error) => {
|
|
6814
|
+
if (error && retriesLeft > 0) {
|
|
6815
|
+
this.log('Shopify loadFeatures(consent-tracking-api) failed, retrying:', error);
|
|
6816
|
+
setTimeout(() => attempt(retriesLeft - 1), 1000);
|
|
6817
|
+
return;
|
|
6818
|
+
}
|
|
6819
|
+
if (error)
|
|
6820
|
+
this.log('Shopify loadFeatures(consent-tracking-api) failed:', error);
|
|
6821
|
+
if (error || !this.getShopifyCustomerPrivacy())
|
|
6822
|
+
this.shopifyConsentUnresolvable = true;
|
|
6823
|
+
try {
|
|
6824
|
+
this.onShopifyConsentChanged();
|
|
6825
|
+
}
|
|
6826
|
+
catch (e) {
|
|
6827
|
+
this.log('Shopify post-load consent eval failed:', e);
|
|
6828
|
+
}
|
|
6829
|
+
});
|
|
6830
|
+
};
|
|
6831
|
+
attempt(1);
|
|
6609
6832
|
}
|
|
6610
6833
|
}
|
|
6611
6834
|
catch (error) {
|