Analytics
Shopify collects analytics for headless storefronts the same way it does for themes, powering the reports and live view in your admin. This module wires that up for you: drop-in components and a composable cover product, collection, search, and cart views, add-to-cart, and consent.
Under the hood it uses Shopify's own @shopify/hydrogen-react
to send events, so you get the exact payloads Shopify expects without reimplementing them.
@shopify/hydrogen-react. Install it before enabling analytics in the module:npm install @shopify/hydrogen-react
Getting started
Analytics is opt-in. Once @shopify/hydrogen-react is installed, you can enable it in your
nuxt.config.ts:
export default defineNuxtConfig({
shopify: {
name: 'quickstart-abcd1234',
clients: {
storefront: {
publicAccessToken: '...',
},
},
analytics: true,
},
})
Configuration
The analytics option accepts true, false (default), or an object with the following settings, all optional:
0).<name>.myshopify.com.gid://shopify/Shop/<id>. The automatic shop lookup is only skipped when shopId, currency and
language are all set.USD). Resolved from the Storefront API when omitted. Only the default market, see
Buyer market for multi-market shops.EN). Resolved from the Storefront API when omitted. Only the default market, see
Buyer market for multi-market shops.page_viewed event on the initial load and on every client-side navigation. Defaults to true; set it to
false to publish page views yourself.Tracking views
Page views are automatic — there is no component to place. The module reports one page_viewed event per URL, on the
initial load and on every client-side navigation. Reporting waits until the shop has resolved, so the first view carries
the shop identifiers and still reaches subscribers registered after the module. Views are keyed on path and query, so
jumping to an anchor on the same page does not report a second view.
Views that Shopify understands (product, collection, search, cart) have a renderless component each. The product,
collection, and search components also re-report when the item they describe changes (the product, the collection,
or the search term), which covers client-side navigation between two products. ShopifyCartView reports once per mount.
<template>
<!-- your product page -->
<ShopifyProductView
:data="{
products: [{
id: product.id,
title: product.title,
vendor: product.vendor,
price: product.selectedOrFirstAvailableVariant.price.amount,
variantId: product.selectedOrFirstAvailableVariant.id,
}],
}"
/>
</template>
The other view components follow the same shape:
<template>
<ShopifyCollectionView :data="{ collection: { id: collection.id, handle: collection.handle } }" />
<ShopifySearchView :data="{ searchTerm: query }" />
<ShopifyCartView :data="{ cart }" />
<ShopifyCustomView event="newsletter_signup" :data="{ source: 'footer' }" />
</template>
ShopifyCustomView prefixes the event name with custom_, so event="newsletter_signup" is published as
custom_newsletter_signup (subscribe to that prefixed name). Custom events are only delivered to your own subscribers,
never sent to Shopify.
Analytics composable
To publish events manually, use the useShopifyAnalytics() composable. It is safe to call anywhere, including on the server.
When analytics is disabled, it returns a no-op.
const analytics = useShopifyAnalytics()
analytics.publish('product_viewed', {
products: [/* ... */]
})
const off = analytics.subscribe('product_viewed', (payload) => {
// payload is typed to the event
})
analytics.canTrack() // whether the visitor consented to analytics
Buyer market
Events carry the currency and language of the market they happened in. By default that is your shop's default market.
If you want to change which market the buyer is in so it re-resolves the shop with Shopify's @inContext directive, call setShopContext with a country and/or language:
export default defineNuxtPlugin(() => {
const analytics = useShopifyAnalytics()
const { locale } = useI18n()
watch(locale, () => analytics.setShopContext({
country: 'CA',
language: 'EN'
}), { immediate: true })
})
Both values are optional and are upper-cased for you, so an i18n locale like en-us is accepted where a
LanguageCode is expected. Pass Shopify's enum codes, not a locale string: country is a CountryCode (CA, DE) and
language is a LanguageCode (EN, PT_BR). Each market is resolved once and cached, and a market Shopify does not
recognize falls back to whatever was resolved before it, so a bad code degrades the reported market instead of
silencing analytics.
currency and language in your configuration remain the default for the first resolve; setting both still skips the
initial lookup entirely.
Cart events
Cart events are derived, so you never publish them yourself. Keep the cart in sync after every mutation and the module works out what changed by diffing the previous cart against the new one:
const analytics = useShopifyAnalytics()
const { data } = await addToCartMutation()
analytics.setCart({
...data.cart,
lines: flattenConnection(data.cart.lines),
})
That publishes cart_updated, plus a product_added_to_cart or product_removed_from_cart for every line whose
quantity changed. Only product_added_to_cart reaches Shopify; the other two are for your own subscribers.
The cart needs id and updatedAt, and each line needs merchandise.product.id and merchandise.product.vendor, or
the event cannot be attributed. Updates carrying an updatedAt that was already reported are ignored, so a reload never
re-reports a cart.
Forwarding to other tools
Because everything runs through one bus, subscribe is also how you fan events out to a third-party provider such as GA4 or Meta:
export default defineNuxtPlugin(() => {
const analytics = useShopifyAnalytics()
analytics.subscribe('product_added_to_cart', ({currentLine }) => {
gtag('event', 'add_to_cart', { /* map from currentLine */ })
})
})
Consent
Every event is gated on Shopify's Customer Privacy API. In regions that require opt-in, such as the EU, nothing is sent and no tracking cookies are set until the visitor consents. Elsewhere, Shopify's API may permit tracking by default. Either way, consent is dictated by Shopify, not the module.
Point consent at your checkout domain so it is shared between your storefront and Shopify's checkout:
export default defineNuxtConfig({
shopify: {
analytics: {
storefrontId: '1234567',
consent: {
checkoutDomain: 'checkout.my-shop.com',
},
},
},
})
publicAccessToken.false. Leave it off to build your own.Recording consent
Record the visitor's choice through the composable, or from your own banner:
const analytics = useShopifyAnalytics()
analytics.setTrackingConsent({
analytics: true,
marketing: true,
preferences: true,
sale_of_data: true,
})
ShopifyPrivacyBanner is a renderless component that holds accept, decline, setConsent, and canTrack
through its slot, you can style the banner however you like:
<template>
<ShopifyPrivacyBanner v-slot="{ accept, decline }">
<div class="banner">
<p>We use cookies to measure our store.</p>
<button @click="accept">
Accept
</button>
<button @click="decline">
Decline
</button>
</div>
</ShopifyPrivacyBanner>
</template>
If you set withPrivacyBanner: true, Shopify renders its own banner and you can skip this component entirely.