{"version":3,"file":"product-detail-hair-care-section.min.js","mappings":"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","sources":["webpack://lange/./lib/perf.ts","webpack://lange/./lib/growthbook-feature.ts","webpack://lange/./sections/product-detail-hair-care/index.ts","webpack://lange/./lib/dom-ready.ts"],"sourcesContent":["/**\n * Returns a timestamp in milliseconds for measuring durations.\n *\n * This decorates performance.now().\n *\n * This tries to use the native high-resolution epoch, falling back to Date.now().\n *\n * If native throws, this still returns a value, but it logs a message as a side effect.\n *\n * This happens in a try/catch, so it is safe.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now\n */\nexport function now() {\n  try {\n    if (typeof performance?.now === 'function') {\n      return performance.now();\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n\n  return Date.now();\n}\n\n/**\n * Records a named point on the User Timing timeline in devtools.\n *\n * This decorates performance.mark within a try/catch that suppresses errors.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark\n */\nexport function mark(name: string) {\n  try {\n    if (typeof performance?.mark === 'function') {\n      performance.mark(name);\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n\n/**\n * Records a named span between two timestamps on the User Timing timeline in devtools.\n *\n * This decorates performance.measure within a try/catch that suppresses errors.\n *\n * The bounds are explicit timestamps rather than named marks, which avoids the error the marked\n * name form throws when a start mark was never recorded.\n *\n * The detail param is optional and must be structured-cloneable. It should be a plain object. If\n * the value cannot be cloned, then performance.measure throws, which is caught and logged by this\n * wrapper. If specified, the detail shows up in the tooltip in the devtools panel.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure\n */\nexport function measure(name: string, start: number, end: number, detail?: unknown) {\n  try {\n    if (typeof performance?.measure === 'function') {\n      performance.measure(name, { start, end, detail });\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n","import { measure, now } from './perf.js';\n\n/**\n * Waits for the runtime's load to reach a terminal state. Resolves true once a client is ready to\n * read, and false once the runtime has failed or the given number of milliseconds passes.\n *\n * The caller does not have to care whether the runtime loaded before or after it, which is the\n * whole point. Arriving late is covered by reading the load state, because the broadcast has\n * already been and gone by then, and arriving early is covered by listening for the broadcast.\n * Between them there is no ordering a caller can lose to.\n *\n * A failure resolves as soon as it is known rather than waiting out the timeout. That matters\n * because two of the failures are decided long before any caller's deadline. The theme editor is\n * known before a single bundle runs, and a missing anonymous id is known the moment the runtime\n * looks for one. Sitting out two or three seconds for an answer that already exists would delay\n * everything downstream of the value for nothing.\n *\n * Anything other than pending is terminal, which is what the check tests for, so a failure reason\n * added later needs no change here.\n *\n * On a preview theme the wait is extended, because the runtime deliberately holds the broadcast\n * open there to give the GrowthBook DevTools extension time to push forced variant overrides in. A\n * caller that gave up partway through that hold would silently drop the forced variant and make the\n * extension unreliable to debug with. How long the hold lasts comes off the runtime global, so the\n * two sides cannot drift apart into a hold that outlives the wait covering it.\n *\n * Neither the preview check nor the hold length can be read from anything the runtime's boot script\n * writes, because that script loads async and can execute after a caller has already asked. Both\n * the Shopify globals and the runtime's inline seed are set in the head instead, so they are always\n * available.\n *\n * The timeout stays as a floor for the one case no broadcast can cover, the boot script never\n * running at all. On that path the listener is left registered rather than torn down, since a\n * broadcast that does eventually arrive resolves a promise that has already settled, which changes\n * nothing.\n */\nfunction whenLoaded(milliseconds: number) {\n  return new Promise<boolean>(resolve => {\n    if (growthbook_runtime.load_state !== 'pending') {\n      resolve(growthbook_runtime.load_state === 'ready');\n      return;\n    }\n\n    const extraMillis = Shopify.previewMode ? growthbook_runtime.preview_hold_ms : 0;\n    const timer = setTimeout(resolve, milliseconds + extraMillis, false);\n\n    addEventListener('growthbook-load-settled', () => {\n      clearTimeout(timer);\n      resolve(growthbook_runtime.load_state === 'ready');\n    }, { once: true });\n  });\n}\n\n/**\n * Fault-tolerant wrapper around GrowthBook's own getFeatureValue SDK method.\n *\n * In GrowthBook the unit is a feature (a feature flag) identified by a feature key. The feature's\n * value for the current user is decided by evaluating its rules. One kind of rule is an experiment\n * rule (a \"feature flag experiment\"): it randomly buckets the user into a variation and returns\n * that variation's value. So the same feature key can resolve to a static flag value or to an A/B\n * test assignment, and this call returns whatever it resolves to. The caller cannot tell which, and\n * does not need to. When the feature is backed by an experiment rule, the returned value is the\n * user's assigned variation.\n *\n * Calling this is not a passive read, and that is the thing to know before adding a call. Bucketing\n * happens during the read, and the SDK reports the exposure to the runtime's tracking callback on\n * the way back out, which is what puts the visitor in the experiment's analytics. So the moment a\n * section calls this, that visitor counts as having seen whatever variation came back.\n *\n * That makes where a call sits matter as much as what it does with the answer. A read the visitor\n * could not possibly have seen still counts, and every such visitor lands in both groups and\n * behaves the same in both, which drags the measured difference between them toward zero. So gate\n * the call on the thing being on the page and reachable, rather than reading up front and deciding\n * later.\n *\n * On top of the raw SDK call this wrapper adds:\n *\n * - Fault tolerance. On a timeout, an error, or an undefined feature it returns the caller's\n *   fallback instead of throwing. It does assume the runtime global is present, which the always-on\n *   layer guarantees, so a page that somehow lacks it rejects rather than falling back.\n * - A shared, pre-initialized client. It reads the single client the cross-section coordination\n *   global exposes, so the SDK is initialized once per page rather than once per call.\n * - Indifference to load order. It waits on the runtime's ready signal rather than on the client\n *   promise existing yet, so a caller whose bundle runs before the runtime's is not penalized for\n *   it.\n * - A bounded wait. It always resolves within the given timeout, or within a global default when no\n *   per-call timeout is passed.\n * - Per-page memoization. The first resolved value for a feature key is cached and returned for\n *   every later call in the same page load. Once the payload has loaded this buys nothing on its\n *   own, because the SDK evaluates from attributes that do not change during a page, so repeated\n *   reads would agree with each other anyway. Where it earns its place is the timeout boundary. A\n *   caller that gave up waiting holds the fallback and the page has already acted on it, the client\n *   can become ready a moment later, and without it the next caller would get the real value and\n *   disagree with what the page is showing.\n *\n * @param featureKey The GrowthBook feature key.\n * @param fallback The value returned when the feature is undefined or when the lookup times out or\n * errors.\n * @param timeoutMs Optional per-call override for the lookup timeout, in milliseconds.\n *\n * @see https://docs.growthbook.io/lib/js#quick-usage GrowthBook JS SDK getFeatureValue\n * @see https://docs.growthbook.io/feature-flag-experiments How A/B tests are delivered via features\n * @see https://docs.growthbook.io/features/basics Feature flag fundamentals (keys, values, rules)\n */\nexport async function getFeatureValue<T extends string = string>(featureKey: string,\n  fallback: NoInfer<T>, timeoutMs?: number) {\n  const resolvedFeatures = growthbook_runtime.resolved_features;\n  if (resolvedFeatures[featureKey]) {\n    return <T>resolvedFeatures[featureKey];\n  }\n\n  let value: T | null = null;\n\n  // The span starts here rather than at function entry, so that it only ever covers a real wait.\n  // The memoized return above answers without waiting on anything, and timing it would put a span\n  // on the timeline for every read in the page. Leaving it outside means a span showing up at all\n  // is a caller that actually waited. Closing the span in a finally is what covers the timeout and\n  // the failure paths, which are the ones most worth a number.\n\n  const startMillis = now();\n  const effectiveTimeoutMs = timeoutMs ?? growthbook_runtime.default_timeout_ms;\n\n  try {\n    const loaded = await whenLoaded(effectiveTimeoutMs);\n\n    // The runtime either failed or ran out of time, and neither is reported here so as to avoid\n    // Sentry noise, so we return instead of throwing. The fallback is cached so that later calls\n    // for the same feature key in the same page load get the same answer, since a feature read in\n    // several places has to agree with itself.\n\n    if (!loaded) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // The signal says the client is ready, so this await is already settled and only defends\n    // against the impossible case of the two disagreeing.\n\n    const growthbook = await growthbook_runtime.client_loader;\n\n    if (!growthbook) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // Passing null here rather than the caller's fallback is deliberate. The SDK applies its own\n    // default only on a strict null check against the evaluated value, so handing it null makes\n    // this a straight pass-through of whatever the feature evaluated to, and every \"nothing usable\"\n    // case then lands on the single type check below.\n    //\n    // The evaluated value is null for an unknown feature key, for a feature with no default value\n    // configured, and for an unmet prerequisite. Passing the fallback here would cover exactly\n    // those and nothing else.\n    //\n    // What it would not cover is a value that is present but not a string. A feature configured\n    // with a number, a boolean, or a JSON value evaluates to that value, the SDK's null check does\n    // not fire, and the value comes back untouched. Undefined slips through for the same reason. So\n    // the type check below is load-bearing either way, and routing everything through it keeps the\n    // decision about what counts as usable in one place.\n\n    value = <T | null>growthbook.getFeatureValue<string | null>(featureKey, null);\n  } catch (error) {\n    console.warn('Error getting feature value from GrowthBook:', error);\n  } finally {\n    measure('growthbook-feature-value', startMillis, now(), {\n      feature: featureKey,\n      timeout: effectiveTimeoutMs\n    });\n  }\n\n  // The value comes from GrowthBook, so its type is an unverified boundary assertion. Coalesce\n  // anything that is not a string (null, or a missing or unexpected value) to the caller's default,\n  // so the resolved value is always a defined string.\n\n  if (typeof value !== 'string') {\n    value = fallback;\n  }\n\n  resolvedFeatures[featureKey] = value;\n\n  return value;\n}\n\n/**\n * Compares a value read through {@link getFeatureValue} against an expected value, ignoring case.\n * Every comparison against a feature value goes through this, even where a plain equality check\n * would do today, so that the casing decision lives in one place and new code copied from an\n * existing call site picks it up.\n *\n * The value is GrowthBook boundary data whose casing is not guaranteed, and a feature has been seen\n * returning an uppercase value. The expected side can be typed by hand too, such as an experiment\n * id typed into a section setting in the theme editor. Neither side's casing is enforced anywhere,\n * so we lowercase both. We leave the value itself untouched in the wrapper, so a caller reading it\n * directly still sees exactly what GrowthBook served.\n *\n * @param value The feature value\n * @param expected The value to compare against\n */\nexport function featureValueMatches(value: string, expected: string) {\n  return value.toLowerCase() === expected.toLowerCase();\n}\n","import { ready } from '../../lib/dom-ready.js';\nimport { getFeatureValue, featureValueMatches } from '../../lib/growthbook-feature.js';\n\nfunction setupExperiment(variation: string) {\n  // Control is shown core hair-care product template, since all sections are initially hidden we\n  // simply short circuit\n  if (featureValueMatches(variation, '0')) {\n    return;\n  }\n\n  // Variations 2 and 3 show the product-complementary section\n  if (featureValueMatches(variation, '2') || featureValueMatches(variation, '3')) {\n    const section = findSectionByName('product-complementary');\n    if (section) {\n      section.hidden = false;\n    }\n  }\n\n  // Variations 1 and 3 show the hair-care-bundles-variation section\n  if (featureValueMatches(variation, '1') || featureValueMatches(variation, '3')) {\n    const bundles = document.querySelector<HTMLElement>('.hair-care-bundles-variation');\n    if (bundles) {\n      bundles.style.display = 'block';\n    }\n  }\n}\n\n/**\n * Return the section based on the name in the template (i.e. \"banner-full-width-video\"). Shopify\n * exposes this information only through the section id attribute, but it is obfuscated with some\n * random numbers. The function will find the section by comparing the postfix of the id with the\n * name\n */\nfunction findSectionByName(name: string) {\n  const sections = document.querySelectorAll<HTMLElement>('main section');\n  for (const section of sections) {\n    const sectionId = section.getAttribute('id');\n    if (sectionId && sectionId.endsWith(name)) {\n      return section;\n    }\n  }\n}\n\nfunction onReady() {\n  // hide the products-complimentary section initially\n  const section = findSectionByName('product-complementary');\n  if (section) {\n    section.hidden = true;\n  }\n\n  const productDetail = document.querySelector('#product-detail-section');\n\n  // The section is routinely absent on 404 pages, which routinely occurs when products are\n  // unpublished, when a stale or spammed product link is followed, or when the variant no longer\n  // exists. This module still loads there as part of the shared product detail bundle.\n  if (!productDetail) {\n    return;\n  }\n\n  // Only set up the experiment if the product detail section has the correct experiment data\n  // attribute. All other PDPs will render the sections as normal - showing/hiding based on the\n  // availability of content only\n  if (productDetail.classList.contains('hair-care-experiment')) {\n    getFeatureValue('hair-care-revenue', '0')\n      .then(setupExperiment).catch(console.warn);\n  }\n}\n\nready(onReady);\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n"],"names":["now","_performance","performance","error","console","debug","Date","async","getFeatureValue","featureKey","fallback","timeoutMs","resolvedFeatures","growthbook_runtime","resolved_features","value","startMillis","effectiveTimeoutMs","default_timeout_ms","milliseconds","Promise","resolve","load_state","extraMillis","Shopify","previewMode","preview_hold_ms","timer","setTimeout","addEventListener","clearTimeout","once","growthbook","client_loader","warn","name","start","end","detail","_performance3","measure","feature","timeout","featureValueMatches","expected","toLowerCase","setupExperiment","variation","section","findSectionByName","hidden","bundles","document","querySelector","style","display","sections","querySelectorAll","sectionId","getAttribute","endsWith","callback","type","readyState","ready","productDetail","classList","contains","then","catch"],"sourceRoot":""}