This folder contains an end-to-end example of how to implement analytics for Hydrogen. Hydrogen supports both Shopify analytics, as well as third-party services.
Hydrogen includes built in support for the Customer Privacy API, a browser-based JavaScript API that you can use to display cookie-consent banners and verify data processing permissions.
- Configure customer privacy settings - You can configure and manage customer privacy settings within your Shopify admin to help comply with privacy and data protection laws.
- Add a cookie banner - A cookie banner is a notification displayed on a website that informs visitors about the use of cookies and asks for their consent for data collection and tracking activities.
Set up a new project with this example:
npm create @shopify/hydrogen@latest -- --template analytics
The following files have been added (🆕) or changed from the default Hydration template:
File | Description |
---|---|
🆕 .env.example |
Example environment variable file. Adds a new required env variable PUBLIC_CHECKOUT_DOMAIN |
🆕 app/components/CustomAnalytics.tsx |
A component that subscribes to all default analytics events and can be used to publish events to third-party services. |
env.d.ts |
Updated Env interface to include PUBLIC_CHECKOUT_DOMAIN . Required for TypeScript only. |
app/root.tsx |
Updated the root layout with the Analytics provider and getShopAnalytics |
app/entry.server.tsx |
Updated the createContentSecurityPolicy with checkoutDomain and storeDomain properties |
app/routes/products.$handle.tsx |
Added Analytics.ProductView component |
app/routes/collections.$handle.tsx |
Added Analytics.CollectionView component |
app/routes/cart.tsx |
Added Analytics.CartView component |
app/routes/search.tsx |
Added Analytics.SearchView component |
In the Shopify admin, head over to / Settings / Customer Privacy / Cookie Banner
- In your Hydrogen app, create the new files from the file list above, copying in the code as you go.
- If you already have a
.env
file, copy over these key-value pairs:PUBLIC_CHECKOUT_DOMAIN
- e.gcheckout.hydrogen.shop
Tip
Importing UNSTABLE_Analytics as Analytics
makes it easier to upgrade to the stable component later, since you’ll only need to update your import statements.
import {
useNonce,
+ UNSTABLE_Analytics as Analytics,
+ getShopAnalytics
} from '@shopify/hydrogen';
+ import {CustomAnalytics} from '~/components/CustomAnalytics'
export async function loader({context}: LoaderFunctionArgs) {
+ // 1. Extract the `env` from the context
+ const {storefront, customerAccount, cart, env} = context;
// ...other code
return defer(
{
// ...other code
+ // 2. return the `shop` environment for analytics
+ shop: getShopAnalytics({
+ storefront: storefront,
+ publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
+ }),
+ // 3. return the `consent` config for analytics
+ consent: {
+ checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
+ storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
+ },
},
// other code...
);
}
Wrap the application Layout
with the Analytics
provider. The analytics provider is
responsible for managing and orchestrating cart, custom and page view events.
export default function App() {
const nonce = useNonce();
const data = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
+ <Analytics.Provider
+ cart={data.cart}
+ shop={data.shop}
+ consent={data.consent}
+ customData={{foo: 'bar'}}
+ >
<Layout {...data}>
<Outlet />
</Layout>
+ </Analytics.Provider>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);
}
Add the CustomAnalytics
component to listen to events:
export default function App() {
const nonce = useNonce();
const data = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Analytics.Provider
cart={data.cart}
shop={data.shop}
consent={data.consent}
customData={{foo: 'bar'}}
>
<Layout {...data}>
<Outlet />
</Layout>
+ <CustomAnalytics />
</Analytics.Provider>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);
}
View the complete component file to see these updates in context.
Add the Analytics.ProductView
component to the product details page route, /app/routes/product.$handle.tsx
:
import {
//...other code
+ UNSTABLE_Analytics as Analytics,
} from '@shopify/hydrogen';
export default function Product() {
const {product, variants} = useLoaderData<typeof loader>();
const {selectedVariant} = product;
return (
<div className="product">
<ProductImage image={selectedVariant?.image} />
<ProductMain
selectedVariant={selectedVariant}
product={product}
variants={variants}
/>
+ <Analytics.ProductView
+ data={{
+ products: [
+ {
+ id: product.id,
+ title: product.title,
+ price: selectedVariant?.price.amount || '0',
+ vendor: product.vendor,
+ variantId: selectedVariant?.id || '',
+ variantTitle: selectedVariant?.title || '',
+ quantity: 1,
+ },
+ ],
+ }}
+ />
</div>
);
}
Add the Analytics.CollectionView
component to the collection route, /app/routes/collection.$handle.tsx
:
import {
//...other code
+ UNSTABLE_Analytics as Analytics,
} from '@shopify/hydrogen';
export default function Collection() {
const {collection} = useLoaderData<typeof loader>();
return (
<div className="collection">
<h1>{collection.title}</h1>
<p className="collection-description">{collection.description}</p>
<Pagination connection={collection.products}>
{({nodes, isLoading, PreviousLink, NextLink}) => (
<>
<PreviousLink>
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
</PreviousLink>
<ProductsGrid products={nodes} />
<br />
<NextLink>
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
</NextLink>
</>
)}
</Pagination>
+ <Analytics.CollectionView
+ data={{
+ collection: {
+ id: collection.id,
+ handle: collection.handle,
+ },
+ }}
+ />
</div>
);
}
Add the Analytics.CartView
component to the cart route /app/routes/cart.tsx
import {
//...other code
+ UNSTABLE_Analytics as Analytics,
} from '@shopify/hydrogen';
export default function Cart() {
const rootData = useRootLoaderData();
const cartPromise = rootData.cart;
return (
<div className="cart">
<h1>Cart</h1>
<Suspense fallback={<p>Loading cart ...</p>}>
<Await
resolve={cartPromise}
errorElement={<div>An error occurred</div>}
>
{(cart) => {
return <CartMain layout="page" cart={cart} />;
}}
</Await>
</Suspense>
+ <Analytics.CartView />
</div>
);
}
Add the Analytics.SearchView
component to the search route /app/routes/search.tsx
```diff
import {
//...other code
+ UNSTABLE_Analytics as Analytics,
} from '@shopify/hydrogen';
export default function SearchPage() {
const {searchTerm, searchResults} = useLoaderData<typeof loader>();
return (
<div className="search">
<h1>Search</h1>
<SearchForm searchTerm={searchTerm} />
{!searchTerm || !searchResults.totalResults ? (
<NoSearchResults />
) : (
<SearchResults
results={searchResults.results}
searchTerm={searchTerm}
/>
)}
+ <Analytics.SearchView
+ data={{searchTerm, searchResults}}
+ />
</div>
);
}
Add storeDomain
and checkoutDomain
to the Content-Security-Policy
//...other code
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
+ context: AppLoadContext,
) {
- const {nonce, header, NonceProvider} = createContentSecurityPolicy();
+ const {nonce, header, NonceProvider} = createContentSecurityPolicy({
+ shop: {
+ checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
+ storeDomain: context.env.PUBLIC_STORE_DOMAIN,
+ },
+ });
//...other code
}
View the complete component file to see these updates in context.
Modify app/components/Header.tsx
to trigger a custom_sidecart_viewed
when the cart icon
is toggled
+ import {useAnalytics} from '@shopify/hydrogen'
function CartToggle({cart}: Pick<HeaderProps, 'cart'>) {
+ const {publish} = useAnalytics();
+ function publishSideCartViewed() {
+ publish('custom_sidecart_viewed', {cart});
+ }
return (
<Suspense
- fallback={<CartBadge count={0} />}
+ fallback={<CartBadge count={0} onClick={publishSideCartViewed} />}
>
<Await resolve={cart}>
{(cart) => {
if (!cart)
- return <CartBadge count={0} />;
+ return <CartBadge count={0} onClick={publishSideCartViewed} />;
return (
<CartBadge
count={cart.totalQuantity || 0}
+ onClick={publishSideCartViewed}
/>
);
}}
</Await>
</Suspense>
);
}
View the complete component file to see these updates in context.
Update the remix.d.ts
file
// ...other code
declare global {
/**
* A global `process` object is only available during build to access NODE_ENV.
*/
const process: {env: {NODE_ENV: 'production' | 'development'}};
/**
* Declare expected Env parameter in fetch handler.
*/
interface Env {
SESSION_SECRET: string;
PUBLIC_STOREFRONT_API_TOKEN: string;
PRIVATE_STOREFRONT_API_TOKEN: string;
PUBLIC_STORE_DOMAIN: string;
PUBLIC_STOREFRONT_ID: string;
+ PUBLIC_CHECKOUT_DOMAIN: string;
}
}
// ...other code
View the complete component file to see these updates in context.