{"version":3,"file":"tracking-utils.mjs","names":[],"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 and marketing from the browser environment.\n * @publicDocs\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"],"mappings":";;AACA,IAAa,6BAA6B;;AAE1C,IAAa,8BAA8B;AAa3C,IAAa,uBAET,EAAC,SAAS,MAAK;;;;;AAMnB,SAAgB,oBAAoC;CAclD,IAAI;AAEJ,KACE,OAAO,WAAW,eAClB,OAAO,OAAO,gBAAgB,YAE9B,KAAI;EAIF,MAAM,aACJ;EAMF,MAAM,UAAU,YAAY,iBAC1B,WACD;EAED,IAAI;AAEJ,OAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GAC5C,MAAM,QAAQ,QAAQ;AAEtB,OAAI,MAAM,kBAAkB,QAAS;GAErC,MAAM,cAAc,OAAO,SAAS;GACpC,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW;AAC1C,OAAI,CAAC,MAAO;GAEZ,MAAM,GAAG,aAAa,aAAa;AAQnC,OAJE,gBAAgB,eAEf,aAAa,aAAa,SAAS,IAAI,cAAc,EAE3C;IACX,MAAM,SAAS,4BAA4B,MAAM;AACjD,QAAI,QAAQ;AACV,qBAAgB;AAChB;;;;AAKN,MAAI,cACF,kBAAiB;AAMnB,MAAI,eACF,sBAAqB,UAAU;WACtB,qBAAqB,QAE9B,kBAAiB,qBAAqB;AAGxC,MAAI,CAAC,gBAAgB;GAEnB,MAAM,oBAAoB,YAAY,iBACpC,aACD,CAAC;AAIF,oBAAiB,4BAA4B,mBAAmB,MAAM;;SAElE;AAIV,KAAI,CAAC,gBAAgB;EACnB,MAAM,SAGJ,OAAO,UAAU,OAAO,WACpB,UAAU,KACV,OAAO,aAAa,cAClB,SAAS,SACT;AAER,mBAAiB;GACf,aAAa,OAAO,MAAM,uBAAuB,GAAG,MAAM;GAC1D,YAAY,OAAO,MAAM,uBAAuB,GAAG,MAAM;GACzD,SAAS,OAAO,MAAM,8BAA8B,GAAG,MAAM;GAC9D;;AAGH,QAAO;;AAGT,SAAS,4BACP,OACA,oBAAoB,MACQ;CAC5B,IAAI,cAAc;CAClB,IAAI,aAAa;CACjB,IAAI,UAAU;CAEd,MAAM,eAAe,MAAM;AAE3B,KAAI,gBAAgB,aAAa,UAAU,EAEzC,MAAK,IAAI,IAAI,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;EACjD,MAAM,EAAC,MAAM,gBAAe,aAAa;AACzC,MAAI,CAAC,QAAQ,CAAC,YAAa;AAE3B,MAAI,SAAS,KACX,eAAc;WACL,SAAS,KAClB,cAAa;WACJ,SAAS,OAGlB,WAAU;AAGZ,MAAI,eAAe,cAAc,QAAS;;AAI9C,QAAO,eAAe,eAAe,oBAAoB,UAAU,QAC/D;EAAC;EAAa;EAAY;EAAQ,GAClC,KAAA"}