Skip to content

Instantly share code, notes, and snippets.

@dangayle
Last active June 3, 2026 19:17
Show Gist options
  • Select an option

  • Save dangayle/6e8995e549d95c5fd388737040b6641c to your computer and use it in GitHub Desktop.

Select an option

Save dangayle/6e8995e549d95c5fd388737040b6641c to your computer and use it in GitHub Desktop.
Hydrogen Analytics / PerfKit Validator — paste into the DevTools console (or run as a bookmarklet) on any live Hydrogen storefront to deterministically check the PerfKit config + analytics contract.

Hydrogen Analytics / PerfKit Validator

Checks whether a Hydrogen storefront has its analytics wired up correctly, by running in your browser on the live store. Read-only — no install, no repo access, nothing sent anywhere. The report prints automatically in the console as you browse.

It covers both trackers Hydrogen runs:

  • PerfKit (web performance / RUM) — script config + navigate() / setPageType() calls.
  • Shopify analytics (Monorail) — the sessions / conversions / product events (page_rendered, collection_page_rendered, product_page_rendered, search_submitted, product_added_to_cart) and their payload validity (e.g. product / add-to-cart events carry product_id + variant_id).

Which one do I use?

🔖 Known SPA store (clicking around does NOT do a full page reload) → Bookmarklet

The easy one.

  1. Open 2-bookmarklet-for-SPA-stores.txt, select all, copy.
  2. Make a new browser bookmark, paste it into the URL/Address field, save it.
  3. Open the console (F12 → Console) so you can see the output.
  4. Go to the store, click the bookmark, then just browse: home → a collection → a product → the cart. The report prints automatically as you go.

🧩 Any store, including full-page-reload stores → DevTools Snippet (recommended)

A Snippet is saved once in DevTools and re-run on any page with one keystroke — no re-pasting. (Snippets docs)

Set up once:

  1. Open DevTools (F12) → SourcesSnippets (you may need the » menu). Click + New snippet.
  2. Open 3-devtools-snippet.js, copy all of it, paste into the snippet, and save (Cmd/Ctrl+S). Name it e.g. hydrogen-validator.

Quick check (any store):

  • On each page you want to check (home, collection, product, cart), run the snippet: Cmd/Ctrl+Enter (or right-click the snippet → Run). Results accumulate and the report auto-prints. Confirms PerfKit config + version/staleness, and that Shopify analytics events fired.

Full-reload stores — to also capture the exact load-time calls (both trackers):

  1. DevTools → SourcesEvent Listener Breakpoints → check “Script First Statement”.
  2. Reload the page — it pauses before the page's scripts run.
  3. Run the snippet (Cmd/Ctrl+Enter). It installs its hooks before the page's analytics fire.
  4. Uncheck “Script First Statement”, then Resume (F8).
  5. Repeat per page. Results accumulate; the report auto-prints.

Why the breakpoint: both PerfKit (navigate/setPageType) and the Shopify analytics events fire during page load, before an on-demand script can observe them. Running the snippet while paused at the first statement gets the hooks in place first, so you capture the exact PerfKit page-type calls and the page-load Monorail events. Without it, full-reload stores still report PerfKit-present + that analytics fired per page — you just won't see the exact page-type argument or the load-time commerce events.

📖 Want to read or host the code?

4-full-source.js is the full, un-minified, commented source.


What you'll see

Two sections:

PerfKit (web performance) — PerfKit build + whether it's stale, and per page: is PerfKit configured (correct script URL + data-* attributes)? which calls fired (navigate / setPageType)?

Shopify analytics (sessions / conversions) — for each commerce event (page view, collection view, product view, search, add to cart): did it fire, how many times, and is the payload valid (product/add-to-cart events must carry product_id + variant_id). Plus whether the session page-view (trekkie) fired and which page types it saw.

If neither tracker shows any activity, it says so and stops (not a Hydrogen store, analytics not loaded, or blocked by consent). Each section also notes if only one of the two trackers is missing.

Reprint on demand: __hydrogenValidator.report() · Start over: __hydrogenValidator.reset()


Files

File Use
2-bookmarklet-for-SPA-stores.txt One-click bookmarklet — SPA stores
3-devtools-snippet.js DevTools Snippet — any store / full-reload stores
4-full-source.js Readable, un-minified source
javascript:%22use%20strict%22%3B(function()%7B%22use%20strict%22%3Bvar%20c%3D%7BperfKitScriptId%3A%22perfkit%22%2CperfKitUrlPattern%3A%2F%5C%2Fshopifycloud%5C%2Fperf-kit%5C%2Fshopify-perf-kit-(spa%7C%5Cd%2B%5C.%5Cd%2B%5C.%5Cd%2B)%5C.min%5C.js(%3F%3A%5C%3F%7C%24)%2F%2CperfKitCurrentVariant%3A%22spa%22%2CfixedAttributes%3A%7B%22data-application%22%3A%22hydrogen%22%2C%22data-monorail-region%22%3A%22global%22%2C%22data-spa-mode%22%3A%22true%22%2C%22data-resource-timing-sampling-rate%22%3A%22100%22%7D%2CrequiredAttributes%3A%5B%22data-shop-id%22%2C%22data-storefront-id%22%5D%2CmonorailHost%3A%22monorail-edge.shopifysvc.com%22%2CexpectedPages%3A%5B%22home%22%2C%22collection%22%2C%22product%22%2C%22search%22%2C%22cart%22%5D%2CanalyticsEvents%3A%5B%7Bname%3A%22page_rendered%22%2Clabel%3A%22page%20view%22%2Cproducts%3A!1%7D%2C%7Bname%3A%22collection_page_rendered%22%2Clabel%3A%22collection%20view%22%2Cproducts%3A!1%7D%2C%7Bname%3A%22product_page_rendered%22%2Clabel%3A%22product%20view%22%2Cproducts%3A!0%7D%2C%7Bname%3A%22search_submitted%22%2Clabel%3A%22search%22%2Cproducts%3A!1%7D%2C%7Bname%3A%22product_added_to_cart%22%2Clabel%3A%22add%20to%20cart%22%2Cproducts%3A!0%7D%5D%2CcustomTrackingSchema%3A%22custom_storefront_customer_tracking%22%2CpageViewSchema%3A%22trekkie_storefront_page_view%22%7D%2CP%3D%22__hydrogenValidator%22%2CE%3D%22__hydrogenValidatorState%22%3Bfunction%20q()%7Breturn%7BstartedAt%3ADate.now()%2CperfKitVariant%3Anull%2ChydrogenVersion%3Anull%2CsalesChannel%3Anull%2CliveCalls%3A%7Bnavigate%3A!1%2Cproduct%3A!1%2Ccollection%3A!1%2Csearch%3A!1%2Ccart%3A!1%7D%2Cpages%3A%7B%7D%2Canalytics%3A%7Bevents%3A%7B%7D%2CpageViewSeen%3A!1%2CpageTypesSeen%3A%7B%7D%7D%7D%7Dfunction%20K()%7Btry%7Bvar%20e%3DsessionStorage.getItem(E)%3Breturn%20e%3FJSON.parse(e)%3Aq()%7Dcatch%7Breturn%20q()%7D%7Dfunction%20j(e)%7Btry%7BsessionStorage.setItem(E%2CJSON.stringify(e))%7Dcatch%7B%7D%7Dfunction%20H()%7Bvar%20e%3Dlocation.pathname%3Breturn%2F%5C%2Fcheckouts%3F(%5C%2F%7C%24)%2F.test(e)%7C%7C%2F(%5E%7C%5C.)checkout%5C.%2F.test(location.host)%3F%22checkout%22%3Ae%3D%3D%3D%22%2F%22%7C%7Ce%3D%3D%3D%22%22%3F%22home%22%3A%2F%5C%2Fproducts%5C%2F%2F.test(e)%3F%22product%22%3A%2F%5C%2Fcollections%5C%2F%5B%5E%2F%5D%2B%2F.test(e)%7C%7C%2F%5C%2Fcollections%5C%2F%3F%24%2F.test(e)%3F%22collection%22%3A%2F%5C%2Fsearch%2F.test(e)%7C%7C%2F%5B%3F%26%5Dq%3D%2F.test(location.search)%3F%22search%22%3A%2F%5C%2Fcart%2F.test(e)%3F%22cart%22%3A%22other%3A%22%2Be%7Dfunction%20D()%7Bvar%20e%3Ddocument.getElementById(c.perfKitScriptId)%2Cr%3De%3Bif(!r)%7Bfor(var%20n%3Ddocument.querySelectorAll(%22script%5Bsrc%5D%22)%2Ci%3D0%3Bi%3Cn.length%3Bi%2B%2B)if(c.perfKitUrlPattern.test(n%5Bi%5D.getAttribute(%22src%22)%7C%7C%22%22))%7Br%3Dn%5Bi%5D%3Bbreak%7D%7Dvar%20t%3D%7BscriptPresent%3A!!r%2Cvariant%3Anull%2CattrsOk%3A!1%2CattrIssues%3A%5B%5D%2CwindowPerfKit%3Atypeof%20window.PerfKit%3D%3D%22object%22%26%26window.PerfKit!%3D%3Dnull%7D%3Bif(r)%7Bvar%20v%3Dr.getAttribute(%22src%22)%7C%7C%22%22%2Cy%3Dv.match(c.perfKitUrlPattern)%3Bt.variant%3Dy%3Fy%5B1%5D%3Anull%3Bvar%20g%3D%5B%5D%3BObject.keys(c.fixedAttributes).forEach(function(d)%7Bvar%20s%3Dr.getAttribute(d)%3Bs!%3D%3Dc.fixedAttributes%5Bd%5D%26%26g.push(d%2B%22%3D%22%2B(s%3D%3D%3Dnull%3F%22(missing)%22%3As))%7D)%2Cc.requiredAttributes.forEach(function(d)%7Bvar%20s%3Dr.getAttribute(d)%3Bs%7C%7Cg.push(d%2B%22%3D(missing)%22)%7D)%2Ct.attrsOk%3Dg.length%3D%3D%3D0%2Ct.attrIssues%3Dg%7Dreturn%20t%7Dfunction%20J()%7Bvar%20e%3D%7BperfKitLoaded%3A!1%2Cvariant%3Anull%2CanalyticsRequest%3A!1%7D%3Bif(!window.performance%7C%7C!performance.getEntriesByType)return%20e%3Bfor(var%20r%3Dperformance.getEntriesByType(%22resource%22)%2Cn%3D0%3Bn%3Cr.length%3Bn%2B%2B)%7Bvar%20i%3Dr%5Bn%5D.name%7C%7C%22%22%2Ct%3Di.match(c.perfKitUrlPattern)%3Bt%26%26(e.perfKitLoaded%3D!0%2Ce.variant%3Dt%5B1%5D)%2Ci.indexOf(c.monorailHost)!%3D%3D-1%26%26(e.analyticsRequest%3D!0)%7Dreturn%20e%7Dvar%20k%3Dnull%2CO%3Dnull%3Bfunction%20B()%7Btry%7Bvar%20e%3DK()%2Cr%3D%7Bv%3Ae.perfKitVariant%2Chv%3Ae.hydrogenVersion%2Csc%3Ae.salesChannel%2Clc%3Ae.liveCalls%2Cp%3A%7B%7D%7D%3Breturn%20Object.keys(e.pages%7C%7C%7B%7D).forEach(function(n)%7Bvar%20i%3De.pages%5Bn%5D%7C%7C%7B%7D%3Br.p%5Bn%5D%3D%5Bi.scriptPresent%3F1%3A0%2Ci.attrsOk%3F1%3A0%2Ci.analyticsRequest%3F1%3A0%2C(i.liveCalls%7C%7C%5B%5D).map(function(t)%7Breturn%20t.method%2B(t.arg%7C%7C%22%22)%7D).sort().join(%22%7C%22)%5D.join(%22%2C%22)%7D)%2Ce.analytics%26%26(r.a%3D%7Bpv%3Ae.analytics.pageViewSeen%2Cpt%3AObject.keys(e.analytics.pageTypesSeen%7C%7C%7B%7D).sort().join(%22%2C%22)%7D%2CObject.keys(e.analytics.events%7C%7C%7B%7D).forEach(function(n)%7Bvar%20i%3De.analytics.events%5Bn%5D%3Br.a%5Bn%5D%3Di.count%2B%22%3A%22%2B(i.issues%7C%7C%5B%5D).sort().join(%22%7C%22)%7D))%2CJSON.stringify(r)%7Dcatch%7Breturn%20null%7D%7Dfunction%20C()%7Bk%26%26clearTimeout(k)%2Ck%3DsetTimeout(function()%7Bk%3Dnull%3Bvar%20e%3DB()%3Bif(!(e!%3D%3Dnull%26%26e%3D%3D%3DO))%7BO%3De%3Btry%7Bx()%7Dcatch%7B%7D%7D%7D%2C700)%7Dfunction%20U(e)%7Bfunction%20r()%7Bj(e)%7Dfunction%20n(a%2Cl)%7Bvar%20u%3DH()%2Cp%3De.pages%5Bu%5D%7C%7C(e.pages%5Bu%5D%3D%7B%7D)%3Bp.liveCalls%3Dp.liveCalls%7C%7C%5B%5D%2Cp.liveCalls.push(%7Bmethod%3Aa%2Carg%3Al%2Cat%3ADate.now()%7D)%2Ca%3D%3D%3D%22navigate%22%26%26(e.liveCalls.navigate%3D!0)%2Ca%3D%3D%3D%22setPageType%22%26%26e.liveCalls.hasOwnProperty(l)%26%26(e.liveCalls%5Bl%5D%3D!0)%2Cr()%2CC()%7Dfunction%20i(a)%7B%5B%22navigate%22%2C%22setPageType%22%5D.forEach(function(l)%7Bif(!(typeof%20a%5Bl%5D!%3D%22function%22%7C%7Ca%5Bl%5D.__wrapped))%7Bvar%20u%3Da%5Bl%5D.bind(a)%2Cp%3Dfunction()%7Breturn%20n(l%2Carguments.length%3Farguments%5B0%5D%3Avoid%200)%2Cu.apply(this%2Carguments)%7D%3Bp.__wrapped%3D!0%2Cp.__original%3Du%3Btry%7Ba%5Bl%5D%3Dp%7Dcatch%7B%7D%7D%7D)%7Dif(window.PerfKit)i(window.PerfKit)%3Belse%7Bvar%20t%3DObject.getOwnPropertyDescriptor(window%2C%22PerfKit%22)%3Bif(!t%7C%7Ct.configurable)try%7Bvar%20v%3BObject.defineProperty(window%2C%22PerfKit%22%2C%7Bconfigurable%3A!0%2Cenumerable%3A!0%2Cget%3Afunction()%7Breturn%20v%7D%2Cset%3Afunction(a)%7Bv%3Da%3Btry%7Bi(a)%7Dcatch%7B%7D%7D%7D)%7Dcatch%7B%7D%7Dvar%20y%3DsetInterval(function()%7Bwindow.PerfKit%26%26i(window.PerfKit)%7D%2C250)%3Bfunction%20g(a%2Cl)%7Bvar%20u%3De.analytics.events%5Ba%5D%7C%7C(e.analytics.events%5Ba%5D%3D%7Bcount%3A0%2CproductCount%3A0%2Cissues%3A%5B%5D%7D)%3Bu.count%2B%2B%3Bfor(var%20p%3Dnull%2Cm%3D0%3Bm%3Cc.analyticsEvents.length%3Bm%2B%2B)c.analyticsEvents%5Bm%5D.name%3D%3D%3Da%26%26(p%3Dc.analyticsEvents%5Bm%5D)%3Bif(p%26%26p.products)%7Bvar%20_%3Dl.products%2Cw%3D%5B%5D%3BArray.isArray(_)%26%26_.forEach(function(f)%7Btry%7Bw.push(typeof%20f%3D%3D%22string%22%3FJSON.parse(f)%3Af)%7Dcatch%7B%7D%7D)%2Cu.productCount%3Dw.length%2Cw.length%7C%7Cu.issues.indexOf(%22no%20products%20in%20payload%22)%3D%3D%3D-1%26%26u.issues.push(%22no%20products%20in%20payload%22)%2Cw.forEach(function(f)%7Bf.product_id%3D%3Dnull%26%26f.product_gid%3D%3Dnull%26%26u.issues.indexOf(%22missing%20product%20id%22)%3D%3D%3D-1%26%26u.issues.push(%22missing%20product%20id%22)%2Cf.variant_id%3D%3Dnull%26%26f.variant_gid%3D%3Dnull%26%26u.issues.indexOf(%22missing%20variant%20id%22)%3D%3D%3D-1%26%26u.issues.push(%22missing%20variant%20id%22)%7D)%7D%7Dfunction%20d(a%2Cl)%7Bif((%22%22%2Ba).indexOf(c.monorailHost)!%3D%3D-1)%7Bvar%20u%3Dtypeof%20l%3D%3D%22string%22%3Fl%3Anull%3Bif(u)try%7Bvar%20p%3DJSON.parse(u)%2Cm%3Dp.events%7C%7C(p.schema_id%3F%5Bp%5D%3A%5B%5D)%3Bm.forEach(function(_)%7Bvar%20w%3D_.schema_id%7C%7C%22%22%2Cf%3D_.payload%7C%7C%7B%7D%3Bf.asset_version_id%26%26(e.hydrogenVersion%3Df.asset_version_id)%2C(f.source%3D%3D%3D%22hydrogen%22%7C%7Cf.shopifySalesChannel%3D%3D%3D%22hydrogen%22)%26%26(e.salesChannel%3D%22hydrogen%22)%2Cw.indexOf(c.customTrackingSchema)%3D%3D%3D0%26%26f.event_name%26%26g(f.event_name%2Cf)%2Cw.indexOf(c.pageViewSchema)%3D%3D%3D0%26%26(e.analytics.pageViewSeen%3D!0%2Cf.pageType%26%26(e.analytics.pageTypesSeen%5Bf.pageType%5D%3D!0))%7D)%2C!e.salesChannel%26%26%2Fhydrogen%2F.test(u)%26%26(e.salesChannel%3D%22hydrogen%22)%2Cr()%2CC()%7Dcatch%7B%7D%7D%7Dif(window.fetch%26%26!window.fetch.__hv)%7Bvar%20s%3Dwindow.fetch%2Co%3Dfunction(a%2Cl)%7Btry%7Bd(typeof%20a%3D%3D%22string%22%3Fa%3Aa%26%26a.url%2Cl%26%26l.body)%7Dcatch%7B%7Dreturn%20s.apply(this%2Carguments)%7D%3Bo.__hv%3D!0%2Cwindow.fetch%3Do%7Dif(navigator.sendBeacon%26%26!navigator.sendBeacon.__hv)%7Bvar%20b%3Dnavigator.sendBeacon.bind(navigator)%2CT%3Dfunction(a%2Cl)%7Btry%7Bd(a%2Ctypeof%20l%3D%3D%22string%22%3Fl%3Anull)%7Dcatch%7B%7Dreturn%20b(a%2Cl)%7D%3BT.__hv%3D!0%3Btry%7Bnavigator.sendBeacon%3DT%7Dcatch%7B%7D%7Dif(!history.pushState.__hv)%7Bvar%20h%3Dfunction()%7Btry%7BI(e)%2Cr()%2CC()%7Dcatch%7B%7D%7D%2CN%3Dhistory.pushState%2CA%3Dfunction()%7Bvar%20a%3DN.apply(this%2Carguments)%3Breturn%20h()%2Ca%7D%3BA.__hv%3D!0%2Chistory.pushState%3DA%3Bvar%20M%3Dhistory.replaceState%2CR%3Dfunction()%7Bvar%20a%3DM.apply(this%2Carguments)%3Breturn%20h()%2Ca%7D%3BR.__hv%3D!0%2Chistory.replaceState%3DR%2Cwindow.addEventListener(%22popstate%22%2Ch)%7Dreturn%20y%7Dfunction%20I(e)%7Bvar%20r%3DH()%2Cn%3DD()%2Ci%3DJ()%2Ct%3De.pages%5Br%5D%7C%7C%7B%7D%3Bt.url%3Dlocation.origin%2Blocation.pathname%2Ct.lastSeen%3DDate.now()%2Ct.scriptPresent%3Dn.scriptPresent%7C%7Ct.scriptPresent%7C%7C!1%2Ct.windowPerfKit%3Dn.windowPerfKit%7C%7Ct.windowPerfKit%7C%7C!1%2Ct.perfKitLoaded%3Di.perfKitLoaded%7C%7Ct.perfKitLoaded%7C%7C!1%2Ct.analyticsRequest%3Di.analyticsRequest%7C%7Ct.analyticsRequest%7C%7C!1%2Ct.attrsOk%3Dn.scriptPresent%3Fn.attrsOk%3At.attrsOk%7C%7C!1%2Cn.attrIssues%26%26n.attrIssues.length%26%26(t.attrIssues%3Dn.attrIssues)%2Ct.variant%3Dn.variant%7C%7Ci.variant%7C%7Ct.variant%7C%7Cnull%2Ct.liveCalls%3Dt.liveCalls%7C%7C%5B%5D%2Ce.pages%5Br%5D%3Dt%3Bvar%20v%3Dn.variant%7C%7Ci.variant%3Breturn%20v%26%26(e.perfKitVariant%3Dv)%2Cr%7Dfunction%20x()%7Bvar%20e%3DK()%2Cr%3De.pages%7C%7C%7B%7D%2Cn%3D!!e.perfKitVariant%7C%7CObject.keys(r).some(function(s)%7Bvar%20o%3Dr%5Bs%5D%3Breturn%20o%26%26(o.scriptPresent%7C%7Co.perfKitLoaded%7C%7Co.windowPerfKit)%7D)%2Ci%3De.perfKitVariant%26%26e.perfKitVariant!%3D%3Dc.perfKitCurrentVariant%2Ct%3De.analytics%7C%7C%7Bevents%3A%7B%7D%2CpageViewSeen%3A!1%2CpageTypesSeen%3A%7B%7D%7D%2Cv%3Dt.pageViewSeen%7C%7CObject.keys(t.events%7C%7C%7B%7D).length%3E0%7C%7CObject.keys(r).some(function(s)%7Breturn%20r%5Bs%5D%26%26r%5Bs%5D.analyticsRequest%7D)%3Bif(console.log(%22%25cHydrogen%20Analytics%20%2F%20PerfKit%20Validator%22%2C%22font-weight%3Abold%3Bfont-size%3A13px%22)%2C!n%26%26!v)return%20console.log(%22%25cNo%20Hydrogen%20analytics%20detected%20on%20the%20page(s)%20checked.%22%2C%22color%3A%23c0392b%3Bfont-weight%3Abold%22)%2Cconsole.log(%22Neither%20PerfKit%20(performance)%20nor%20Shopify%20analytics%20(Monorail)%20activity%20was%20observed.%20This%20may%20not%20be%20a%20Hydrogen%20storefront%2C%20the%20analytics%20may%20not%20be%20installed%2Floaded%2C%20or%20tracking%20is%20blocked%20until%20the%20visitor%20consents.%20Nothing%20to%20validate%20yet.%22)%2Ce%3Bif(console.log(%22Hydrogen%20version%3A%20%22%2B(e.hydrogenVersion%7C%7C%22not%20observed%22)%2B%22%20%7C%20sales%20channel%3A%20%22%2B(e.salesChannel%7C%7C%22not%20observed%22))%2Cconsole.log(%22%25c%5Cu2014%20PerfKit%20(web%20performance)%20%5Cu2014%22%2C%22font-weight%3Abold%22)%2C!n)console.log(%22PerfKit%20NOT%20detected%20%5Cu2014%20web-performance%20data%20is%20likely%20missing.%20(Shopify%20analytics%20below%20was%20still%20observed.)%22)%3Belse%7Bconsole.log(%22PerfKit%20build%3A%20%22%2B(e.perfKitVariant%7C%7C%22detected%22)%2B(i%3F%22%20(STALE%20%5Cu2014%20upgrade%20Hydrogen)%22%3A%22%22))%3Bvar%20y%3Dc.expectedPages.map(function(s)%7Bvar%20o%3De.pages%5Bs%5D%3Bif(!o)return%7Bpage%3As%2Cvisited%3A%22NO%20%5Cu2014%20visit%20it%22%2CperfkitConfigured%3A%22-%22%2CliveCall%3A%22-%22%7D%3Bvar%20b%3Do.scriptPresent%26%26o.attrsOk%2CT%3D(o.liveCalls%7C%7C%5B%5D).map(function(h)%7Breturn%20h.method%2B(h.arg%3F%22(%22%2Bh.arg%2B%22)%22%3A%22()%22)%7D).filter(function(h%2CN%2CA)%7Breturn%20A.indexOf(h)%3D%3D%3DN%7D).join(%22%2C%20%22)%3Breturn%7Bpage%3As%2Cvisited%3A%22yes%22%2CperfkitConfigured%3A(b%3F%22PASS%22%3Ao.scriptPresent%3F%22FAIL%20attrs%22%3A%22FAIL%20no%20script%22)%2B(o.attrIssues%26%26o.attrIssues.length%3F%22%20%5B%22%2Bo.attrIssues.join(%22%3B%20%22)%2B%22%5D%22%3A%22%22)%2CliveCall%3AT%7C%7C%22(none%20%5Cu2014%20full%20reload%3B%20config%20still%20checked)%22%7D%7D)%3Bconsole.table%3Fconsole.table(y)%3Aconsole.log(y)%7Dif(console.log(%22%25c%5Cu2014%20Shopify%20analytics%20(sessions%20%2F%20conversions)%20%5Cu2014%22%2C%22font-weight%3Abold%22)%2C!v)console.log(%22No%20Shopify%20analytics%20(Monorail)%20events%20observed%20yet%20%5Cu2014%20browse%20the%20key%20pages%2C%20or%20check%20consent%20(analytics%20is%20blocked%20until%20the%20visitor%20consents).%22)%3Belse%7Bvar%20g%3Dc.analyticsEvents.map(function(s)%7Bvar%20o%3Dt.events%5Bs.name%5D%2Cb%3D%7Bevent%3As.label%2B%22%20(%22%2Bs.name%2B%22)%22%2Cfired%3Ao%3F%22PASS%22%3A%22not%20seen%22%2Ccount%3Ao%3Fo.count%3A0%2Cpayload%3A%22-%22%7D%3Breturn%20s.products%26%26(b.payload%3Do%3Fo.issues%26%26o.issues.length%3F%22FAIL%20%5B%22%2Bo.issues.join(%22%3B%20%22)%2B%22%5D%22%3Ao.productCount%2B%22%20product(s)%20ok%22%3A%22-%22)%2Cb%7D)%3Bconsole.table%3Fconsole.table(g)%3Aconsole.log(g)%2Cconsole.log(%22Session%20page-view%20event%20(trekkie)%3A%20%22%2B(t.pageViewSeen%3F%22seen%22%3A%22NOT%20seen%22)%2B%22%20%7C%20page%20types%20seen%3A%20%22%2B(Object.keys(t.pageTypesSeen%7C%7C%7B%7D).join(%22%2C%20%22)%7C%7C%22none%22))%7Dvar%20d%3Dc.expectedPages.filter(function(s)%7Breturn%20e.pages%5Bs%5D%7D).length%3Breturn%20console.log(%22Visited%20%22%2Bd%2B%22%2F%22%2Bc.expectedPages.length%2B%22%20key%20page%20types.%20Auto-updates%20as%20you%20browse%20(full-reload%20stores%3A%20re-run%20on%20each%20page).%20Reprint%3A%20__hydrogenValidator.report()%20%5CxB7%20reset%3A%20__hydrogenValidator.reset()%22)%2Ce%7Dif(window%5BP%5D%26%26window%5BP%5D.__armed)%7Btry%7Bwindow%5BP%5D.report()%7Dcatch%7B%7Dreturn%7Dvar%20S%3DK()%2CF%3DI(S)%3Bj(S)%2CU(S)%3Bfunction%20V()%7Btry%7BI(S)%2Cj(S)%2CC()%7Dcatch%7B%7D%7Ddocument.readyState%3D%3D%3D%22complete%22%3FsetTimeout(V%2C0)%3Awindow.addEventListener(%22load%22%2Cfunction()%7BsetTimeout(V%2C300)%7D)%2CsetTimeout(V%2C1500)%2CsetTimeout(V%2C4e3)%2Cwindow%5BP%5D%3D%7B__armed%3A!0%2Creport%3Ax%2Ccontract%3Ac%2Cstate%3Afunction()%7Breturn%20K()%7D%2Creset%3Afunction()%7Btry%7BsessionStorage.removeItem(E)%7Dcatch%7B%7DO%3Dnull%2Cconsole.log(%22%5Bvalidator%5D%20reset%20%5Cu2014%20cleared%20saved%20page%20results.%22)%7D%7D%2Cconsole.log(%22%25c%5Bvalidator%5D%20checked%20this%20page%3A%20%25c%22%2BF%2C%22color%3A%235a31f4%22%2C%22font-weight%3Abold%22)%3Bvar%20L%3Dx()%3BO%3DB()%3Bvar%20%24%3D!!L.perfKitVariant%7C%7CObject.keys(L.pages%7C%7C%7B%7D).some(function(e)%7Bvar%20r%3DL.pages%5Be%5D%3Breturn%20r%26%26(r.scriptPresent%7C%7Cr.perfKitLoaded%7C%7Cr.windowPerfKit)%7D)%3B%24%26%26console.log(%22Now%20visit%20the%20other%20key%20pages%20(home%2C%20collection%2C%20product%2C%20cart).%20On%20a%20SPA%20store%20just%20browse%20%5Cu2014%20this%20report%20prints%20automatically%20as%20you%20go%3B%20on%20a%20full-reload%20store%20re-run%20this%20script%20on%20each%20page.%22)%7D)()%3B
/* Hydrogen Analytics / PerfKit Validator — DevTools Snippet.
* Works on ANY store (SPA or full-page-reload). Read-only.
* Validates BOTH trackers: PerfKit (web performance) and Shopify analytics
* (Monorail sessions/conversions/product events, incl. payload validity).
* The report prints automatically (only when something changes); reprint with
* __hydrogenValidator.report(), reset with __hydrogenValidator.reset().
*
* SET UP ONCE: DevTools (F12) -> Sources -> Snippets -> + New snippet ->
* paste this whole file -> save (Cmd/Ctrl+S).
*
* RUN: open the store page, then run the snippet with Cmd/Ctrl+Enter
* (or right-click the snippet -> Run). On a SPA store, run it once and just
* browse. On a full-reload store, run it on each page; results accumulate.
*
* FULL-RELOAD STORES — to capture exact load-time setPageType()/navigate() AND
* the page-load Monorail analytics events:
* 1) Sources -> Event Listener Breakpoints -> check "Script First Statement".
* 2) Reload the page (it pauses before the page's scripts run).
* 3) Run this snippet (Cmd/Ctrl+Enter).
* 4) Uncheck "Script First Statement", then Resume (F8). Repeat per page.
*/
"use strict";(function(){"use strict";var c={perfKitScriptId:"perfkit",perfKitUrlPattern:/\/shopifycloud\/perf-kit\/shopify-perf-kit-(spa|\d+\.\d+\.\d+)\.min\.js(?:\?|$)/,perfKitCurrentVariant:"spa",fixedAttributes:{"data-application":"hydrogen","data-monorail-region":"global","data-spa-mode":"true","data-resource-timing-sampling-rate":"100"},requiredAttributes:["data-shop-id","data-storefront-id"],monorailHost:"monorail-edge.shopifysvc.com",expectedPages:["home","collection","product","search","cart"],analyticsEvents:[{name:"page_rendered",label:"page view",products:!1},{name:"collection_page_rendered",label:"collection view",products:!1},{name:"product_page_rendered",label:"product view",products:!0},{name:"search_submitted",label:"search",products:!1},{name:"product_added_to_cart",label:"add to cart",products:!0}],customTrackingSchema:"custom_storefront_customer_tracking",pageViewSchema:"trekkie_storefront_page_view"},P="__hydrogenValidator",E="__hydrogenValidatorState";function q(){return{startedAt:Date.now(),perfKitVariant:null,hydrogenVersion:null,salesChannel:null,liveCalls:{navigate:!1,product:!1,collection:!1,search:!1,cart:!1},pages:{},analytics:{events:{},pageViewSeen:!1,pageTypesSeen:{}}}}function K(){try{var e=sessionStorage.getItem(E);return e?JSON.parse(e):q()}catch{return q()}}function j(e){try{sessionStorage.setItem(E,JSON.stringify(e))}catch{}}function H(){var e=location.pathname;return/\/checkouts?(\/|$)/.test(e)||/(^|\.)checkout\./.test(location.host)?"checkout":e==="/"||e===""?"home":/\/products\//.test(e)?"product":/\/collections\/[^/]+/.test(e)||/\/collections\/?$/.test(e)?"collection":/\/search/.test(e)||/[?&]q=/.test(location.search)?"search":/\/cart/.test(e)?"cart":"other:"+e}function D(){var e=document.getElementById(c.perfKitScriptId),r=e;if(!r){for(var n=document.querySelectorAll("script[src]"),i=0;i<n.length;i++)if(c.perfKitUrlPattern.test(n[i].getAttribute("src")||"")){r=n[i];break}}var t={scriptPresent:!!r,variant:null,attrsOk:!1,attrIssues:[],windowPerfKit:typeof window.PerfKit=="object"&&window.PerfKit!==null};if(r){var v=r.getAttribute("src")||"",y=v.match(c.perfKitUrlPattern);t.variant=y?y[1]:null;var g=[];Object.keys(c.fixedAttributes).forEach(function(d){var s=r.getAttribute(d);s!==c.fixedAttributes[d]&&g.push(d+"="+(s===null?"(missing)":s))}),c.requiredAttributes.forEach(function(d){var s=r.getAttribute(d);s||g.push(d+"=(missing)")}),t.attrsOk=g.length===0,t.attrIssues=g}return t}function J(){var e={perfKitLoaded:!1,variant:null,analyticsRequest:!1};if(!window.performance||!performance.getEntriesByType)return e;for(var r=performance.getEntriesByType("resource"),n=0;n<r.length;n++){var i=r[n].name||"",t=i.match(c.perfKitUrlPattern);t&&(e.perfKitLoaded=!0,e.variant=t[1]),i.indexOf(c.monorailHost)!==-1&&(e.analyticsRequest=!0)}return e}var k=null,O=null;function B(){try{var e=K(),r={v:e.perfKitVariant,hv:e.hydrogenVersion,sc:e.salesChannel,lc:e.liveCalls,p:{}};return Object.keys(e.pages||{}).forEach(function(n){var i=e.pages[n]||{};r.p[n]=[i.scriptPresent?1:0,i.attrsOk?1:0,i.analyticsRequest?1:0,(i.liveCalls||[]).map(function(t){return t.method+(t.arg||"")}).sort().join("|")].join(",")}),e.analytics&&(r.a={pv:e.analytics.pageViewSeen,pt:Object.keys(e.analytics.pageTypesSeen||{}).sort().join(",")},Object.keys(e.analytics.events||{}).forEach(function(n){var i=e.analytics.events[n];r.a[n]=i.count+":"+(i.issues||[]).sort().join("|")})),JSON.stringify(r)}catch{return null}}function C(){k&&clearTimeout(k),k=setTimeout(function(){k=null;var e=B();if(!(e!==null&&e===O)){O=e;try{x()}catch{}}},700)}function U(e){function r(){j(e)}function n(a,l){var u=H(),p=e.pages[u]||(e.pages[u]={});p.liveCalls=p.liveCalls||[],p.liveCalls.push({method:a,arg:l,at:Date.now()}),a==="navigate"&&(e.liveCalls.navigate=!0),a==="setPageType"&&e.liveCalls.hasOwnProperty(l)&&(e.liveCalls[l]=!0),r(),C()}function i(a){["navigate","setPageType"].forEach(function(l){if(!(typeof a[l]!="function"||a[l].__wrapped)){var u=a[l].bind(a),p=function(){return n(l,arguments.length?arguments[0]:void 0),u.apply(this,arguments)};p.__wrapped=!0,p.__original=u;try{a[l]=p}catch{}}})}if(window.PerfKit)i(window.PerfKit);else{var t=Object.getOwnPropertyDescriptor(window,"PerfKit");if(!t||t.configurable)try{var v;Object.defineProperty(window,"PerfKit",{configurable:!0,enumerable:!0,get:function(){return v},set:function(a){v=a;try{i(a)}catch{}}})}catch{}}var y=setInterval(function(){window.PerfKit&&i(window.PerfKit)},250);function g(a,l){var u=e.analytics.events[a]||(e.analytics.events[a]={count:0,productCount:0,issues:[]});u.count++;for(var p=null,m=0;m<c.analyticsEvents.length;m++)c.analyticsEvents[m].name===a&&(p=c.analyticsEvents[m]);if(p&&p.products){var _=l.products,w=[];Array.isArray(_)&&_.forEach(function(f){try{w.push(typeof f=="string"?JSON.parse(f):f)}catch{}}),u.productCount=w.length,w.length||u.issues.indexOf("no products in payload")===-1&&u.issues.push("no products in payload"),w.forEach(function(f){f.product_id==null&&f.product_gid==null&&u.issues.indexOf("missing product id")===-1&&u.issues.push("missing product id"),f.variant_id==null&&f.variant_gid==null&&u.issues.indexOf("missing variant id")===-1&&u.issues.push("missing variant id")})}}function d(a,l){if((""+a).indexOf(c.monorailHost)!==-1){var u=typeof l=="string"?l:null;if(u)try{var p=JSON.parse(u),m=p.events||(p.schema_id?[p]:[]);m.forEach(function(_){var w=_.schema_id||"",f=_.payload||{};f.asset_version_id&&(e.hydrogenVersion=f.asset_version_id),(f.source==="hydrogen"||f.shopifySalesChannel==="hydrogen")&&(e.salesChannel="hydrogen"),w.indexOf(c.customTrackingSchema)===0&&f.event_name&&g(f.event_name,f),w.indexOf(c.pageViewSchema)===0&&(e.analytics.pageViewSeen=!0,f.pageType&&(e.analytics.pageTypesSeen[f.pageType]=!0))}),!e.salesChannel&&/hydrogen/.test(u)&&(e.salesChannel="hydrogen"),r(),C()}catch{}}}if(window.fetch&&!window.fetch.__hv){var s=window.fetch,o=function(a,l){try{d(typeof a=="string"?a:a&&a.url,l&&l.body)}catch{}return s.apply(this,arguments)};o.__hv=!0,window.fetch=o}if(navigator.sendBeacon&&!navigator.sendBeacon.__hv){var b=navigator.sendBeacon.bind(navigator),T=function(a,l){try{d(a,typeof l=="string"?l:null)}catch{}return b(a,l)};T.__hv=!0;try{navigator.sendBeacon=T}catch{}}if(!history.pushState.__hv){var h=function(){try{I(e),r(),C()}catch{}},N=history.pushState,A=function(){var a=N.apply(this,arguments);return h(),a};A.__hv=!0,history.pushState=A;var M=history.replaceState,R=function(){var a=M.apply(this,arguments);return h(),a};R.__hv=!0,history.replaceState=R,window.addEventListener("popstate",h)}return y}function I(e){var r=H(),n=D(),i=J(),t=e.pages[r]||{};t.url=location.origin+location.pathname,t.lastSeen=Date.now(),t.scriptPresent=n.scriptPresent||t.scriptPresent||!1,t.windowPerfKit=n.windowPerfKit||t.windowPerfKit||!1,t.perfKitLoaded=i.perfKitLoaded||t.perfKitLoaded||!1,t.analyticsRequest=i.analyticsRequest||t.analyticsRequest||!1,t.attrsOk=n.scriptPresent?n.attrsOk:t.attrsOk||!1,n.attrIssues&&n.attrIssues.length&&(t.attrIssues=n.attrIssues),t.variant=n.variant||i.variant||t.variant||null,t.liveCalls=t.liveCalls||[],e.pages[r]=t;var v=n.variant||i.variant;return v&&(e.perfKitVariant=v),r}function x(){var e=K(),r=e.pages||{},n=!!e.perfKitVariant||Object.keys(r).some(function(s){var o=r[s];return o&&(o.scriptPresent||o.perfKitLoaded||o.windowPerfKit)}),i=e.perfKitVariant&&e.perfKitVariant!==c.perfKitCurrentVariant,t=e.analytics||{events:{},pageViewSeen:!1,pageTypesSeen:{}},v=t.pageViewSeen||Object.keys(t.events||{}).length>0||Object.keys(r).some(function(s){return r[s]&&r[s].analyticsRequest});if(console.log("%cHydrogen Analytics / PerfKit Validator","font-weight:bold;font-size:13px"),!n&&!v)return console.log("%cNo Hydrogen analytics detected on the page(s) checked.","color:#c0392b;font-weight:bold"),console.log("Neither PerfKit (performance) nor Shopify analytics (Monorail) activity was observed. This may not be a Hydrogen storefront, the analytics may not be installed/loaded, or tracking is blocked until the visitor consents. Nothing to validate yet."),e;if(console.log("Hydrogen version: "+(e.hydrogenVersion||"not observed")+" | sales channel: "+(e.salesChannel||"not observed")),console.log("%c\u2014 PerfKit (web performance) \u2014","font-weight:bold"),!n)console.log("PerfKit NOT detected \u2014 web-performance data is likely missing. (Shopify analytics below was still observed.)");else{console.log("PerfKit build: "+(e.perfKitVariant||"detected")+(i?" (STALE \u2014 upgrade Hydrogen)":""));var y=c.expectedPages.map(function(s){var o=e.pages[s];if(!o)return{page:s,visited:"NO \u2014 visit it",perfkitConfigured:"-",liveCall:"-"};var b=o.scriptPresent&&o.attrsOk,T=(o.liveCalls||[]).map(function(h){return h.method+(h.arg?"("+h.arg+")":"()")}).filter(function(h,N,A){return A.indexOf(h)===N}).join(", ");return{page:s,visited:"yes",perfkitConfigured:(b?"PASS":o.scriptPresent?"FAIL attrs":"FAIL no script")+(o.attrIssues&&o.attrIssues.length?" ["+o.attrIssues.join("; ")+"]":""),liveCall:T||"(none \u2014 full reload; config still checked)"}});console.table?console.table(y):console.log(y)}if(console.log("%c\u2014 Shopify analytics (sessions / conversions) \u2014","font-weight:bold"),!v)console.log("No Shopify analytics (Monorail) events observed yet \u2014 browse the key pages, or check consent (analytics is blocked until the visitor consents).");else{var g=c.analyticsEvents.map(function(s){var o=t.events[s.name],b={event:s.label+" ("+s.name+")",fired:o?"PASS":"not seen",count:o?o.count:0,payload:"-"};return s.products&&(b.payload=o?o.issues&&o.issues.length?"FAIL ["+o.issues.join("; ")+"]":o.productCount+" product(s) ok":"-"),b});console.table?console.table(g):console.log(g),console.log("Session page-view event (trekkie): "+(t.pageViewSeen?"seen":"NOT seen")+" | page types seen: "+(Object.keys(t.pageTypesSeen||{}).join(", ")||"none"))}var d=c.expectedPages.filter(function(s){return e.pages[s]}).length;return console.log("Visited "+d+"/"+c.expectedPages.length+" key page types. Auto-updates as you browse (full-reload stores: re-run on each page). Reprint: __hydrogenValidator.report() \xB7 reset: __hydrogenValidator.reset()"),e}if(window[P]&&window[P].__armed){try{window[P].report()}catch{}return}var S=K(),F=I(S);j(S),U(S);function V(){try{I(S),j(S),C()}catch{}}document.readyState==="complete"?setTimeout(V,0):window.addEventListener("load",function(){setTimeout(V,300)}),setTimeout(V,1500),setTimeout(V,4e3),window[P]={__armed:!0,report:x,contract:c,state:function(){return K()},reset:function(){try{sessionStorage.removeItem(E)}catch{}O=null,console.log("[validator] reset \u2014 cleared saved page results.")}},console.log("%c[validator] checked this page: %c"+F,"color:#5a31f4","font-weight:bold");var L=x();O=B();var $=!!L.perfKitVariant||Object.keys(L.pages||{}).some(function(e){var r=L.pages[e];return r&&(r.scriptPresent||r.perfKitLoaded||r.windowPerfKit)});$&&console.log("Now visit the other key pages (home, collection, product, cart). On a SPA store just browse \u2014 this report prints automatically as you go; on a full-reload store re-run this script on each page.")})();
/**
* Hydrogen Analytics / PerfKit Validator
* ---------------------------------------
* A standalone, dependency-free script that deterministically checks a LIVE
* Hydrogen storefront's analytics from the browser, covering BOTH trackers
* that ride Hydrogen's analytics event bus:
* 1. PerfKit (web performance / RUM) — script config + navigate()/setPageType()
* 2. Shopify analytics (Monorail `custom_storefront_customer_tracking` +
* `trekkie_storefront_page_view`) — the sessions / conversions / product
* events, including payload validity (e.g. product / add-to-cart events
* carry product_id + variant_id).
* No automation, no repo access, no shipped-in instrumentation, no server-side
* telemetry. Read-only.
*
* It accumulates results across page views in sessionStorage, so it works on
* stores that do full page reloads on navigation (where a one-page script
* would be unloaded) as well as SPA stores.
*
* HOW TO USE — see 1-README.md in this gist for step-by-step instructions.
* Two delivery options:
* - SPA stores: the one-click bookmarklet (2-bookmarklet-for-SPA-stores.txt).
* - Any store, incl. full page reloads: save as a DevTools Snippet
* (Sources -> Snippets -> New snippet -> paste -> Ctrl/Cmd+Enter to run).
* The Snippet is saved once and re-run on each page with one keystroke.
* The report prints automatically as you browse; reprint with
* __hydrogenValidator.report(), reset with __hydrogenValidator.reset().
*
* Why per page: PerfKit fires its navigate()/setPageType() calls while the
* page is loading, before an on-demand script (console/snippet) can wrap them.
* To stay deterministic on every store, this script reads the browser's
* buffered Performance resource timings — so it can see that the PerfKit
* script loaded and that analytics requests fired on THIS page even though it
* ran after load. On SPA navigations it also captures the live
* navigate()/setPageType() calls directly.
*
* CAPTURING EXACT LOAD-TIME CALLS ON FULL-RELOAD (MPA) STORES, via DevTools:
* To capture the exact setPageType('product'|'collection'|...) arguments on
* a store that does full page reloads, this code must run BEFORE PerfKit is
* assigned. Run it as a Snippet while paused at the first statement (no
* extension needed, works under any CSP):
* 1) DevTools -> Sources -> Event Listener Breakpoints ->
* check "Script First Statement".
* 2) Reload the page. It pauses before the first script runs.
* 3) Run this Snippet (Ctrl/Cmd+Enter). It installs a window.PerfKit
* assignment trap.
* 4) Uncheck "Script First Statement", then Resume (F8).
* PerfKit then loads, and its load-time navigate()/setPageType() calls are
* captured. Repeat per page; results accumulate in sessionStorage.
* (Without this, full-reload stores still get PerfKit-present + analytics-
* fired per page; you just won't see the exact page-type argument.)
*
* Note on checkout: checkout is a separate Shopify-hosted origin with its own
* (Shopify-managed) analytics; this storefront validator does not span it, and
* sessionStorage does not carry across to the checkout domain.
*/
(function () {
'use strict';
// ----- Contract: the Hydrogen source of truth this script encodes ---------
var CONTRACT = {
perfKitScriptId: 'perfkit',
// PerfKit's URL is not stable across Hydrogen versions:
// current source: shopify-perf-kit-spa.min.js
// older builds: shopify-perf-kit-1.0.1.min.js
perfKitUrlPattern:
/\/shopifycloud\/perf-kit\/shopify-perf-kit-(spa|\d+\.\d+\.\d+)\.min\.js(?:\?|$)/,
perfKitCurrentVariant: 'spa',
fixedAttributes: {
'data-application': 'hydrogen',
'data-monorail-region': 'global',
'data-spa-mode': 'true',
'data-resource-timing-sampling-rate': '100',
},
requiredAttributes: ['data-shop-id', 'data-storefront-id'],
monorailHost: 'monorail-edge.shopifysvc.com',
// The page types we want a merchant to walk through.
expectedPages: ['home', 'collection', 'product', 'search', 'cart'],
// Shopify commerce-analytics events sent to Monorail via
// `custom_storefront_customer_tracking` (the data behind Shopify's
// sessions / conversions / product reports). label = friendly name;
// products = whether the event must carry a products[] payload.
analyticsEvents: [
{name: 'page_rendered', label: 'page view', products: false},
{name: 'collection_page_rendered', label: 'collection view', products: false},
{name: 'product_page_rendered', label: 'product view', products: true},
{name: 'search_submitted', label: 'search', products: false},
{name: 'product_added_to_cart', label: 'add to cart', products: true},
],
customTrackingSchema: 'custom_storefront_customer_tracking',
pageViewSchema: 'trekkie_storefront_page_view',
};
var GLOBAL_KEY = '__hydrogenValidator';
var SS_KEY = '__hydrogenValidatorState';
// ----- Cross-page state, persisted in sessionStorage ----------------------
function freshState() {
return {
startedAt: Date.now(),
perfKitVariant: null,
hydrogenVersion: null,
salesChannel: null,
// observed live PerfKit calls (only catchable on SPA navigations)
liveCalls: {
navigate: false,
product: false,
collection: false,
search: false,
cart: false,
},
// per page-type evidence, accumulated across visits
pages: {},
// Shopify commerce analytics observed on the wire (Monorail).
// events: event_name -> {count, productCount, issues:[]}
// pageView: the trekkie session page-view event + page types seen.
analytics: {
events: {},
pageViewSeen: false,
pageTypesSeen: {},
},
};
}
function loadState() {
try {
var raw = sessionStorage.getItem(SS_KEY);
return raw ? JSON.parse(raw) : freshState();
} catch (e) {
return freshState();
}
}
function saveState(state) {
try {
sessionStorage.setItem(SS_KEY, JSON.stringify(state));
} catch (e) {
/* sessionStorage unavailable; degrade to in-memory only */
}
}
// ----- Where are we? ------------------------------------------------------
function currentPageType() {
var p = location.pathname;
if (/\/checkouts?(\/|$)/.test(p) || /(^|\.)checkout\./.test(location.host))
return 'checkout';
if (p === '/' || p === '') return 'home';
if (/\/products\//.test(p)) return 'product';
if (/\/collections\/[^/]+/.test(p)) return 'collection';
if (/\/collections\/?$/.test(p)) return 'collection';
if (/\/search/.test(p) || /[?&]q=/.test(location.search)) return 'search';
if (/\/cart/.test(p)) return 'cart';
return 'other:' + p;
}
// ----- Static, synchronous DOM audit of the current page ------------------
function auditCurrentPage() {
var el = document.getElementById(CONTRACT.perfKitScriptId);
var script = el;
if (!script) {
var scripts = document.querySelectorAll('script[src]');
for (var i = 0; i < scripts.length; i++) {
if (CONTRACT.perfKitUrlPattern.test(scripts[i].getAttribute('src') || '')) {
script = scripts[i];
break;
}
}
}
var result = {
scriptPresent: !!script,
variant: null,
attrsOk: false,
attrIssues: [],
windowPerfKit:
typeof window.PerfKit === 'object' && window.PerfKit !== null,
};
if (script) {
var src = script.getAttribute('src') || '';
var m = src.match(CONTRACT.perfKitUrlPattern);
result.variant = m ? m[1] : null;
var issues = [];
Object.keys(CONTRACT.fixedAttributes).forEach(function (attr) {
var actual = script.getAttribute(attr);
if (actual !== CONTRACT.fixedAttributes[attr]) {
issues.push(attr + '=' + (actual === null ? '(missing)' : actual));
}
});
CONTRACT.requiredAttributes.forEach(function (attr) {
var actual = script.getAttribute(attr);
if (!actual) issues.push(attr + '=(missing)');
});
result.attrsOk = issues.length === 0;
result.attrIssues = issues;
}
return result;
}
// ----- Retroactive evidence from buffered Performance resource timings -----
// Survives "ran after load": the browser keeps resource entries for requests
// already made on this page, so we can see PerfKit + analytics activity.
function scanPerformance() {
var out = {perfKitLoaded: false, variant: null, analyticsRequest: false};
if (!window.performance || !performance.getEntriesByType) return out;
var entries = performance.getEntriesByType('resource');
for (var i = 0; i < entries.length; i++) {
var name = entries[i].name || '';
var m = name.match(CONTRACT.perfKitUrlPattern);
if (m) {
out.perfKitLoaded = true;
out.variant = m[1];
}
if (name.indexOf(CONTRACT.monorailHost) !== -1) {
out.analyticsRequest = true;
}
}
return out;
}
// Debounced auto-report so the cumulative report prints on its own as the
// user browses (no need to manually call __hydrogenValidator.report()).
// De-duplicated: only reprints when the observed state actually changed, so
// the post-load re-record timers and repeated events don't stack identical
// reports in the console.
var reportTimer = null;
var lastReportSig = null;
function reportSignature() {
try {
var st = loadState();
var sig = {
v: st.perfKitVariant,
hv: st.hydrogenVersion,
sc: st.salesChannel,
lc: st.liveCalls,
p: {},
};
Object.keys(st.pages || {}).forEach(function (k) {
var pg = st.pages[k] || {};
sig.p[k] = [
pg.scriptPresent ? 1 : 0,
pg.attrsOk ? 1 : 0,
pg.analyticsRequest ? 1 : 0,
(pg.liveCalls || [])
.map(function (call) {
return call.method + (call.arg || '');
})
.sort()
.join('|'),
].join(',');
});
if (st.analytics) {
sig.a = {
pv: st.analytics.pageViewSeen,
pt: Object.keys(st.analytics.pageTypesSeen || {})
.sort()
.join(','),
};
Object.keys(st.analytics.events || {}).forEach(function (n) {
var e = st.analytics.events[n];
sig.a[n] = e.count + ':' + (e.issues || []).sort().join('|');
});
}
return JSON.stringify(sig);
} catch (e) {
return null;
}
}
function scheduleReport() {
if (reportTimer) clearTimeout(reportTimer);
reportTimer = setTimeout(function () {
reportTimer = null;
var sig = reportSignature();
if (sig !== null && sig === lastReportSig) return; // nothing new
lastReportSig = sig;
try {
report();
} catch (e) {}
}, 700);
}
// ----- Live capture (catches SPA navigate()/setPageType() going forward) --
function armLive(state) {
function persist() {
saveState(state);
}
function record(method, arg) {
var type = currentPageType();
var pg = state.pages[type] || (state.pages[type] = {});
pg.liveCalls = pg.liveCalls || [];
pg.liveCalls.push({method: method, arg: arg, at: Date.now()});
if (method === 'navigate') state.liveCalls.navigate = true;
if (method === 'setPageType' && state.liveCalls.hasOwnProperty(arg))
state.liveCalls[arg] = true;
persist();
scheduleReport();
}
function install(target) {
['navigate', 'setPageType'].forEach(function (method) {
if (typeof target[method] !== 'function' || target[method].__wrapped)
return;
var original = target[method].bind(target);
var wrapped = function () {
record(method, arguments.length ? arguments[0] : undefined);
return original.apply(this, arguments);
};
wrapped.__wrapped = true;
wrapped.__original = original;
try {
target[method] = wrapped;
} catch (e) {}
});
}
// Deterministic capture: if PerfKit is already present, wrap it in place.
// Otherwise trap the assignment so we wrap it the instant the PerfKit
// script defines window.PerfKit — this catches the load-time
// navigate()/setPageType() calls on full-reload (MPA) stores, PROVIDED
// this code runs before PerfKit is assigned (see the DevTools
// "Script First Statement" flow in the header). CSP does not restrict
// property definition, so this works under any CSP.
if (window.PerfKit) {
install(window.PerfKit);
} else {
var desc = Object.getOwnPropertyDescriptor(window, 'PerfKit');
if (!desc || desc.configurable) {
try {
var _pk;
Object.defineProperty(window, 'PerfKit', {
configurable: true,
enumerable: true,
get: function () {
return _pk;
},
set: function (v) {
_pk = v;
try {
install(v);
} catch (e) {}
},
});
} catch (e) {}
}
}
// Fallback for environments where the trap can't be installed.
var pollId = setInterval(function () {
if (window.PerfKit) install(window.PerfKit);
}, 250);
// Capture Shopify commerce analytics going to Monorail: decode the batch,
// classify each event by schema/event_name, and validate product payloads.
function recordAnalyticsEvent(name, payload) {
var ev =
state.analytics.events[name] ||
(state.analytics.events[name] = {count: 0, productCount: 0, issues: []});
ev.count++;
var spec = null;
for (var i = 0; i < CONTRACT.analyticsEvents.length; i++) {
if (CONTRACT.analyticsEvents[i].name === name)
spec = CONTRACT.analyticsEvents[i];
}
if (spec && spec.products) {
// `products` is an array of JSON strings, each describing one product.
var raw = payload.products;
var prods = [];
if (Array.isArray(raw)) {
raw.forEach(function (s) {
try {
prods.push(typeof s === 'string' ? JSON.parse(s) : s);
} catch (e) {}
});
}
ev.productCount = prods.length;
if (!prods.length) {
if (ev.issues.indexOf('no products in payload') === -1)
ev.issues.push('no products in payload');
}
prods.forEach(function (p) {
if (p.product_id == null && p.product_gid == null) {
if (ev.issues.indexOf('missing product id') === -1)
ev.issues.push('missing product id');
}
if (p.variant_id == null && p.variant_gid == null) {
if (ev.issues.indexOf('missing variant id') === -1)
ev.issues.push('missing variant id');
}
});
}
}
function capture(url, body) {
if (('' + url).indexOf(CONTRACT.monorailHost) === -1) return;
var text = typeof body === 'string' ? body : null;
if (!text) return;
try {
var json = JSON.parse(text);
var events = json.events || (json.schema_id ? [json] : []);
events.forEach(function (entry) {
var schema = entry.schema_id || '';
var payload = entry.payload || {};
if (payload.asset_version_id)
state.hydrogenVersion = payload.asset_version_id;
if (
payload.source === 'hydrogen' ||
payload.shopifySalesChannel === 'hydrogen'
)
state.salesChannel = 'hydrogen';
if (
schema.indexOf(CONTRACT.customTrackingSchema) === 0 &&
payload.event_name
) {
recordAnalyticsEvent(payload.event_name, payload);
}
if (schema.indexOf(CONTRACT.pageViewSchema) === 0) {
state.analytics.pageViewSeen = true;
if (payload.pageType)
state.analytics.pageTypesSeen[payload.pageType] = true;
}
});
// Fallback so the sales channel still shows on any hydrogen payload.
if (!state.salesChannel && /hydrogen/.test(text))
state.salesChannel = 'hydrogen';
persist();
scheduleReport();
} catch (e) {}
}
if (window.fetch && !window.fetch.__hv) {
var of = window.fetch;
var nf = function (input, init) {
try {
capture(typeof input === 'string' ? input : input && input.url, init && init.body);
} catch (e) {}
return of.apply(this, arguments);
};
nf.__hv = true;
window.fetch = nf;
}
if (navigator.sendBeacon && !navigator.sendBeacon.__hv) {
var ob = navigator.sendBeacon.bind(navigator);
var nb = function (url, data) {
try {
capture(url, typeof data === 'string' ? data : null);
} catch (e) {}
return ob(url, data);
};
nb.__hv = true;
try {
navigator.sendBeacon = nb;
} catch (e) {}
}
// SPA navigations: re-record the page as the user clicks through, so a
// single run can cover a client-side-routed store end to end.
if (!history.pushState.__hv) {
var reRecord = function () {
try {
recordCurrentPage(state);
persist();
scheduleReport();
} catch (e) {}
};
var origPush = history.pushState;
var newPush = function () {
var r = origPush.apply(this, arguments);
reRecord();
return r;
};
newPush.__hv = true;
history.pushState = newPush;
var origReplace = history.replaceState;
var newReplace = function () {
var r = origReplace.apply(this, arguments);
reRecord();
return r;
};
newReplace.__hv = true;
history.replaceState = newReplace;
window.addEventListener('popstate', reRecord);
}
return pollId;
}
// ----- Record the current page into the cross-page state ------------------
function recordCurrentPage(state) {
var type = currentPageType();
var stat = auditCurrentPage();
var perf = scanPerformance();
var pg = state.pages[type] || {};
pg.url = location.origin + location.pathname;
pg.lastSeen = Date.now();
pg.scriptPresent = stat.scriptPresent || pg.scriptPresent || false;
pg.windowPerfKit = stat.windowPerfKit || pg.windowPerfKit || false;
pg.perfKitLoaded = perf.perfKitLoaded || pg.perfKitLoaded || false;
pg.analyticsRequest = perf.analyticsRequest || pg.analyticsRequest || false;
pg.attrsOk =
stat.scriptPresent ? stat.attrsOk : pg.attrsOk || false;
if (stat.attrIssues && stat.attrIssues.length) pg.attrIssues = stat.attrIssues;
pg.variant = stat.variant || perf.variant || pg.variant || null;
pg.liveCalls = pg.liveCalls || [];
state.pages[type] = pg;
var variant = stat.variant || perf.variant;
if (variant) state.perfKitVariant = variant;
return type;
}
// ----- Report -------------------------------------------------------------
function report() {
var state = loadState();
var pages = state.pages || {};
var perfKitDetected =
!!state.perfKitVariant ||
Object.keys(pages).some(function (t) {
var p = pages[t];
return p && (p.scriptPresent || p.perfKitLoaded || p.windowPerfKit);
});
var stale =
state.perfKitVariant &&
state.perfKitVariant !== CONTRACT.perfKitCurrentVariant;
var an = state.analytics || {events: {}, pageViewSeen: false, pageTypesSeen: {}};
var analyticsSeen =
an.pageViewSeen ||
Object.keys(an.events || {}).length > 0 ||
Object.keys(pages).some(function (t) {
return pages[t] && pages[t].analyticsRequest;
});
/* eslint-disable no-console */
console.log(
'%cHydrogen Analytics / PerfKit Validator',
'font-weight:bold;font-size:13px',
);
// Nothing to validate if neither tracker shows any sign of life.
if (!perfKitDetected && !analyticsSeen) {
console.log(
'%cNo Hydrogen analytics detected on the page(s) checked.',
'color:#c0392b;font-weight:bold',
);
console.log(
'Neither PerfKit (performance) nor Shopify analytics (Monorail) activity was observed. This may not be a Hydrogen storefront, the analytics may not be installed/loaded, or tracking is blocked until the visitor consents. Nothing to validate yet.',
);
return state;
}
console.log(
'Hydrogen version: ' +
(state.hydrogenVersion || 'not observed') +
' | sales channel: ' +
(state.salesChannel || 'not observed'),
);
// ---- PerfKit (web performance / RUM) -----------------------------------
console.log('%c— PerfKit (web performance) —', 'font-weight:bold');
if (!perfKitDetected) {
console.log(
'PerfKit NOT detected — web-performance data is likely missing. (Shopify analytics below was still observed.)',
);
} else {
console.log(
'PerfKit build: ' +
(state.perfKitVariant || 'detected') +
(stale ? ' (STALE — upgrade Hydrogen)' : ''),
);
var rows = CONTRACT.expectedPages.map(function (type) {
var pg = state.pages[type];
if (!pg) {
return {
page: type,
visited: 'NO — visit it',
perfkitConfigured: '-',
liveCall: '-',
};
}
var configured = pg.scriptPresent && pg.attrsOk;
var live = (pg.liveCalls || [])
.map(function (c) {
return c.method + (c.arg ? '(' + c.arg + ')' : '()');
})
.filter(function (v, i, a) {
return a.indexOf(v) === i;
})
.join(', ');
return {
page: type,
visited: 'yes',
perfkitConfigured:
(configured ? 'PASS' : pg.scriptPresent ? 'FAIL attrs' : 'FAIL no script') +
(pg.attrIssues && pg.attrIssues.length
? ' [' + pg.attrIssues.join('; ') + ']'
: ''),
liveCall: live || '(none — full reload; config still checked)',
};
});
if (console.table) console.table(rows);
else console.log(rows);
}
// ---- Shopify analytics (Monorail commerce events) ----------------------
// The data behind Shopify's sessions / conversions / product reports.
console.log(
'%c— Shopify analytics (sessions / conversions) —',
'font-weight:bold',
);
if (!analyticsSeen) {
console.log(
'No Shopify analytics (Monorail) events observed yet — browse the key pages, or check consent (analytics is blocked until the visitor consents).',
);
} else {
var aRows = CONTRACT.analyticsEvents.map(function (spec) {
var ev = an.events[spec.name];
var row = {
event: spec.label + ' (' + spec.name + ')',
fired: ev ? 'PASS' : 'not seen',
count: ev ? ev.count : 0,
payload: '-',
};
if (spec.products) {
row.payload = ev
? ev.issues && ev.issues.length
? 'FAIL [' + ev.issues.join('; ') + ']'
: ev.productCount + ' product(s) ok'
: '-';
}
return row;
});
if (console.table) console.table(aRows);
else console.log(aRows);
console.log(
'Session page-view event (trekkie): ' +
(an.pageViewSeen ? 'seen' : 'NOT seen') +
' | page types seen: ' +
(Object.keys(an.pageTypesSeen || {}).join(', ') || 'none'),
);
}
var visited = CONTRACT.expectedPages.filter(function (t) {
return state.pages[t];
}).length;
console.log(
'Visited ' +
visited +
'/' +
CONTRACT.expectedPages.length +
' key page types. Auto-updates as you browse (full-reload stores: re-run on each page). Reprint: __hydrogenValidator.report() · reset: __hydrogenValidator.reset()',
);
/* eslint-enable no-console */
return state;
}
// ----- Boot (runs every time the script is pasted/clicked) ----------------
// If we're already armed on THIS page (e.g. the snippet was run again on a
// SPA route), don't re-arm or re-schedule — just reprint once. Re-arming
// would stack extra timers and duplicate reports.
if (window[GLOBAL_KEY] && window[GLOBAL_KEY].__armed) {
try {
window[GLOBAL_KEY].report();
} catch (e) {}
return;
}
var state = loadState();
var type = recordCurrentPage(state);
saveState(state);
armLive(state);
// When injected at document-start (the DevTools "Script First Statement"
// flow), the PerfKit <script> tag and the buffered Performance entries don't
// exist yet, so the initial recordCurrentPage() captures nothing. Re-record
// after the page loads so the static audit + Performance evidence land.
function reRecordCurrent() {
try {
recordCurrentPage(state);
saveState(state);
scheduleReport();
} catch (e) {}
}
if (document.readyState === 'complete') {
setTimeout(reRecordCurrent, 0);
} else {
window.addEventListener('load', function () {
setTimeout(reRecordCurrent, 300);
});
}
setTimeout(reRecordCurrent, 1500);
setTimeout(reRecordCurrent, 4000);
window[GLOBAL_KEY] = {
__armed: true,
report: report,
contract: CONTRACT,
state: function () {
return loadState();
},
reset: function () {
try {
sessionStorage.removeItem(SS_KEY);
} catch (e) {}
lastReportSig = null;
/* eslint-disable-next-line no-console */
console.log('[validator] reset — cleared saved page results.');
},
};
/* eslint-disable no-console */
console.log(
'%c[validator] checked this page: %c' + type,
'color:#5a31f4',
'font-weight:bold',
);
var reported = report();
// Mark the just-printed state as reported so the post-load re-record timers
// don't immediately reprint the same thing.
lastReportSig = reportSignature();
var perfKitHere =
!!reported.perfKitVariant ||
Object.keys(reported.pages || {}).some(function (t) {
var p = reported.pages[t];
return p && (p.scriptPresent || p.perfKitLoaded || p.windowPerfKit);
});
if (perfKitHere) {
console.log(
'Now visit the other key pages (home, collection, product, cart). On a SPA store just browse — this report prints automatically as you go; on a full-reload store re-run this script on each page.',
);
}
/* eslint-enable no-console */
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment