@datalyr/web 1.6.4 → 1.7.0

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.4
2
+ * @datalyr/web v1.7.0
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -2403,9 +2403,10 @@ var Datalyr = (function (exports) {
2403
2403
  throw new Error(`Failed to fetch container scripts: ${response.status}`);
2404
2404
  }
2405
2405
  const data = yield response.json();
2406
- // Store scripts and pixels
2406
+ // Store scripts, pixels, and the SDK runtime config envelope.
2407
2407
  this.scripts = data.scripts || [];
2408
2408
  this.pixels = data.pixels || null;
2409
+ this.remoteConfig = (data.config && typeof data.config === 'object') ? data.config : undefined;
2409
2410
  // Initialize pixels if configured. Awaited so advanced-matching hashes
2410
2411
  // are resolved before the first dl.track() flushes through trackToPixels
2411
2412
  // (otherwise fbq('init') would lag fbq('track') in fast-path tracks).
@@ -2435,6 +2436,13 @@ var Datalyr = (function (exports) {
2435
2436
  }
2436
2437
  });
2437
2438
  }
2439
+ /**
2440
+ * The SDK runtime config delivered by /container-scripts, or undefined if the
2441
+ * response omitted it. The SDK merges this under explicit init() options.
2442
+ */
2443
+ getRemoteConfig() {
2444
+ return this.remoteConfig;
2445
+ }
2438
2446
  /**
2439
2447
  * Load scripts by trigger type
2440
2448
  */
@@ -3092,7 +3100,7 @@ var Datalyr = (function (exports) {
3092
3100
  this.config = {
3093
3101
  enabled: config.enabled !== false,
3094
3102
  captureFromForms: config.captureFromForms !== false,
3095
- captureFromAPI: config.captureFromAPI !== false,
3103
+ captureFromAPI: config.captureFromAPI === true, // default OFF: same-origin response-scan can mis-identify (e.g. admin views)
3096
3104
  captureFromShopify: config.captureFromShopify !== false,
3097
3105
  trustedDomains: config.trustedDomains || [],
3098
3106
  debug: config.debug || false
@@ -3501,12 +3509,57 @@ var Datalyr = (function (exports) {
3501
3509
  }
3502
3510
  }
3503
3511
 
3512
+ /** Keys the remote config is allowed to fill on DatalyrConfig. */
3513
+ const REMOTE_KEYS = [
3514
+ 'autoIdentify',
3515
+ 'autoIdentifyForms',
3516
+ 'autoIdentifyAPI',
3517
+ 'autoIdentifyShopify',
3518
+ 'shopifyCartAttributes',
3519
+ 'checkoutChampDomains',
3520
+ 'respectGlobalPrivacyControl',
3521
+ 'respectDoNotTrack',
3522
+ 'privacyMode',
3523
+ ];
3524
+ /**
3525
+ * Fold `remote` into `config` IN PLACE. Precedence per key:
3526
+ * explicit init() value > remote (dashboard) > built-in default
3527
+ *
3528
+ * `explicitKeys` = the keys the CALLER passed to init() (before built-in
3529
+ * defaults were merged in). For any remote key NOT in that set, remote
3530
+ * OVERRIDES the built-in default. This is essential because some keys
3531
+ * (respectDoNotTrack, respectGlobalPrivacyControl, privacyMode) get a built-in
3532
+ * default at init() and are therefore never `undefined` — a naive
3533
+ * "fill-if-undefined" would silently ignore the dashboard for them.
3534
+ *
3535
+ * No-op when there's no remote config (older worker / container disabled /
3536
+ * failed fetch) — built-in defaults then stand.
3537
+ */
3538
+ function applyRemoteConfig(config, remote, explicitKeys) {
3539
+ if (!remote)
3540
+ return;
3541
+ const target = config;
3542
+ for (const key of REMOTE_KEYS) {
3543
+ const remoteVal = remote[key];
3544
+ if (remoteVal === undefined || remoteVal === null)
3545
+ continue;
3546
+ // Explicit init() always wins; otherwise remote overrides the built-in default.
3547
+ if (explicitKeys === null || explicitKeys === void 0 ? void 0 : explicitKeys.has(key))
3548
+ continue;
3549
+ target[key] = remoteVal;
3550
+ }
3551
+ }
3552
+
3504
3553
  /**
3505
3554
  * Datalyr Web SDK
3506
3555
  * Modern attribution tracking for web applications
3507
3556
  */
3508
3557
  class Datalyr {
3509
3558
  constructor() {
3559
+ // Keys the caller passed to init() (before built-in defaults were merged).
3560
+ // Lets the remote-config merge override built-in defaults while always
3561
+ // deferring to an explicit init() value. See applyRemoteConfig.
3562
+ this.explicitConfigKeys = new Set();
3510
3563
  this.superProperties = {};
3511
3564
  this.userProperties = {};
3512
3565
  this.optedOut = false;
@@ -3529,6 +3582,10 @@ var Datalyr = (function (exports) {
3529
3582
  if (!config.workspaceId) {
3530
3583
  throw new Error('[Datalyr] workspaceId is required');
3531
3584
  }
3585
+ // Snapshot the keys the caller explicitly set, BEFORE built-in defaults are
3586
+ // merged in — so the remote-config merge can override built-in defaults
3587
+ // while still deferring to anything the caller passed explicitly.
3588
+ this.explicitConfigKeys = new Set(Object.keys(config));
3532
3589
  // Set default config values
3533
3590
  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
3591
  // platform:'shopify' implies cart-attribute sync: stamp visitor_id + Meta
@@ -3605,6 +3662,7 @@ var Datalyr = (function (exports) {
3605
3662
  return this.initializationPromise;
3606
3663
  }
3607
3664
  this.initializationPromise = (() => __awaiter(this, void 0, void 0, function* () {
3665
+ var _a;
3608
3666
  try {
3609
3667
  // SEC-03 Fix: Initialize encryption for PII data
3610
3668
  const deviceId = this.identity.getAnonymousId();
@@ -3639,6 +3697,26 @@ var Datalyr = (function (exports) {
3639
3697
  yield this.container.init().catch(error => {
3640
3698
  this.log('Container initialization failed:', error);
3641
3699
  });
3700
+ // Checkout Champ thank-you/upsell page: co-fire the browser Meta Pixel
3701
+ // Purchase with the SAME deterministic event_id the CC webhook stamps
3702
+ // server-side, so Meta dedupes the browser event against the server-side
3703
+ // CAPI event (dedup = event_name + event_id). Pixel-only — the CC Export
3704
+ // Profile webhook owns the server event + CAPI postback. No-op anywhere
3705
+ // except a post-purchase page (keyed on CC's sessionStorage orderData).
3706
+ // Must run AFTER container.init() so fbq is loaded + init'd.
3707
+ if (this.config.platform === 'checkoutchamp') {
3708
+ this.fireCheckoutChampPurchasePixel();
3709
+ }
3710
+ }
3711
+ // Fold the remote /container-scripts config UNDER explicit init()
3712
+ // overrides (precedence: defaults <- remote <- explicit). No-op if the
3713
+ // container is disabled or the worker didn't send `config`. This is what
3714
+ // makes the dashboard toggles take effect with no snippet edits.
3715
+ applyRemoteConfig(this.config, (_a = this.container) === null || _a === void 0 ? void 0 : _a.getRemoteConfig(), this.explicitConfigKeys);
3716
+ // Privacy-strict turns auto-identify (email capture) OFF regardless of
3717
+ // the dashboard/remote value — privacy wins over the bridge.
3718
+ if (this.config.privacyMode === 'strict') {
3719
+ this.config.autoIdentify = false;
3642
3720
  }
3643
3721
  // autoIdentify default for the CC bridge:
3644
3722
  // - platform === 'checkoutchamp' (CC funnel pages) OR
@@ -3650,8 +3728,10 @@ var Datalyr = (function (exports) {
3650
3728
  if (ccBridgeActive && this.config.autoIdentify === undefined) {
3651
3729
  this.config.autoIdentify = true;
3652
3730
  }
3653
- // Initialize auto-identify if explicitly enabled (opt-in)
3654
- if (this.config.autoIdentify === true) {
3731
+ // Initialize auto-identify when enabled (explicit or remote) AND
3732
+ // tracking is allowed. The shouldTrack() gate keeps capture from even
3733
+ // setting up its form/API interceptors for opted-out / DNT / GPC users.
3734
+ if (this.config.autoIdentify === true && this.shouldTrack()) {
3655
3735
  this.autoIdentify = new AutoIdentifyManager({
3656
3736
  enabled: true,
3657
3737
  captureFromForms: this.config.autoIdentifyForms,
@@ -3677,7 +3757,7 @@ var Datalyr = (function (exports) {
3677
3757
  // Lets server-side order webhooks recover the browser visitor + Meta click
3678
3758
  // signals (the postback webhook reads these as note_attributes). Inert unless
3679
3759
  // enabled; best-effort and never blocks init.
3680
- if (this.config.shopifyCartAttributes === true) {
3760
+ if (this.config.shopifyCartAttributes === true && this.shouldTrack()) {
3681
3761
  this.syncShopifyCartAttributes().catch((error) => {
3682
3762
  this.log('Shopify cart attribute sync failed:', error);
3683
3763
  });
@@ -4274,6 +4354,93 @@ var Datalyr = (function (exports) {
4274
4354
  this.log("restoreFromURL failed:", error);
4275
4355
  }
4276
4356
  }
4357
+ /**
4358
+ * Checkout Champ Meta Pixel ⇄ CAPI deduplication.
4359
+ *
4360
+ * On a CC thank-you / upsell page, fire the BROWSER Meta Pixel Purchase with the
4361
+ * exact same event_id the CC webhook stamps server-side, so Meta collapses the
4362
+ * browser Pixel event and the server-side CAPI event into one (dedup key =
4363
+ * event_name + event_id). This gives the EMQ lift of a matched browser+server
4364
+ * event without double-counting conversions in Ads Manager.
4365
+ *
4366
+ * PIXEL-ONLY by design. We do NOT enqueue a server event here: the CC Export
4367
+ * Profile webhook (webhooks/platforms/checkoutchamp.js) already ingests the
4368
+ * purchase and fires CAPI. Calling track() here would create a second server
4369
+ * event (source='web') AND double-fire CAPI — defeating the whole point.
4370
+ *
4371
+ * The event_id MUST stay byte-identical to the server formula:
4372
+ * webhooks/platforms/checkoutchamp.js:250
4373
+ * generateEventId('checkoutchamp', `${event_type}_${order_id}`)
4374
+ * webhooks/core/ingest.js:122 → `${platform}_${webhookEventId}`
4375
+ * ⇒ `checkoutchamp_purchase_<order_id>`
4376
+ * where <order_id> is the value CC posts to the webhook via its [orderId] macro.
4377
+ * We read the browser-side counterpart from CC's own client-side order object:
4378
+ * JSON.parse(sessionStorage.getItem('orderData')).orderId
4379
+ * (CC docs: referenced in custom scripts as `orderDataTmp.orderId`).
4380
+ *
4381
+ * ASSUMPTION TO VERIFY ON A REAL CC TEST ORDER: that sessionStorage
4382
+ * orderData.orderId === the [orderId] CC sends to the postback. If a merchant's
4383
+ * CC plan exposes a different id client-side, the two event_ids won't match and
4384
+ * Meta will show duplicates — caught by the test order in the setup checklist.
4385
+ *
4386
+ * v1 scope: the primary order only. Per-upsell Pixel dedup (each upsell is its
4387
+ * own order_id + parent_order_id server-side) needs the upsell sessionStorage
4388
+ * shape confirmed on a live funnel first — tracked as a follow-up.
4389
+ */
4390
+ fireCheckoutChampPurchasePixel() {
4391
+ var _a;
4392
+ if (typeof window === "undefined")
4393
+ return;
4394
+ try {
4395
+ // Needs the container (where the Meta Pixel lives). trackToPixels itself
4396
+ // no-ops unless a Meta pixel is enabled + fbq is present, so an
4397
+ // unconfigured workspace silently does nothing.
4398
+ if (!this.container)
4399
+ return;
4400
+ const raw = (_a = window.sessionStorage) === null || _a === void 0 ? void 0 : _a.getItem("orderData");
4401
+ if (!raw)
4402
+ return; // not a post-purchase page — nothing to dedupe
4403
+ const order = JSON.parse(raw) || {};
4404
+ const orderId = order.orderId;
4405
+ if (!orderId)
4406
+ return;
4407
+ // orderData persists across upsell page loads — fire the primary Purchase
4408
+ // Pixel once per order. (Meta also dedupes by event_id over 48h, so this is
4409
+ // belt-and-suspenders against a per-page re-fire.)
4410
+ const guardKey = `__dl_cc_purchase_${orderId}`;
4411
+ if (window.sessionStorage.getItem(guardKey))
4412
+ return;
4413
+ window.sessionStorage.setItem(guardKey, "1");
4414
+ // KEEP IN SYNC with the server formula above.
4415
+ const eventId = `checkoutchamp_purchase_${orderId}`;
4416
+ // value/currency are for EMQ quality only — Meta dedup is event_id+name, so
4417
+ // a mismatch here never breaks dedup. Best-effort parse of CC's fields.
4418
+ const value = Number(order.totalAmount);
4419
+ const properties = {
4420
+ order_id: String(orderId),
4421
+ content_type: "product",
4422
+ };
4423
+ if (Number.isFinite(value))
4424
+ properties.value = value;
4425
+ const currency = order.currencyCode || order.currency;
4426
+ if (currency)
4427
+ properties.currency = String(currency);
4428
+ if (order.productId)
4429
+ properties.content_ids = [String(order.productId)];
4430
+ // Pixel-only co-fire. 'purchase' maps to Meta 'Purchase' in trackToPixels,
4431
+ // matching the server-side rule's platform_event_name.
4432
+ this.container.trackToPixels("purchase", properties, eventId);
4433
+ this.log("Checkout Champ Purchase Pixel co-fired (CAPI dedup):", {
4434
+ eventId,
4435
+ value: properties.value,
4436
+ currency: properties.currency,
4437
+ });
4438
+ }
4439
+ catch (error) {
4440
+ // Never let dedup co-fire break the page or init.
4441
+ this.log("fireCheckoutChampPurchasePixel failed:", error);
4442
+ }
4443
+ }
4277
4444
  /**
4278
4445
  * Storefront → Checkout Champ link stamping. Finds every `<a href>` whose host
4279
4446
  * matches the configured CC domain list and appends `?_dl_vid=…&_dl_fbc=…&
@@ -4458,7 +4625,7 @@ var Datalyr = (function (exports) {
4458
4625
  resolution_method: 'browser_sdk',
4459
4626
  resolution_confidence: 1.0,
4460
4627
  // SDK metadata (keep in sync with package.json version)
4461
- sdk_version: '1.6.4',
4628
+ sdk_version: '1.6.5',
4462
4629
  sdk_name: 'datalyr-web-sdk'
4463
4630
  };
4464
4631
  return payload;