@datalyr/web 1.5.0 → 1.6.1

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @datalyr/web v1.5.0
2
+ * @datalyr/web v1.6.1
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2026 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -1504,10 +1504,13 @@ class AttributionManager {
1504
1504
  // Optionally set the cookie for future use
1505
1505
  cookies.set('_fbp', adCookies._fbp, 90);
1506
1506
  }
1507
- // Generate _fbc if we have fbclid but no _fbc
1507
+ // Generate _fbc if we have fbclid but no _fbc.
1508
+ // Meta's fbc format is `fb.{subdomainIndex}.{creationTime}.{fbclid}` where
1509
+ // creationTime is UNIX time in MILLISECONDS (matches the _fbp generation above
1510
+ // and the real _fbc cookie the Meta Pixel writes). Do NOT use seconds here.
1508
1511
  const fbclid = this.getCurrentFbclid();
1509
1512
  if (fbclid && !adCookies._fbc) {
1510
- const timestamp = Math.floor(Date.now() / 1000);
1513
+ const timestamp = Date.now();
1511
1514
  adCookies._fbc = `fb.1.${timestamp}.${fbclid}`;
1512
1515
  // Optionally set the cookie for future use
1513
1516
  cookies.set('_fbc', adCookies._fbc, 90);
@@ -2672,9 +2675,12 @@ class ContainerManager {
2672
2675
  s = b.getElementsByTagName(e)[0];
2673
2676
  s.parentNode.insertBefore(t, s);
2674
2677
  })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2675
- // Initialize pixel
2678
+ // Initialize pixel only — do NOT fire PageView here. The SDK's own pageview
2679
+ // tracking (track('pageview')) routes through trackToPixels and fires a single
2680
+ // mapped PageView with a shared eventID. Firing it again here produced TWO
2681
+ // PageViews per load (one un-deduped). Note: if the host app disables pageview
2682
+ // tracking entirely, no PageView is sent — which is the correct outcome.
2676
2683
  window.fbq('init', config.pixel_id);
2677
- window.fbq('track', 'PageView');
2678
2684
  this.log('Meta Pixel initialized:', config.pixel_id);
2679
2685
  }
2680
2686
  catch (error) {
@@ -2756,22 +2762,63 @@ class ContainerManager {
2756
2762
  /**
2757
2763
  * Track event to all initialized pixels
2758
2764
  */
2759
- trackToPixels(eventName, properties = {}) {
2760
- var _a, _b, _c, _e, _f, _g;
2765
+ trackToPixels(eventName, properties = {}, eventId) {
2766
+ var _a, _b, _c, _e, _f, _g, _h, _j;
2767
+ // Datalyr-internal events ($identify, $group, $alias, $auto_identify,
2768
+ // $app_download_click, …) are not conversions — never forward them to ad
2769
+ // pixels. (Previously they fired as noise custom events with the $ stripped.)
2770
+ if (eventName.startsWith('$'))
2771
+ return;
2761
2772
  // Sanitize inputs to prevent XSS
2762
2773
  const sanitizedEventName = this.sanitizeEventName(eventName);
2763
2774
  const sanitizedProperties = this.sanitizeProperties(properties);
2764
2775
  // Track to Meta Pixel
2765
2776
  if (((_b = (_a = this.pixels) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.enabled) && window.fbq) {
2766
2777
  try {
2767
- window.fbq('track', sanitizedEventName, sanitizedProperties);
2778
+ // Map our event name to Meta's standard event vocabulary so the browser
2779
+ // Pixel fires e.g. "Purchase", not "purchase". This MUST match the
2780
+ // platform_standard_event the postback worker sends server-side, or Meta
2781
+ // won't dedupe (dedup = event_name + event_id). These defaults mirror the
2782
+ // server-side auto-detect map; custom rule choices may not line up.
2783
+ const metaEventMap = {
2784
+ page_view: 'PageView', pageview: 'PageView',
2785
+ view_content: 'ViewContent', product_viewed: 'ViewContent', view_item: 'ViewContent',
2786
+ add_to_cart: 'AddToCart', product_added: 'AddToCart',
2787
+ add_to_wishlist: 'AddToWishlist',
2788
+ initiate_checkout: 'InitiateCheckout', begin_checkout: 'InitiateCheckout', checkout_started: 'InitiateCheckout',
2789
+ add_payment_info: 'AddPaymentInfo',
2790
+ purchase: 'Purchase', order_completed: 'Purchase', order_paid: 'Purchase',
2791
+ lead: 'Lead',
2792
+ complete_registration: 'CompleteRegistration', sign_up: 'CompleteRegistration', signup: 'CompleteRegistration',
2793
+ search: 'Search',
2794
+ subscribe: 'Subscribe', subscription_created: 'Subscribe',
2795
+ start_trial: 'StartTrial', trial_started: 'StartTrial',
2796
+ contact: 'Contact',
2797
+ schedule: 'Schedule',
2798
+ };
2799
+ // Source of truth: the workspace's Meta conversion-rule map (from
2800
+ // /container-scripts, keyed by the exact trigger event name) — this is what
2801
+ // the server-side CAPI sends, so it guarantees event_name dedup alignment.
2802
+ // Fall back to the static default map, then the sanitized raw name.
2803
+ const ruleEventMap = (_e = (_c = this.pixels) === null || _c === void 0 ? void 0 : _c.meta) === null || _e === void 0 ? void 0 : _e.event_mappings;
2804
+ const metaEvent = (ruleEventMap === null || ruleEventMap === void 0 ? void 0 : ruleEventMap[eventName])
2805
+ || metaEventMap[String(eventName).toLowerCase()]
2806
+ || sanitizedEventName;
2807
+ // Pass the shared eventID so this Pixel event dedupes against the
2808
+ // server-side CAPI event carrying the same event_id.
2809
+ if (eventId) {
2810
+ window.fbq('track', metaEvent, sanitizedProperties, { eventID: eventId });
2811
+ }
2812
+ else {
2813
+ window.fbq('track', metaEvent, sanitizedProperties);
2814
+ }
2768
2815
  }
2769
2816
  catch (error) {
2770
2817
  this.log('Error tracking Meta Pixel event:', error);
2771
2818
  }
2772
2819
  }
2773
2820
  // Track to Google Tag
2774
- if (((_e = (_c = this.pixels) === null || _c === void 0 ? void 0 : _c.google) === null || _e === void 0 ? void 0 : _e.enabled) && window.gtag) {
2821
+ if (((_g = (_f = this.pixels) === null || _f === void 0 ? void 0 : _f.google) === null || _g === void 0 ? void 0 : _g.enabled) && window.gtag) {
2775
2822
  try {
2776
2823
  window.gtag('event', sanitizedEventName, sanitizedProperties);
2777
2824
  }
@@ -2780,7 +2827,7 @@ class ContainerManager {
2780
2827
  }
2781
2828
  }
2782
2829
  // Track to TikTok Pixel
2783
- if (((_g = (_f = this.pixels) === null || _f === void 0 ? void 0 : _f.tiktok) === null || _g === void 0 ? void 0 : _g.enabled) && window.ttq) {
2830
+ if (((_j = (_h = this.pixels) === null || _h === void 0 ? void 0 : _h.tiktok) === null || _j === void 0 ? void 0 : _j.enabled) && window.ttq) {
2784
2831
  try {
2785
2832
  // Map common events to TikTok names
2786
2833
  const tiktokEventMap = {
@@ -3488,6 +3535,15 @@ class Datalyr {
3488
3535
  this.identify(email, { email });
3489
3536
  });
3490
3537
  }
3538
+ // Stamp attribution signals into the Shopify cart (OPT-IN, default off).
3539
+ // Lets server-side order webhooks recover the browser visitor + Meta click
3540
+ // signals (the postback webhook reads these as note_attributes). Inert unless
3541
+ // enabled; best-effort and never blocks init.
3542
+ if (this.config.shopifyCartAttributes === true) {
3543
+ this.syncShopifyCartAttributes().catch((error) => {
3544
+ this.log('Shopify cart attribute sync failed:', error);
3545
+ });
3546
+ }
3491
3547
  // Track initial page view if enabled (AFTER encryption ready)
3492
3548
  if (this.config.trackPageViews) {
3493
3549
  this.page();
@@ -3529,13 +3585,19 @@ class Datalyr {
3529
3585
  // PRIVACY: Heavy fingerprinting removed - using minimal fingerprinting only
3530
3586
  // Update session activity
3531
3587
  this.session.updateActivity(eventName);
3588
+ // Generate the event_id ONCE here so the same value is used for both the
3589
+ // ingested event (→ CAPI dedup key in the postback worker) and the browser
3590
+ // Meta Pixel co-fire below. Sharing it is what lets Meta dedupe the Pixel
3591
+ // event against the server-side CAPI event (dedup = event_id + event_name).
3592
+ const eventId = generateUUID();
3532
3593
  // Create event payload
3533
- const payload = this.createEventPayload(eventName, properties);
3594
+ const payload = this.createEventPayload(eventName, properties, eventId);
3534
3595
  // Queue event
3535
3596
  this.queue.enqueue(payload);
3536
- // Track to third-party pixels if container is initialized
3597
+ // Track to third-party pixels if container is initialized.
3598
+ // Pass the shared eventId so the Meta Pixel fires with the same { eventID }.
3537
3599
  if (this.container) {
3538
- this.container.trackToPixels(eventName, properties);
3600
+ this.container.trackToPixels(eventName, properties, eventId);
3539
3601
  }
3540
3602
  // Call plugin handlers
3541
3603
  if (this.config.plugins) {
@@ -3926,10 +3988,61 @@ class Datalyr {
3926
3988
  getSuperProperties() {
3927
3989
  return Object.assign({}, this.superProperties);
3928
3990
  }
3991
+ /**
3992
+ * Stamp Datalyr attribution signals into the Shopify cart as cart attributes.
3993
+ * Cart attributes become `order.note_attributes` with the same names, which the
3994
+ * server-side order webhook reads to recover the browser visitor_id + Meta click
3995
+ * signals (_fbc/_fbp/fbclid) — enabling accurate Meta CAPI attribution for orders.
3996
+ *
3997
+ * Opt-in (config.shopifyCartAttributes), best-effort, runs once during init.
3998
+ * Merges via Shopify's /cart/update.js (does not clobber other cart attributes).
3999
+ */
4000
+ syncShopifyCartAttributes() {
4001
+ return __awaiter(this, void 0, void 0, function* () {
4002
+ if (typeof window === "undefined" || typeof document === "undefined")
4003
+ return;
4004
+ const isShopify = !!(window.Shopify ||
4005
+ document.querySelector('meta[name="shopify-checkout-api-token"]') ||
4006
+ window.location.hostname.includes(".myshopify.com"));
4007
+ if (!isShopify)
4008
+ return;
4009
+ const attribution = this.attribution.getAttributionData();
4010
+ const fbclid = attribution.clickIdType === "fbclid" ? attribution.clickId : null;
4011
+ // Cookies are freshest (the real Meta Pixel may have just written them);
4012
+ // fall back to whatever the SDK captured in attribution.
4013
+ const attributes = {};
4014
+ const visitorId = this.identity.getAnonymousId();
4015
+ const fbc = this.cookies.get("_fbc") || attribution._fbc;
4016
+ const fbp = this.cookies.get("_fbp") || attribution._fbp;
4017
+ if (visitorId)
4018
+ attributes._datalyr_visitor_id = visitorId;
4019
+ if (fbc)
4020
+ attributes._datalyr_fbc = String(fbc);
4021
+ if (fbp)
4022
+ attributes._datalyr_fbp = String(fbp);
4023
+ if (fbclid)
4024
+ attributes._datalyr_fbclid = String(fbclid);
4025
+ if (Object.keys(attributes).length === 0)
4026
+ return;
4027
+ try {
4028
+ yield fetch("/cart/update.js", {
4029
+ method: "POST",
4030
+ headers: { "Content-Type": "application/json" },
4031
+ credentials: "same-origin",
4032
+ body: JSON.stringify({ attributes }),
4033
+ });
4034
+ this.log("Shopify cart attributes stamped:", Object.keys(attributes));
4035
+ }
4036
+ catch (error) {
4037
+ // Never let cart sync affect tracking — swallow (e.g. no cart yet / CSP).
4038
+ this.log("Shopify /cart/update.js failed:", error);
4039
+ }
4040
+ });
4041
+ }
3929
4042
  /**
3930
4043
  * Create event payload
3931
4044
  */
3932
- createEventPayload(eventName, properties) {
4045
+ createEventPayload(eventName, properties, eventIdArg) {
3933
4046
  // Sanitize and merge properties
3934
4047
  const sanitizedProperties = sanitizeEventData(properties);
3935
4048
  const eventData = deepMerge({}, this.superProperties, sanitizedProperties);
@@ -3959,7 +4072,9 @@ class Datalyr {
3959
4072
  });
3960
4073
  // Create payload using snake_case only (matches backend API and production script)
3961
4074
  const identityFields = this.identity.getIdentityFields();
3962
- const eventId = generateUUID();
4075
+ // Use the caller-provided event_id (shared with the Meta Pixel co-fire for
4076
+ // dedup); fall back to a fresh UUID for any direct caller that omits it.
4077
+ const eventId = eventIdArg !== null && eventIdArg !== void 0 ? eventIdArg : generateUUID();
3963
4078
  // Ensure we have all required identity fields
3964
4079
  const distinctId = identityFields.distinct_id;
3965
4080
  const anonymousId = identityFields.anonymous_id;
@@ -3983,8 +4098,8 @@ class Datalyr {
3983
4098
  // Resolution metadata
3984
4099
  resolution_method: 'browser_sdk',
3985
4100
  resolution_confidence: 1.0,
3986
- // SDK metadata
3987
- sdk_version: '1.4.1',
4101
+ // SDK metadata (keep in sync with package.json version)
4102
+ sdk_version: '1.6.0',
3988
4103
  sdk_name: 'datalyr-web-sdk'
3989
4104
  };
3990
4105
  return payload;
@@ -4204,5 +4319,5 @@ if (typeof window !== 'undefined') {
4204
4319
  window.datalyr = datalyr;
4205
4320
  }
4206
4321
 
4207
- export { datalyr as default };
4322
+ export { datalyr, datalyr as default };
4208
4323
  //# sourceMappingURL=datalyr.esm.js.map