|
/** |
|
* 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 */ |
|
})(); |