@datalyr/web 1.6.3 → 1.6.4

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.3
2
+ * @datalyr/web v1.6.4
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -1527,6 +1527,7 @@ var Datalyr = (function (exports) {
1527
1527
  * Capture advertising platform cookies
1528
1528
  */
1529
1529
  captureAdCookies() {
1530
+ var _a, _b;
1530
1531
  const adCookies = {};
1531
1532
  // Facebook/Meta cookies
1532
1533
  adCookies._fbp = cookies.get('_fbp');
@@ -1551,13 +1552,29 @@ var Datalyr = (function (exports) {
1551
1552
  // Optionally set the cookie for future use
1552
1553
  cookies.set('_fbp', adCookies._fbp, 90);
1553
1554
  }
1555
+ // Persist the click time the FIRST time we see fbclid/gclid in URL so we can
1556
+ // rebuild fbc (and stamp real click time on server-side events) later even
1557
+ // when _fbc/_gclid cookies get evicted. Once-only: never overwrite.
1558
+ const fbclid = this.getCurrentFbclid();
1559
+ if (fbclid && !cookies.get('_dl_fbclid_at')) {
1560
+ cookies.set('_dl_fbclid_at', String(Date.now()), 90);
1561
+ }
1562
+ const gclid = this.hasClickId('gclid') ? ((_b = (_a = this.queryParamsCache) === null || _a === void 0 ? void 0 : _a.gclid) !== null && _b !== void 0 ? _b : null) : null;
1563
+ if (gclid && !cookies.get('_dl_gclid_at')) {
1564
+ cookies.set('_dl_gclid_at', String(Date.now()), 90);
1565
+ }
1554
1566
  // Generate _fbc if we have fbclid but no _fbc.
1555
1567
  // Meta's fbc format is `fb.{subdomainIndex}.{creationTime}.{fbclid}` where
1556
1568
  // creationTime is UNIX time in MILLISECONDS (matches the _fbp generation above
1557
1569
  // and the real _fbc cookie the Meta Pixel writes). Do NOT use seconds here.
1558
- const fbclid = this.getCurrentFbclid();
1559
1570
  if (fbclid && !adCookies._fbc) {
1560
- const timestamp = Date.now();
1571
+ // Prefer the persisted click time when valid; fall back to now if the
1572
+ // cookie is missing or corrupted (e.g. user edited it to garbage).
1573
+ // Without this guard, Number("abc") = NaN and Meta would reject
1574
+ // `fb.1.NaN.{fbclid}`.
1575
+ const stored = cookies.get('_dl_fbclid_at');
1576
+ const parsed = stored ? Number(stored) : NaN;
1577
+ const timestamp = Number.isFinite(parsed) && parsed > 0 ? parsed : Date.now();
1561
1578
  adCookies._fbc = `fb.1.${timestamp}.${fbclid}`;
1562
1579
  // Optionally set the cookie for future use
1563
1580
  cookies.set('_fbc', adCookies._fbc, 90);
@@ -3514,6 +3531,14 @@ var Datalyr = (function (exports) {
3514
3531
  }
3515
3532
  // Set default config values
3516
3533
  this.config = Object.assign({ endpoint: 'https://ingest.datalyr.com', debug: false, batchSize: 10, flushInterval: 5000, flushAt: 10, criticalEvents: undefined, highPriorityEvents: undefined, sessionTimeout: 60 * 60 * 1000, trackSessions: true, attributionWindow: 90 * 24 * 60 * 60 * 1000, trackedParams: [], respectDoNotTrack: false, respectGlobalPrivacyControl: true, privacyMode: 'standard', cookieDomain: 'auto', cookieExpires: 365, secureCookie: 'auto', sameSite: 'Lax', cookiePrefix: '__dl_', enablePerformanceTracking: true, enableFingerprinting: true, maxRetries: 5, retryDelay: 1000, maxOfflineQueueSize: 100, trackSPA: true, trackPageViews: true, fallbackEndpoints: [], plugins: [] }, config);
3534
+ // platform:'shopify' implies cart-attribute sync: stamp visitor_id + Meta
3535
+ // click signals into note_attributes so server-side order webhooks can
3536
+ // attribute guest checkouts. Lets the Shopify theme app-embed enable it via
3537
+ // data-platform="shopify" alone. `=== undefined` so an explicit
3538
+ // shopifyCartAttributes:false still wins.
3539
+ if (this.config.platform === 'shopify' && this.config.shopifyCartAttributes === undefined) {
3540
+ this.config.shopifyCartAttributes = true;
3541
+ }
3517
3542
  // Initialize cookie storage with config
3518
3543
  this.cookies = new CookieStorage({
3519
3544
  domain: this.config.cookieDomain,
@@ -3525,6 +3550,12 @@ var Datalyr = (function (exports) {
3525
3550
  storage.migrateFromLegacyPrefix();
3526
3551
  // Check opt-out AFTER cookies configured (Issue #14)
3527
3552
  this.optedOut = this.cookies.get('__dl_opt_out') === 'true';
3553
+ // CC funnel page entry: restore the _dl_* URL bridge BEFORE IdentityManager
3554
+ // initializes so the storefront's visitor_id is the one used here (instead
3555
+ // of auto-generating a fresh one on this domain).
3556
+ if (this.config.platform === 'checkoutchamp') {
3557
+ this.restoreFromURL();
3558
+ }
3528
3559
  // Initialize modules
3529
3560
  this.identity = new IdentityManager();
3530
3561
  this.session = new SessionManager(this.config.sessionTimeout);
@@ -3609,6 +3640,16 @@ var Datalyr = (function (exports) {
3609
3640
  this.log('Container initialization failed:', error);
3610
3641
  });
3611
3642
  }
3643
+ // autoIdentify default for the CC bridge:
3644
+ // - platform === 'checkoutchamp' (CC funnel pages) OR
3645
+ // - checkoutChampDomains set (Shopify storefronts feeding a CC funnel)
3646
+ // turn autoIdentify on unless the caller explicitly set it to false.
3647
+ // The 95% claim relies on email capture on BOTH ends of the bridge.
3648
+ const ccBridgeActive = this.config.platform === 'checkoutchamp'
3649
+ || (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0);
3650
+ if (ccBridgeActive && this.config.autoIdentify === undefined) {
3651
+ this.config.autoIdentify = true;
3652
+ }
3612
3653
  // Initialize auto-identify if explicitly enabled (opt-in)
3613
3654
  if (this.config.autoIdentify === true) {
3614
3655
  this.autoIdentify = new AutoIdentifyManager({
@@ -3641,6 +3682,12 @@ var Datalyr = (function (exports) {
3641
3682
  this.log('Shopify cart attribute sync failed:', error);
3642
3683
  });
3643
3684
  }
3685
+ // Storefront → CC bridge: stamp _dl_* params on outbound links to the
3686
+ // configured CC domains so visitor_id + click signals cross the domain
3687
+ // boundary. Inert unless checkoutChampDomains is set.
3688
+ if (Array.isArray(this.config.checkoutChampDomains) && this.config.checkoutChampDomains.length > 0) {
3689
+ this.syncOutboundLinkParams(this.config.checkoutChampDomains);
3690
+ }
3644
3691
  // Track initial page view if enabled (AFTER encryption ready)
3645
3692
  if (this.config.trackPageViews) {
3646
3693
  this.page();
@@ -4111,6 +4158,7 @@ var Datalyr = (function (exports) {
4111
4158
  const visitorId = this.identity.getAnonymousId();
4112
4159
  const fbc = this.cookies.get("_fbc") || attribution._fbc;
4113
4160
  const fbp = this.cookies.get("_fbp") || attribution._fbp;
4161
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4114
4162
  if (visitorId)
4115
4163
  attributes._datalyr_visitor_id = visitorId;
4116
4164
  if (fbc)
@@ -4119,6 +4167,8 @@ var Datalyr = (function (exports) {
4119
4167
  attributes._datalyr_fbp = String(fbp);
4120
4168
  if (fbclid)
4121
4169
  attributes._datalyr_fbclid = String(fbclid);
4170
+ if (fbclidAt)
4171
+ attributes._datalyr_fbclid_at = String(fbclidAt);
4122
4172
  if (Object.keys(attributes).length === 0)
4123
4173
  return;
4124
4174
  try {
@@ -4136,6 +4186,218 @@ var Datalyr = (function (exports) {
4136
4186
  }
4137
4187
  });
4138
4188
  }
4189
+ /**
4190
+ * Restore _dl_* URL bridge params on a Checkout Champ funnel page.
4191
+ * Runs BEFORE IdentityManager so the storefront's visitor_id wins over a
4192
+ * freshly auto-generated one. Also restores _fbc / _fbp cookies and the
4193
+ * fbclid click time so server-side rebuilds carry the real click moment.
4194
+ *
4195
+ * Strategy: stamp matching cookies (without clobbering pre-existing values),
4196
+ * then rewrite the URL so `_dl_fbclid` becomes `fbclid` — that way the rest
4197
+ * of the SDK's attribution layer (which reads `params.fbclid`) works
4198
+ * unchanged, and any merchant analytics also see the canonical click ID.
4199
+ */
4200
+ restoreFromURL() {
4201
+ var _a;
4202
+ if (typeof window === "undefined" || typeof document === "undefined")
4203
+ return;
4204
+ try {
4205
+ const params = new URLSearchParams(window.location.search);
4206
+ const get = (k) => params.get(k);
4207
+ const vid = get("_dl_vid");
4208
+ const fbc = get("_dl_fbc");
4209
+ const fbp = get("_dl_fbp");
4210
+ const fbclid = get("_dl_fbclid");
4211
+ const fbclidAt = get("_dl_fbclid_at");
4212
+ const gclid = get("_dl_gclid");
4213
+ const gclidAt = get("_dl_gclid_at");
4214
+ // visitor_id: bridge wins. The whole point of `_dl_vid` is to unify the
4215
+ // storefront's session with the CC funnel session. A pre-existing local
4216
+ // cookie from a prior direct CC visit would silently sink the integration
4217
+ // (CC events stay on the local id; storefront events use the bridged id;
4218
+ // the two never link). Overwrite — orphaned local events are fine.
4219
+ if (vid)
4220
+ this.cookies.set("__dl_visitor_id", vid, 365);
4221
+ // Meta cookies + click-time cookies: existing wins. _fbc / _fbp may have
4222
+ // been written by Meta Pixel on the CC funnel page itself (more recent
4223
+ // than the bridged value); _dl_fbclid_at should record first-touch click
4224
+ // time per device, not get reset by a bridge from a new campaign.
4225
+ const setIfMissing = (name, value) => {
4226
+ if (!value)
4227
+ return;
4228
+ if (this.cookies.get(name))
4229
+ return;
4230
+ this.cookies.set(name, value, 365);
4231
+ };
4232
+ setIfMissing("_fbc", fbc);
4233
+ setIfMissing("_fbp", fbp);
4234
+ setIfMissing("_dl_fbclid_at", fbclidAt);
4235
+ setIfMissing("_dl_gclid_at", gclidAt);
4236
+ // Rewrite URL: _dl_fbclid → fbclid (etc.) so captureAttribution() picks
4237
+ // them up via its existing `params.fbclid` path. Strip the _dl_* params
4238
+ // either way so they don't leak into downstream analytics URLs.
4239
+ let rewrote = false;
4240
+ const mappings = [
4241
+ ["fbclid", fbclid],
4242
+ ["gclid", gclid]
4243
+ ];
4244
+ for (const [canonical, value] of mappings) {
4245
+ if (value && !params.get(canonical)) {
4246
+ params.set(canonical, value);
4247
+ rewrote = true;
4248
+ }
4249
+ }
4250
+ for (const k of ["_dl_vid", "_dl_fbc", "_dl_fbp", "_dl_fbclid", "_dl_fbclid_at", "_dl_gclid", "_dl_gclid_at"]) {
4251
+ if (params.has(k)) {
4252
+ params.delete(k);
4253
+ rewrote = true;
4254
+ }
4255
+ }
4256
+ if (rewrote && typeof ((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState) === "function") {
4257
+ const newSearch = params.toString();
4258
+ const newUrl = window.location.pathname +
4259
+ (newSearch ? "?" + newSearch : "") +
4260
+ window.location.hash;
4261
+ window.history.replaceState(window.history.state, "", newUrl);
4262
+ }
4263
+ this.log("Checkout Champ bridge restored:", {
4264
+ had_vid: !!vid,
4265
+ had_fbc: !!fbc,
4266
+ had_fbp: !!fbp,
4267
+ had_fbclid: !!fbclid,
4268
+ had_gclid: !!gclid
4269
+ });
4270
+ }
4271
+ catch (error) {
4272
+ // Don't let bridge restoration block init — fall through to normal SDK
4273
+ // behavior (a fresh visitor_id, no restored click signals).
4274
+ this.log("restoreFromURL failed:", error);
4275
+ }
4276
+ }
4277
+ /**
4278
+ * Storefront → Checkout Champ link stamping. Finds every `<a href>` whose host
4279
+ * matches the configured CC domain list and appends `?_dl_vid=…&_dl_fbc=…&
4280
+ * _dl_fbp=…&_dl_fbclid=…&_dl_fbclid_at=…&_dl_gclid=…` so the user's
4281
+ * visitor_id + Meta click signals cross the domain. MutationObserver watches
4282
+ * for dynamically-injected links. On click, force-flush the event queue so
4283
+ * any pending track() events land before the browser navigates away.
4284
+ */
4285
+ syncOutboundLinkParams(domains) {
4286
+ if (typeof window === "undefined" || typeof document === "undefined")
4287
+ return;
4288
+ const lowerDomains = domains.map((d) => d.toLowerCase());
4289
+ const matchesCcDomain = (href) => {
4290
+ try {
4291
+ const host = new URL(href, window.location.href).hostname.toLowerCase();
4292
+ return lowerDomains.some((d) => host === d || host.endsWith("." + d));
4293
+ }
4294
+ catch (_a) {
4295
+ return false;
4296
+ }
4297
+ };
4298
+ const buildBridgeParams = () => {
4299
+ const attribution = this.attribution.getAttributionData();
4300
+ const out = {};
4301
+ const vid = this.identity.getAnonymousId();
4302
+ const fbc = this.cookies.get("_fbc") || attribution._fbc;
4303
+ const fbp = this.cookies.get("_fbp") || attribution._fbp;
4304
+ const fbclid = attribution.clickIdType === "fbclid" ? attribution.clickId : null;
4305
+ const fbclidAt = this.cookies.get("_dl_fbclid_at");
4306
+ const gclid = attribution.clickIdType === "gclid" ? attribution.clickId : null;
4307
+ const gclidAt = this.cookies.get("_dl_gclid_at");
4308
+ if (vid)
4309
+ out._dl_vid = vid;
4310
+ if (fbc)
4311
+ out._dl_fbc = String(fbc);
4312
+ if (fbp)
4313
+ out._dl_fbp = String(fbp);
4314
+ if (fbclid)
4315
+ out._dl_fbclid = String(fbclid);
4316
+ if (fbclidAt)
4317
+ out._dl_fbclid_at = String(fbclidAt);
4318
+ if (gclid)
4319
+ out._dl_gclid = String(gclid);
4320
+ if (gclidAt)
4321
+ out._dl_gclid_at = String(gclidAt);
4322
+ return out;
4323
+ };
4324
+ const stampLink = (anchor) => {
4325
+ if (!anchor.href || !matchesCcDomain(anchor.href))
4326
+ return;
4327
+ try {
4328
+ const u = new URL(anchor.href, window.location.href);
4329
+ const bridge = buildBridgeParams();
4330
+ let mutated = false;
4331
+ for (const [k, v] of Object.entries(bridge)) {
4332
+ if (!u.searchParams.get(k)) {
4333
+ u.searchParams.set(k, v);
4334
+ mutated = true;
4335
+ }
4336
+ }
4337
+ if (mutated)
4338
+ anchor.href = u.toString();
4339
+ }
4340
+ catch (_a) {
4341
+ // Ignore malformed URLs — don't break the page.
4342
+ }
4343
+ };
4344
+ const stampAll = () => {
4345
+ document.querySelectorAll("a[href]").forEach((el) => stampLink(el));
4346
+ };
4347
+ const onClick = (e) => {
4348
+ var _a, _b;
4349
+ const target = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest("a[href]");
4350
+ if (!target)
4351
+ return;
4352
+ const anchor = target;
4353
+ if (!matchesCcDomain(anchor.href))
4354
+ return;
4355
+ stampLink(anchor); // re-stamp in case attribution changed since DOMReady
4356
+ try {
4357
+ (_b = this.queue) === null || _b === void 0 ? void 0 : _b.flush();
4358
+ }
4359
+ catch (_c) {
4360
+ // Best-effort — never block navigation.
4361
+ }
4362
+ };
4363
+ stampAll();
4364
+ document.addEventListener("click", onClick, true);
4365
+ try {
4366
+ // Debounce re-stamping. A busy SPA (infinite scroll, animations,
4367
+ // React/Vue updates) can fire thousands of mutations per second; a naive
4368
+ // re-stamp on every notification would burn CPU pointlessly when
4369
+ // outbound link sets only change occasionally. 150ms is small enough to
4370
+ // catch links before the user can click them, large enough to coalesce
4371
+ // bursts.
4372
+ let restampTimer = null;
4373
+ const scheduleRestamp = () => {
4374
+ if (restampTimer != null)
4375
+ return;
4376
+ restampTimer = setTimeout(() => {
4377
+ restampTimer = null;
4378
+ stampAll();
4379
+ }, 150);
4380
+ };
4381
+ const observer = new MutationObserver(scheduleRestamp);
4382
+ observer.observe(document.documentElement, { childList: true, subtree: true });
4383
+ // Observer + click listener live for the session; pagehide cleans up on
4384
+ // full-page unload. SPA route changes are fine — we WANT stamping to keep
4385
+ // working across virtual navigations.
4386
+ window.addEventListener("pagehide", () => {
4387
+ try {
4388
+ observer.disconnect();
4389
+ }
4390
+ catch ( /* idempotent */_a) { /* idempotent */ }
4391
+ if (restampTimer != null)
4392
+ clearTimeout(restampTimer);
4393
+ document.removeEventListener("click", onClick, true);
4394
+ }, { once: true });
4395
+ }
4396
+ catch (error) {
4397
+ this.log("MutationObserver setup failed (CC link sync):", error);
4398
+ }
4399
+ this.log("Checkout Champ outbound-link sync active for:", lowerDomains);
4400
+ }
4139
4401
  /**
4140
4402
  * Create event payload
4141
4403
  */
@@ -4196,7 +4458,7 @@ var Datalyr = (function (exports) {
4196
4458
  resolution_method: 'browser_sdk',
4197
4459
  resolution_confidence: 1.0,
4198
4460
  // SDK metadata (keep in sync with package.json version)
4199
- sdk_version: '1.6.3',
4461
+ sdk_version: '1.6.4',
4200
4462
  sdk_name: 'datalyr-web-sdk'
4201
4463
  };
4202
4464
  return payload;