{"version":3,"file":"tracking-utils.mjs","sources":["../../src/tracking-utils.ts"],"sourcesContent":["/** Storefront API header for VisitToken */\nexport const SHOPIFY_VISIT_TOKEN_HEADER = 'X-Shopify-VisitToken';\n/** Storefront API header for UniqueToken */\nexport const SHOPIFY_UNIQUE_TOKEN_HEADER = 'X-Shopify-UniqueToken';\n\ntype TrackingValues = {\n  /** Identifier for the unique user. Equivalent to the deprecated _shopify_y cookie */\n  uniqueToken: string;\n  /** Identifier for the current visit. Equivalent to the deprecated _shopify_s cookie */\n  visitToken: string;\n  /** Represents the consent given by the user or the default region consent configured in Admin */\n  consent: string;\n};\n\n// Cache values to avoid losing them when performance\n// entries are cleared from the buffer over time.\nexport const cachedTrackingValues: {\n  current: null | TrackingValues;\n} = {current: null};\n\n/**\n * Retrieves user session tracking values for analytics\n * and marketing from the browser environment.\n */\nexport function getTrackingValues(): TrackingValues {\n  // Overall behavior: Tracking values are returned in Server-Timing headers from\n  // Storefront API responses, and we want to find and return these tracking values.\n  //\n  // Search recent fetches for SFAPI requests matching either: same origin (proxy case)\n  // or a subdomain of the current host (eg: checkout subdomain, if there is no proxy).\n  // We consider SF API-like endpoints (/api/.../graphql.json) on subdomains, as well as\n  // any same-origin request. The reason for the latter is that Hydrogen server collects\n  // tracking values and returns them in any non-cached response, not just direct SF API\n  // responses. For example, a cart mutation in a server action could return tracking values.\n  //\n  // If we didn't find tracking values in fetch requests, we fall back to checking cached values,\n  // then the initial page navigation entry, and finally the deprecated `_shopify_s` and `_shopify_y`.\n\n  let trackingValues: TrackingValues | undefined;\n\n  if (\n    typeof window !== 'undefined' &&\n    typeof window.performance !== 'undefined'\n  ) {\n    try {\n      // RE to extract host and optionally match SFAPI pathname.\n      // Group 1: host (e.g. \"checkout.mystore.com\")\n      // Group 2: SFAPI path if present (e.g. \"/api/2024-01/graphql.json\")\n      const resourceRE =\n        /^https?:\\/\\/([^/]+)(\\/api\\/(?:unstable|2\\d{3}-\\d{2})\\/graphql\\.json(?=$|\\?))?/;\n\n      // Search backwards through resource entries to find the most recent match.\n      // Match criteria (first one with _y and _s values wins):\n      // - Same origin (exact host match) with tracking values, OR\n      // - Subdomain + SFAPI pathname with tracking values\n      const entries = performance.getEntriesByType(\n        'resource',\n      ) as PerformanceResourceTiming[];\n\n      let matchedValues: ReturnType<typeof extractFromPerformanceEntry>;\n\n      for (let i = entries.length - 1; i >= 0; i--) {\n        const entry = entries[i];\n\n        if (entry.initiatorType !== 'fetch') continue;\n\n        const currentHost = window.location.host;\n        const match = entry.name.match(resourceRE);\n        if (!match) continue;\n\n        const [, matchedHost, sfapiPath] = match;\n\n        const isMatch =\n          // Same origin (exact host match)\n          matchedHost === currentHost ||\n          // Subdomain with SFAPI path\n          (sfapiPath && matchedHost?.endsWith(`.${currentHost}`));\n\n        if (isMatch) {\n          const values = extractFromPerformanceEntry(entry);\n          if (values) {\n            matchedValues = values;\n            break;\n          }\n        }\n      }\n\n      if (matchedValues) {\n        trackingValues = matchedValues;\n      }\n\n      // Resource entries have a limited buffer and are removed over time.\n      // Cache the latest values for future calls if we find them.\n      // A cached resource entry is always newer than a navigation entry.\n      if (trackingValues) {\n        cachedTrackingValues.current = trackingValues;\n      } else if (cachedTrackingValues.current) {\n        // Fallback to cached values from previous calls:\n        trackingValues = cachedTrackingValues.current;\n      }\n\n      if (!trackingValues) {\n        // Fallback to navigation entry from full page rendering load:\n        const navigationEntries = performance.getEntriesByType(\n          'navigation',\n        )[0] as PerformanceNavigationTiming;\n\n        // Navigation entries might omit consent when the Hydrogen server generates it.\n        // In this case, we skip consent requirement and only extract _y and _s values.\n        trackingValues = extractFromPerformanceEntry(navigationEntries, false);\n      }\n    } catch {}\n  }\n\n  // Fallback to deprecated cookies to support transitioning:\n  if (!trackingValues) {\n    const cookie =\n      // Read from arguments to avoid declaring parameters in this function signature.\n      // This logic is only used internally from `getShopifyCookies` and will be deprecated.\n      typeof arguments[0] === 'string'\n        ? arguments[0]\n        : typeof document !== 'undefined'\n          ? document.cookie\n          : '';\n\n    trackingValues = {\n      uniqueToken: cookie.match(/\\b_shopify_y=([^;]+)/)?.[1] || '',\n      visitToken: cookie.match(/\\b_shopify_s=([^;]+)/)?.[1] || '',\n      consent: cookie.match(/\\b_tracking_consent=([^;]+)/)?.[1] || '',\n    };\n  }\n\n  return trackingValues;\n}\n\nfunction extractFromPerformanceEntry(\n  entry: PerformanceNavigationTiming | PerformanceResourceTiming,\n  isConsentRequired = true,\n): TrackingValues | undefined {\n  let uniqueToken = '';\n  let visitToken = '';\n  let consent = '';\n\n  const serverTiming = entry.serverTiming;\n  // Quick check: we need at least 3 entries (_y, _s, _cmp)\n  if (serverTiming && serverTiming.length >= 3) {\n    // Iterate backwards since our headers are typically at the end\n    for (let i = serverTiming.length - 1; i >= 0; i--) {\n      const {name, description} = serverTiming[i];\n      if (!name || !description) continue;\n\n      if (name === '_y') {\n        uniqueToken = description;\n      } else if (name === '_s') {\n        visitToken = description;\n      } else if (name === '_cmp') {\n        // _cmp (consent management platform) holds the consent value\n        // used by consent-tracking-api and privacy-banner scripts.\n        consent = description;\n      }\n\n      if (uniqueToken && visitToken && consent) break;\n    }\n  }\n\n  return uniqueToken && visitToken && (isConsentRequired ? consent : true)\n    ? {uniqueToken, visitToken, consent}\n    : undefined;\n}\n"],"names":[],"mappings":"AACO,MAAM,6BAA6B;AAEnC,MAAM,8BAA8B;AAapC,MAAM,uBAET,EAAC,SAAS,KAAA;AAMP,SAAS,oBAAoC;AAvB7C;AAqCL,MAAI;AAEJ,MACE,OAAO,WAAW,eAClB,OAAO,OAAO,gBAAgB,aAC9B;AACA,QAAI;AAIF,YAAM,aACJ;AAMF,YAAM,UAAU,YAAY;AAAA,QAC1B;AAAA,MAAA;AAGF,UAAI;AAEJ,eAAS,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;AAC5C,cAAM,QAAQ,QAAQ,CAAC;AAEvB,YAAI,MAAM,kBAAkB,QAAS;AAErC,cAAM,cAAc,OAAO,SAAS;AACpC,cAAM,QAAQ,MAAM,KAAK,MAAM,UAAU;AACzC,YAAI,CAAC,MAAO;AAEZ,cAAM,CAAA,EAAG,aAAa,SAAS,IAAI;AAEnC,cAAM;AAAA;AAAA,UAEJ,gBAAgB;AAAA,UAEf,cAAa,2CAAa,SAAS,IAAI,WAAW;AAAA;AAErD,YAAI,SAAS;AACX,gBAAM,SAAS,4BAA4B,KAAK;AAChD,cAAI,QAAQ;AACV,4BAAgB;AAChB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI,eAAe;AACjB,yBAAiB;AAAA,MACnB;AAKA,UAAI,gBAAgB;AAClB,6BAAqB,UAAU;AAAA,MACjC,WAAW,qBAAqB,SAAS;AAEvC,yBAAiB,qBAAqB;AAAA,MACxC;AAEA,UAAI,CAAC,gBAAgB;AAEnB,cAAM,oBAAoB,YAAY;AAAA,UACpC;AAAA,QAAA,EACA,CAAC;AAIH,yBAAiB,4BAA4B,mBAAmB,KAAK;AAAA,MACvE;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAGA,MAAI,CAAC,gBAAgB;AACnB,UAAM;AAAA;AAAA;AAAA,MAGJ,OAAO,UAAU,CAAC,MAAM,WACpB,UAAU,CAAC,IACX,OAAO,aAAa,cAClB,SAAS,SACT;AAAA;AAER,qBAAiB;AAAA,MACf,eAAa,YAAO,MAAM,sBAAsB,MAAnC,mBAAuC,OAAM;AAAA,MAC1D,cAAY,YAAO,MAAM,sBAAsB,MAAnC,mBAAuC,OAAM;AAAA,MACzD,WAAS,YAAO,MAAM,6BAA6B,MAA1C,mBAA8C,OAAM;AAAA,IAAA;AAAA,EAEjE;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,OACA,oBAAoB,MACQ;AAC5B,MAAI,cAAc;AAClB,MAAI,aAAa;AACjB,MAAI,UAAU;AAEd,QAAM,eAAe,MAAM;AAE3B,MAAI,gBAAgB,aAAa,UAAU,GAAG;AAE5C,aAAS,IAAI,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;AACjD,YAAM,EAAC,MAAM,gBAAe,aAAa,CAAC;AAC1C,UAAI,CAAC,QAAQ,CAAC,YAAa;AAE3B,UAAI,SAAS,MAAM;AACjB,sBAAc;AAAA,MAChB,WAAW,SAAS,MAAM;AACxB,qBAAa;AAAA,MACf,WAAW,SAAS,QAAQ;AAG1B,kBAAU;AAAA,MACZ;AAEA,UAAI,eAAe,cAAc,QAAS;AAAA,IAC5C;AAAA,EACF;AAEA,SAAO,eAAe,eAAe,oBAAoB,UAAU,QAC/D,EAAC,aAAa,YAAY,QAAA,IAC1B;AACN;"}