Hydrogen publishes eight analytics events of its own: page_viewed, product_viewed, collection_viewed, cart_viewed, search_viewed, cart_updated, product_added_to_cart and product_removed_from_cart. To get them into GA4, Segment or your own warehouse, subscribe from a single component with useAnalytics(). The payload shape for each event is fixed by Hydrogen, not by you.
That last part is the bit people fight. A storefront that has already grown a useEffect in every route, each one hand-rolling a view event, is doing work the provider already did.
Which analytics events does Hydrogen publish?
The names are exported as AnalyticsEvent from @shopify/hydrogen, and the useAnalytics reference lists the subscribe signature for each one. Five end in _viewed and come from components you place on a route. The other three are produced by the provider itself, from changes in cart line quantity and cart line id.
Every payload carries the shop object and whatever you passed as customData on the provider. Most also carry url. Beyond that they differ. product_viewed gives you a products array, each entry holding id, title, price, vendor, variantId, variantTitle and quantity, with sku and productType optional. collection_viewed gives you collection.id and collection.handle. search_viewed gives you searchTerm and searchResults. The three cart events carry cart and prevCart, and the two line-level ones add prevLine and currentLine so you can work out what actually moved.
Note what is missing. There is no checkout_started and no purchase event, because checkout is not your app. Anything post-cart comes from Shopify’s own pixels, which is a different system with a different subscriber model; our write-up on app pixels versus custom pixels covers that side.
How do I forward those events to GA4 or Segment?
One component, mounted inside the provider. Subscribe in an effect, then tell the provider you are ready.
import {useEffect} from 'react';
import {useAnalytics} from '@shopify/hydrogen';
declare global {
interface Window {
dataLayer?: Array<Record<string, unknown>>;
}
}
export function AnalyticsBridge() {
const {subscribe, register, canTrack} = useAnalytics();
const {ready} = register('AnalyticsBridge');
useEffect(() => {
subscribe('product_viewed', ({products, shop}) => {
window.dataLayer?.push({
event: 'view_item',
shop_id: shop?.shopId,
items: products.map((product) => ({
item_id: product.variantId,
item_name: product.title,
item_brand: product.vendor,
price: product.price,
quantity: product.quantity,
})),
});
});
subscribe('product_added_to_cart', ({currentLine}) => {
if (!currentLine) return;
window.dataLayer?.push({
event: 'add_to_cart',
items: [
{
item_id: currentLine.merchandise.id,
quantity: currentLine.quantity,
},
],
});
});
ready();
}, []);
return null;
}
register is the line people delete. The reference describes it as delaying initial events until every registered key calls ready, and without it the page_viewed for the landing page publishes while your effect is still queued, so the first page of every session goes missing. Register during render, subscribe in the effect, call ready() at the end of it.
canTrack() is there if you want to branch on consent yourself, though you rarely need to. More on that below.
What has to be in root for any of this to fire?
Two things: a loader that returns shop and consent, and Analytics.Provider wrapping the app. Shopify’s tracking guide has the full file, and the shape is this.
import type {LoaderFunctionArgs} from 'react-router';
import {getShopAnalytics} from '@shopify/hydrogen';
export async function loader(args: LoaderFunctionArgs) {
const {storefront, env} = args.context;
return {
cart: args.context.cart.get(),
shop: getShopAnalytics({
storefront,
publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
}),
consent: {
checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
withPrivacyBanner: true,
country: storefront.i18n.country,
language: storefront.i18n.language,
},
};
}
Then <Analytics.Provider cart={data.cart} shop={data.shop} consent={data.consent}> with your layout and your bridge component inside it. cart accepts the promise, so nothing blocks on it.
The view events still need a trigger per route. Analytics.ProductView, Analytics.CollectionView and Analytics.SearchView take a data prop and publish on mount; Analytics.CartView needs no data prop. A cart drawer is the exception, because there is no route change to hang the component on, so publish by hand from the click handler with publish('cart_viewed', {cart, prevCart, shop, url: window.location.href || ''}).
Skeleton projects created since Hydrogen 2024.4.3 have all of this already. The docs say plainly that analytics is included by default from that version, and the setup steps are for upgrades.
Does Hydrogen check consent before it tracks?
Yes, by default. The canTrack prop on Analytics.Provider is documented as falling back to the Customer Privacy API’s window.Shopify.customerPrivacy.analyticsProcessingAllowed(), so the gate is Shopify’s consent state rather than a boolean you maintain.
That only holds if the consent config is right. checkoutDomain and storefrontAccessToken load the Customer Privacy API, and withPrivacyBanner: true loads the banner you configured in admin. One trap worth knowing before you test: Shopify’s consent guide says the cookie banner will not work on default Oxygen URLs (*.myshopify.dev), so run it locally or attach a real domain first. A banner that never appears on a preview deploy is usually this, not your code.
Rolling your own banner instead? useAnalytics() also returns customerPrivacy and privacyBanner, already configured with the values you passed to the provider.
Why are my cart events missing?
Almost always the cart query. Hydrogen derives cart_updated, product_added_to_cart and product_removed_from_cart by diffing cart state, and it needs a timestamp to know the cart changed at all. Leave updatedAt out of the fragment and you get this in the browser console:
[h2:error:CartAnalytics] Can't set up cart analytics events because the `cart.updatedAt` value is missing from your GraphQL cart query. In standard Hydrogen projects, the cart query is contained in `app/lib/fragments.js`. Make sure it includes `cart.updatedAt`.
Add the field to your cart fragment and the three events come back. View events are unaffected, which is why this often shows up as “add to cart never fires but product views do”.
When is a custom event the right answer?
When the thing you want to measure is yours. A size guide opening, a filter applied, a delivery estimate expanded. Publish it with a name prefixed custom_, which the type signature enforces as `custom_${string}`, and subscribe to it in the same bridge component next to the standard events. Shopify’s sample uses custom_checkbox_toggled.
What a custom event should not be is a duplicate of a standard one under a different name. Two publishers for the same user action means two numbers that disagree by a few percent, and a fortnight lost finding out why.
Set the provider up once, register one subscriber per destination, and let route components do the publishing. A third-party tag manager should sit behind that subscriber too; left to collect its own page views it puts consent enforcement in two places, and the two will disagree. The one case for leaving Hydrogen’s analytics alone and going straight to a server-side collector is a storefront where ad blockers already eat most client events, and even then the subscriber is how you get the data out. Event coverage is the second thing worth auditing on a Hydrogen storefront. The first is what it caches.
Versions, because these move: this was written against @shopify/hydrogen 2026.4.5, the current release on npm in September 2026, which declares a peer dependency on react-router ~7.16.0. The Hydrogen reference pages under /latest carry api_version 2026-04, while Shopify’s latest stable API version is 2026-07.
We build and maintain Hydrogen storefronts, including the analytics and consent plumbing that tends to get bolted on late and then audited under pressure. A second pair of eyes on a storefront’s event coverage, or a build that starts with it in place, is part of our Shopify development work.