{"id":"2a4e98ed-9bb5-4f36-b505-4e8a65ca66cd","shortId":"ukZJ2F","kind":"skill","title":"hydrogen-analytics-tracking","tagline":"End-to-end analytics & conversion tracking on Shopify Hydrogen — GTM, GA4 (browser + Measurement Protocol), Meta Pixel + CAPI, Google Ads, consent mode, CSP, Oxygen full-page cache. Real-world patterns from production deployments.","description":"# Hydrogen Analytics & Tracking — Agent Skill\n\n> Build a complete tracking pipeline on Shopify Hydrogen: client dataLayer → GTM → browser pixels, AND server `/api/track` → GA4 MP / Meta CAPI / Google Ads, with shared `event_id` for cross-side deduplication. Covers consent mode v2, CSP `strict-dynamic`, Oxygen full-page cache compatibility, and the surprising gotchas that bite every implementation.\n\nThis skill encodes hard-won lessons from production tracking work on Hydrogen storefronts. The reference files contain detailed implementations; this top page is the map.\n\n---\n\n## When to use this skill\n\nYou need this if you're:\n\n- Implementing GA4 / Meta / Google Ads / TikTok tracking on Hydrogen and the default Hydrogen Analytics components aren't enough.\n- Adding **server-side tracking** (Measurement Protocol, Conversions API) for resilience against ad-blockers and ITP.\n- Debugging \"event X is in GTM Preview but not in GA4 / Meta\".\n- Wiring up **conversion deduplication** between browser pixel and server CAPI.\n- Setting up tracking on a Hydrogen storefront with **Weaverse** as the CMS layer.\n- Investigating why **Oxygen full-page cache** is being disabled despite a correct `Oxygen-Cache-Control` header.\n\nIf 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.\n\n---\n\n## The mental model\n\n### Three layers of tracking\n\n| Layer | Where it runs | Strengths | Weaknesses |\n|---|---|---|---|\n| **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 |\n| **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 |\n| **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 |\n\n**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).\n\n### Dual-send + event_id dedup\n\nThe cornerstone pattern. Every trackable event:\n\n1. **Generates a UUID `event_id` once** on the client.\n2. **Pushes to `dataLayer`** with that `event_id` → GTM → browser pixels send the hit with `event_id` as the dedup key.\n3. **POSTs to `/api/track`** via `navigator.sendBeacon` with the same `event_id` → server forwards to GA4 MP / Meta CAPI / Google Ads with the same key.\n4. Each vendor's backend dedupes on `(event_name, event_id)` → exactly one count, not two.\n\n```ts\nfunction trackEvent({ event_name, custom_data, user_data }) {\n  const event_id = crypto.randomUUID();\n\n  // (1) Browser side\n  window.dataLayer.push({ event: event_name, event_id, ...custom_data });\n\n  // (2) Server side, same event_id\n  const payload = { event_id, event_name, custom_data, user_data, consent };\n  navigator.sendBeacon(\"/api/track\", new Blob([JSON.stringify(payload)]));\n\n  return event_id;\n}\n```\n\n### Why sendBeacon, why not fetch?\n\nAdd-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.\n\n### Why event_id can't come from the server\n\nIf 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.\n\n---\n\n## Reference files\n\nRead these in order if you're implementing from scratch. Skip to the relevant one if you're debugging:\n\n| Reference | Read if you're… |\n|---|---|\n| [`architecture.md`](./references/architecture.md) | Setting up the whole pipeline. Covers the dual-send pattern, dedup contract, vendor responsibilities, and how the pieces fit together. |\n| [`gtm-meta-implementation.md`](./references/gtm-meta-implementation.md) | Wiring up GTM dataLayer pushes, GA4 Event tags, Meta CAPI forwarder. Real code patterns. |\n| [`webhook-forwarding-via-builder.md`](./references/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. |\n| [`cart-attribute-stash.md`](./references/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. |\n| [`oxygen-full-page-cache.md`](./references/oxygen-full-page-cache.md) | Configuring FPC, why `Set-Cookie` disables it, the `entry.server.tsx` strip trick. |\n| [`csp-for-tracking.md`](./references/csp-for-tracking.md) | CSP directives that allow Google/Meta/Hotjar; nonce vs strict-dynamic; GTM Custom HTML tags and inline-script violations. |\n| [`gotchas.md`](./references/gotchas.md) | The bugs that bite every implementation. Read this first if something isn't working. |\n\n---\n\n## Five things every Hydrogen tracking implementation gets wrong\n\n1. **Using Hydrogen's `PRODUCT_ADD_TO_CART` analytics event for `add_to_cart`.** Hydrogen diffs cart state after revalidation and emits the event then. The timing is unreliable — events often miss GA4 DebugView entirely. **Fix:** fire `add_to_cart` directly from the button onClick handler via `sendBeacon` (it survives the form submit / navigation).\n\n2. **Loading GTM after hydration via `<Script waitForHydration>`.** It hides GTM from Tag Assistant standalone scans and blocks the move to nonce-based `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.\n\n3. **Pushing GA4-named events but configuring GTM triggers with legacy snake_case names** (or vice versa). After \"Custom Event\" renaming there's a coverage gap. **Fix:** match GTM trigger filters to whatever the storefront actually pushes today; do code + GTM in one coordinated change.\n\n4. **Letting `<Analytics.ProductView>` gate on `selectedVariant`.** For combined listings or any product where the variant resolves after hydration, the analytics component never mounts and `view_item` doesn't fire. **Fix:** mount unconditionally with safe per-variant fallbacks.\n\n5. **Treating \"consent denied\" as \"send nothing\".** Meta CAPI's relaxed pattern (LDU flag + ip/ua/fbp/fbc only, no hashed PII) recovers a large chunk of optimisation signal compliantly. GA4 Consent Mode v2 modeled conversions work the same way. **Fix:** in the server forwarder, when `ad_storage !== \"granted\"` drop hashed PII but still send the event with `data_processing_options: [\"LDU\"]`.\n\n---\n\n## The order to build it\n\nIf you're starting fresh on a new Hydrogen storefront:\n\n1. **Hydrogen `<Analytics.Provider>` wired at root.** Subscribe to its events in a `<CustomAnalytics />` component. (See [`architecture.md`](./references/architecture.md))\n2. **Inline `<head>` Consent Mode v2 default-deny block + dataLayer + gtm.start marker.**\n3. **`gtm.js` external script with nonce, async, in `<head>` after the inline block.**\n4. **`trackEvent()` helper** that pushes dataLayer + `sendBeacon('/api/track')` with shared `event_id`.\n5. **`/api/track` server endpoint** that validates the payload, hashes PII server-side, fans out to GA4 MP + Meta CAPI + Google Ads forwarders.\n6. **Shopify `orders/create` webhook** that maps the order to a `purchase` event with `event_id = \"purchase_\" + orderId` (deterministic for retries).\n7. **Shopify \"Google & YouTube\" sales channel + Customer Events Pixel** for checkout-side events (Meta Pixel events, anything that needs to fire inside Shopify checkout where your GTM can't reach).\n8. **GTM container** with one GA4 Event tag per dataLayer event, plus Meta Pixel + TikTok + Google Ads conversion tags as needed.\n9. **CSP** updated to allow all vendor domains in `script-src`, `connect-src`, `img-src`. Use `strict-dynamic` + nonce.\n10. **Oxygen full-page cache** opted in per route via `Oxygen-Cache-Control: public, max-age=N, ...` header. Strip `Set-Cookie` from cacheable responses in `entry.server.tsx`.\n\n---\n\n## Skill-level conventions\n\nWhen working on a Hydrogen tracking implementation in this skill's scope:\n\n- **Server code lives under `app/.server/tracking/`** (forwarders, validators, hash util, audit log).\n- **Client helper at `app/utils/track-client.ts`** (exports `trackEvent`, consent listener, attribution capture).\n- **dataLayer bridge at `app/components/root/custom-analytics.tsx`** (subscribes to Hydrogen `<Analytics.Provider>` events).\n- **Inline Consent Mode + GTM bootstrap in `app/root.tsx` `<head>`**, with nonce.\n- **CSP config at `app/weaverse/csp.ts`** (Weaverse projects) or wherever your storefront sets CSP.\n- **Per-vendor forwarder modules at `app/.server/tracking/forwarders/{ga4,meta-capi,google-ads}.ts`** — each returns `{forwarder, ok, skipped?, reason?}` so the audit log can show why an event was dropped.\n\nWhen a question is broader than a single vendor, prefer the reference doc that addresses the architectural layer rather than one vendor's docs.\n\n---\n\n## Live docs\n\nFor up-to-date official sources:\n\n```bash\n# Shopify Hydrogen / Oxygen\nnode scripts/search_shopify_docs.mjs \"oxygen full-page cache\"\nnode scripts/search_shopify_docs.mjs \"consent mode\"\nnode scripts/search_shopify_docs.mjs \"analytics provider\"\n\n# Weaverse (if using Weaverse CMS)\nnode scripts/search_weaverse_docs.mjs \"csp\"\n```\n\nVendor docs (open in browser, no script):\n- GA4 Measurement Protocol — https://developers.google.com/analytics/devguides/collection/protocol/ga4\n- Meta Conversions API — https://developers.facebook.com/docs/marketing-api/conversions-api\n- Google Ads Enhanced Conversions for Web — https://developers.google.com/google-ads/api/docs/conversions/enhanced-conversions-for-web\n- Shopify \"Customer Events\" / Web Pixels — https://shopify.dev/docs/api/web-pixels-api","tags":["hydrogen","analytics","tracking","shopify","skills","weaverse","agent-skills","agentic-commerce","shopify-hydrogen","shopify-hydrogen-skills","weaverse-hydrogen"],"capabilities":["skill","source-weaverse","skill-hydrogen-analytics-tracking","topic-agent-skills","topic-agentic-commerce","topic-shopify-hydrogen","topic-shopify-hydrogen-skills","topic-weaverse-hydrogen"],"categories":["shopify-hydrogen-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/Weaverse/shopify-hydrogen-skills/hydrogen-analytics-tracking","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add Weaverse/shopify-hydrogen-skills","source_repo":"https://github.com/Weaverse/shopify-hydrogen-skills","install_from":"skills.sh"}},"qualityScore":"0.467","qualityRationale":"deterministic score 0.47 from registry signals: · indexed on github topic:agent-skills · 34 github stars · SKILL.md body (10,687 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:04:12.098Z","embedding":null,"createdAt":"2026-05-13T13:00:30.403Z","updatedAt":"2026-05-18T19:04:12.098Z","lastSeenAt":"2026-05-18T19:04:12.098Z","tsv":"'/api/track':60,302,456,535 '/cart':786 '/references/architecture.md':684 '/references/cart-attribute-stash.md':754 '/references/csp-for-tracking.md':808 '/references/gotchas.md':829 '/references/gtm-meta-implementation.md':707 '/references/oxygen-full-page-cache.md':794 '/references/webhook-forwarding-via-builder.md':723 '1':422,506,852 '2':432,517,906 '3':453 '4':477 'action':784 'ad':24,66,139,153,166,277,292,310,315,472 'ad-block':165,291,314 'add':549,857,863,889 'add-to-cart':548 'affili':766 'agent':43 'allow':812 'alreadi':627 'alway':645 'analyt':3,9,41,148,860 'api':161,579 'app':350,740 'architecture.md':683 'aren':150 'avail':609 'back':600 'backend':481 'backfil':644 'begin':552 'bite':95,833 'bless':367 'blob':537 'blocker':167,293,316 'bridg':755 'browser':17,56,187,267,441,507,578,590,626,771 'bug':831 'build':45 'builder':745 'built':232 'built-in':231 'button':895 'buy':554 'cach':32,88,211,220 'cancel':567 'capi':22,64,191,308,470,717 'cart':234,551,780,859,865,868,891 'cart-attribute-stash.md':753 'channel':349 'checkout':358,403,553 'click':767 'client':53,286,320,431,648,741 'client-sid':285,647 'cms':203 'code':720 'combin':381 'come':246,615 'compat':89 'complet':47,384 'compon':149 'condit':298 'configur':795 'consent':25,77,533 'const':502,523 'contain':115 'context':281,329 'contract':697 'control':221,344 'convers':10,160,184,312 'cooki':283,758,800 'cornerston':417 'correct':217 'count':490 'cover':76,690,777 'cross':73 'cross-sid':72 'crypto.randomuuid':505 'csp':27,80,809 'csp-for-tracking.md':807 'custom':352,369,498,515,529,820 'data':499,501,516,530,532 'datalay':54,435,630,711 'datalayer.push':270 'debug':170,677 'debugview':885 'dedup':399,415,451,482,696 'dedupl':75,185 'default':146 'deploy':39 'design':580 'despit':215 'detail':116 'diff':867 'differ':634 'direct':652,810,892 'disabl':214,801 'doc':243 'dual':411,693 'dual-send':410,692 'duplic':371 'dynam':83,818 'ecommerc':288 'emit':873 'encod':100 'end':6,8 'end-to-end':5 'enhanc':311 'enough':152,245 'entir':886 'entri':781 'entry.server.tsx':804 'even':318,596 'event':69,171,235,289,353,413,421,426,438,447,462,484,486,496,503,510,511,513,521,525,527,541,574,611,623,631,714,861,875,881 'everi':96,419,834,846 'exact':488,581 'fall':599 'fbc':764 'fbp':331,763 'fbp/fbc':282 'fetch':547,565,602 'file':114,658 'fire':888 'first':838 'fit':704 'five':844 'fix':887 'form':903 'forward':236,333,465,718 'fpc':796 'full':30,86,209,252 'full-pag':29,85,208 'function':494 'funnel':253 'ga4':16,61,136,180,238,273,305,467,713,884 'gap':759 'gclid':765 'generat':423,622,646 'get':566,762,850 'go':364 'googl':23,65,138,276,309,346,471 'google/meta/hotjar':813 'gotcha':93 'gotchas.md':828 'gtm':15,55,175,240,268,271,361,373,389,440,710,819,908 'gtm-meta-implementation.md':706 'guarante':592 'handler':897 'hard':102 'hard-won':101 'header':222 'hit':445 'host':726 'html':821 'hydrat':910 'hydrogen':2,14,40,52,110,143,147,197,229,303,847,854,866 'hydrogen-analytics-track':1 'id':70,414,427,439,448,463,487,504,514,522,526,542,612,624,637,768 'immedi':561 'implement':97,117,135,666,835,849 'inlin':825 'inline-script':824 'insid':356 'instant':284 'investig':205 'ip':335 'isn':607,841 'itp':169,290 'json.stringify':538 'keepal':603 'key':338,452,476 'layer':204,258,261 'leak':735 'lesson':104 'limit':368 'load':907 'loader':787 'lock':408 'logic':792 'lose':327,572 'map':123 'match':337 'matter':382 'measur':18,158 'mental':255 'merchant':360,372 'meta':20,63,137,181,274,307,469,716 'miss':883 'mode':26,78 'model':256,747 'mp':62,306,468 'multi':738 'multi-ten':737 'name':485,497,512,528 'navig':296,560,598,905 'navigator.sendbeacon':458,534 'need':130,250,334,790 'new':536 'nonc':814 'often':882 'onclick':896 'one':489,673 'order':662,775 'oxygen':28,84,207,219 'oxygen-cache-control':218 'oxygen-full-page-cache.md':793 'page':31,87,120,210,227,295,392,404,559,570 'page-navig':294 'path':782 'pattern':36,418,695,721 'payload':524,539 'per':749 'per-stor':748 'piec':703 'pipe':340,401 'pipelin':49,385,689 'pixel':21,57,188,269,275,354,442 'plus':407 'post':454,783 'preview':176 'product':38,106,856 'protocol':19,159 'push':433,628,712 'queu':587 'race':297 're':134,665,676,682 'reach':731 'read':659,679,836 'real':34,719 'real-world':33 'refer':113,657,678 'regular':564 'relev':672 'request':585 'resili':163,397 'respons':699 'return':540 'revalid':871 'rich':279 'run':264,317 'sale':348 'scratch':668 'script':826 'secret':742,752 'send':412,443,650,694 'sendbeacon':544,575,606,899 'sent':595 'server':59,155,190,300,394,464,518,618,621 'server-sid':154,299,393 'set':192,377,685,799 'set-cooki':798 'share':68 'shopifi':13,51,242,345,351,357,366,406,729,774 'shopify-bless':365 'side':74,156,287,301,395,508,519,649 'sign':751 'skill':44,99,128 'skill-hydrogen-analytics-tracking' 'skip':669 'someth':840 'source-weaverse' 'stash':791 'state':869 'store':750 'storefront':111,198,391,727,733 'strength':265 'strict':82,817 'strict-dynam':81,816 'strip':805 'submit':904 'surpris':92 'surviv':313,901 'tag':272,715,822 'tenant':739 'thing':845 'three':257,388 'tiktok':140,278 'time':878 'togeth':705 'top':119 'topic-agent-skills' 'topic-agentic-commerce' 'topic-shopify-hydrogen' 'topic-shopify-hydrogen-skills' 'topic-weaverse-hydrogen' 'track':4,11,42,48,107,141,157,194,260,848 'trackabl':420 'trackev':495 'trick':806 'trigger':324,558 'true':604 'ts':493 'twice':379 'two':492,779 'ua':336 'unload':321,571 'unreli':880 'use':126,386,743,853 'user':280,500,531 'uuid':425 'v2':79 'valu':656 'vendor':339,376,400,479,698 'via':239,457,898,911 'view':228 'violat':827 'vs':815 'want':226 'way':642 'weak':266 'weavers':200,725 'weaverse-host':724 'webhook':326,730,757,776 'webhook-forwarding-via-builder.md':722 'webhookforward':746 'whole':688 'window.datalayer.push':509 'wire':182,708 'without':332,734 'won':103 'work':108,355,843 'worker':304 'world':35 'wrong':851 'x':172 'youtub':347","prices":[{"id":"b9b5a3c6-02df-4e21-b526-26a3844edb54","listingId":"2a4e98ed-9bb5-4f36-b505-4e8a65ca66cd","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"Weaverse","category":"shopify-hydrogen-skills","install_from":"skills.sh"},"createdAt":"2026-05-13T13:00:30.403Z"}],"sources":[{"listingId":"2a4e98ed-9bb5-4f36-b505-4e8a65ca66cd","source":"github","sourceId":"Weaverse/shopify-hydrogen-skills/hydrogen-analytics-tracking","sourceUrl":"https://github.com/Weaverse/shopify-hydrogen-skills/tree/main/skills/hydrogen-analytics-tracking","isPrimary":false,"firstSeenAt":"2026-05-13T13:00:30.403Z","lastSeenAt":"2026-05-18T19:04:12.098Z"}],"details":{"listingId":"2a4e98ed-9bb5-4f36-b505-4e8a65ca66cd","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"Weaverse","slug":"hydrogen-analytics-tracking","github":{"repo":"Weaverse/shopify-hydrogen-skills","stars":34,"topics":["agent-skills","agentic-commerce","shopify-hydrogen","shopify-hydrogen-skills","weaverse-hydrogen"],"license":null,"html_url":"https://github.com/Weaverse/shopify-hydrogen-skills","pushed_at":"2026-05-16T06:13:59Z","description":"Dedicated agent skills for building, upgrading, and maintaining Shopify Hydrogen storefronts — works with Claude, Cursor, Copilot, and more.","skill_md_sha":"cc7203ecf7717f552d632f27ab65dd1941359e97","skill_md_path":"skills/hydrogen-analytics-tracking/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/Weaverse/shopify-hydrogen-skills/tree/main/skills/hydrogen-analytics-tracking"},"layout":"multi","source":"github","category":"shopify-hydrogen-skills","frontmatter":{"name":"hydrogen-analytics-tracking","description":"End-to-end analytics & conversion tracking on Shopify Hydrogen — GTM, GA4 (browser + Measurement Protocol), Meta Pixel + CAPI, Google Ads, consent mode, CSP, Oxygen full-page cache. Real-world patterns from production deployments."},"skills_sh_url":"https://skills.sh/Weaverse/shopify-hydrogen-skills/hydrogen-analytics-tracking"},"updatedAt":"2026-05-18T19:04:12.098Z"}}