hydrogen-analytics-tracking — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hydrogen-analytics-tracking (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build a complete tracking pipeline on Shopify Hydrogen: client dataLayer → GTM → browser pixels, AND server/api/track→ GA4 MP / Meta CAPI / Google Ads, with sharedevent_idfor cross-side deduplication. Covers consent mode v2, CSPstrict-dynamic, Oxygen full-page cache compatibility, and the surprising gotchas that bite every implementation.
This skill encodes hard-won lessons from production tracking work on Hydrogen storefronts. The reference files contain detailed implementations; this top page is the map.
You need this if you're:
Oxygen-Cache-Control header.If you just want page_view + Hydrogen's built-in <Analytics.Provider> cart events forwarded to GA4 via GTM, the Shopify docs are enough. Come here when you need the full funnel.
| Layer | Where it runs | Strengths | Weaknesses |
|---|---|---|---|
| Browser (GTM → pixels) | dataLayer.push() → GTM tags → GA4, Meta Pixel, Google Ads, TikTok | Rich user context, fbp/fbc cookies, instant client-side ECommerce events | ITP, ad-blockers, page-navigation race conditions |
| Server-side (`/api/track`) | Hydrogen worker → GA4 MP, Meta CAPI, Google Ads Enhanced Conversions | Survives ad-blockers, runs even when client unloads, can be triggered by webhooks | Loses some context (no fbp without forwarding), needs IP + UA + match keys |
| Vendor pipes you don't control | Shopify "Google & YouTube" sales channel app, Shopify Customer Events Pixel | Works inside Shopify checkout (where merchant GTM can't go), Shopify-blessed | Limited customization, can DUPLICATE merchant GTM if same vendor set up twice |
The combination matters. A complete pipeline uses all three: GTM for storefront pages, server-side for resilience and dedup, vendor pipes for checkout pages (which Shopify Plus locks down).
The cornerstone pattern. Every trackable event:
event_id → GTM → browser pixels send the hit with event_id as the dedup key.navigator.sendBeacon with the same event_id → server forwards to GA4 MP / Meta CAPI / Google Ads with the same key.(event_name, event_id) → exactly one count, not two.function trackEvent({ event_name, custom_data, user_data }) {
const event_id = crypto.randomUUID();
// (1) Browser side
window.dataLayer.push({ event: event_name, event_id, ...custom_data });
// (2) Server side, same event_id
const payload = { event_id, event_name, custom_data, user_data, consent };
navigator.sendBeacon("/api/track", new Blob([JSON.stringify(payload)]));
return event_id;
}Add-to-cart, begin_checkout, "Buy now" — these all trigger page navigation immediately after. A regular fetch() gets cancelled when the page unloads, losing the event. sendBeacon is the browser API designed exactly for this: the request is queued by the browser and guaranteed to be sent even after navigation. Fall back to fetch(..., {keepalive: true}) if sendBeacon isn't available.
If the server generates event_id, the browser already pushed its dataLayer event with a different (or no) id, and there's no way to backfill. Always generate client-side, send both directions with the same value.
A/B tests on a Weaverse storefront use @weaverse/experiments — deterministic, project-level variant assignment. Exposure rides the same pipeline as every other event:
<Analytics.Provider customData={{ experiments: { '<id>': '<variant>' } }}>. customData is merged into every event, so add_to_cart / purchase are already tagged with the variant — this is what measures conversion impact, not just impressions. No need to re-attach the experiment per event.useAnalytics().publish('custom_experiment_viewed', { experimentId, variantId }) from the experiments onExpose callback, gated on canTrack(). Bridge custom_experiment_viewed → dataLayer/GA4 in your <CustomAnalytics> subscriber like any other custom event (custom_ prefix required; call ready()).event_id dual-send is usually unnecessary. If you forward it to /api/track, reuse the storefront trackEvent() helper.Server-side getExperiments() wiring lives in the weaverse-hydrogen skill (Multi-Project Architecture → A/B Testing).
Read these in order if you're implementing from scratch. Skip to the relevant one if you're debugging:
| Reference | Read if you're… |
|---|---|
architecture.md | Setting up the whole pipeline. Covers the dual-send pattern, dedup contract, vendor responsibilities, and how the pieces fit together. |
gtm-meta-implementation.md | Wiring up GTM dataLayer pushes, GA4 Event tags, Meta CAPI forwarder. Real code patterns. |
webhook-forwarding-via-builder.md | Weaverse-hosted storefronts: how Shopify webhooks reach your storefront without leaking the multi-tenant app client secret. Uses the builder WebhookForward model + per-store signing secrets. |
cart-attribute-stash.md | Bridging the webhook cookie gap: how to get _fbp / _fbc / gclid / affiliate click IDs from the browser into the Shopify orders webhook. Covers the two cart entry paths (POST action AND /cart/<id>:<qty> loader) that both need stash logic. |
oxygen-full-page-cache.md | Configuring FPC, why Set-Cookie disables it, the entry.server.tsx strip trick. |
csp-for-tracking.md | CSP directives that allow Google/Meta/Hotjar; nonce vs strict-dynamic; GTM Custom HTML tags and inline-script violations. |
gotchas.md | The bugs that bite every implementation. Read this first if something isn't working. |
add_to_cart directly from the button onClick handler via sendBeacon (it survives the form submit / navigation).strict-dynamic CSP. Fix: load gtm.js as a regular <script async nonce={nonce}> in <head>, with the inline gtm.start + Consent Mode v2 default-deny block before it.view_item doesn't fire. Fix: mount unconditionally with safe per-variant fallbacks.ad_storage !== "granted" drop hashed PII but still send the event with data_processing_options: ["LDU"].{{ product.id }}, {{ collection.title }}, {{ product.price | money_without_currency }}). These do nothing in Hydrogen — it's React/SSR, there is no Liquid at runtime, so the braces render as literal text or break. Fix: rebuild the same object from Hydrogen data and push it in JS. Liquid → Hydrogen mapping: {{ product.id }} → product.id, {{ product.title }} → product.title, {{ product.price | money_without_currency }} → product.priceRange?.minVariantPrice?.amount (a string, no currency symbol), {{ collection.id/title }} → collection.id/title.select_item is a click (user clicks a product card in a list) — fire it from the product card's onClick on the collection/PLP, where you already hold the product + collection + index. view_item_list is a view — push it from the COLLECTION_VIEWED subscriber in app/components/root/custom-analytics.tsx. Include index (list position) for GA4. Ensure the collection query returns id, title, handle, and priceRange so the values exist to push.If you're starting fresh on a new Hydrogen storefront:
<CustomAnalytics /> component. (See architecture.md)sendBeacon('/api/track') with shared event_id.purchase event with event_id = "purchase_" + orderId (deterministic for retries).script-src, connect-src, img-src. Use strict-dynamic + nonce.Oxygen-Cache-Control: public, max-age=N, ... header. Strip Set-Cookie from cacheable responses in entry.server.tsx.When working on a Hydrogen tracking implementation in this skill's scope:
trackEvent, consent listener, attribution capture).<Analytics.Provider> events).{forwarder, ok, skipped?, reason?} so the audit log can show why an event was dropped.When a question is broader than a single vendor, prefer the reference doc that addresses the architectural layer rather than one vendor's docs.
For up-to-date official sources:
# Shopify Hydrogen / Oxygen
node scripts/search_shopify_docs.mjs "oxygen full-page cache"
node scripts/search_shopify_docs.mjs "consent mode"
node scripts/search_shopify_docs.mjs "analytics provider"
# Weaverse (if using Weaverse CMS)
node scripts/search_weaverse_docs.mjs "csp"Vendor docs (open in browser, no script):
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.