Ecommerce
Shopify Analytics into Your Next.js Custom Storefront
If you're constructing a headless Shopify store using Next.js, acquiring clear insights from analytics data can often be challenging. Although solutions are available for Shopify's Hydrogen (paired with Remix) framework, documentation on directly connecting Shopify analytics within a Next.js headless environment is notably lacking. This leaves many developers navigating a complex maze of GitHub discussions and the Hydrogen repository in search of definitive answers.
This tutorial aims to dispel any confusion, offering a reliable method for integrating Shopify analytics into your Next.js project. We will utilise ngrok alongside essential Shopify configuration steps to close this gap.
For the purposes of this tutorial, I will be leveraging the Next.js commerce framework available at vercel/commerce: Next.js Commerce (github.com).
The steps for installing Vercel Commerce, along with integrating Vercel and Shopify, can be found in this guide.
The complete source code for this tutorial is available at https://github.com/oybek-daniyarov/vercel-commerce-with-shopify-analytics, serving as a valuable resource to guide you through your implementation.
Prerequisites
- Successful setup of Next.js commerce.
- A fundamental understanding of Next.js concepts.
- An active Shopify store.
Unravelling the Complexity
Shopify's ADD_TO_CART analytics functionality hinges on the use of specific cookies (_shopify_y and _shopify_s) to monitor user interactions. Unfortunately, these analytics features falter when operating within a local development environment, necessitating the intervention of ngrok to seamlessly bridge this gap.
Upon consulting the documentation, it is noted that:
The storefrontHeaders property for createStorefrontClient must be explicitly defined using a getStorefrontHeaders(request) helper function.
A visit to the Hydrogen repository to examine the getStorefrontHeaders function reveals the inclusion of the aforementioned cookies (_shopify_y and _shopify_s) within the request headers.
...
export function getStorefrontHeaders(request: Request):
StorefrontHeaders {
const headers = request.headers;
return {
requestGroupId: headers.get('request-id'),
buyerIp: headers.get('oxygen-buyer-ip'),
cookie: headers.get('cookie'),
};
}
Implementing the Solution
1. Installing Dependencies
Begin by installing the @shopify/hydrogen-react package via your preferred package manager:
pnpm add @shopify/hydrogen-react
Define the currency and default language:
//./lib/constants.ts
...
export const currency = 'AED';
export const defaultLanguage = 'EN';
Incorporate the SHOP_ID into your environment variables:
Locate your SHOP_ID by visiting https://[your-store-id].myshopify.com/shop.json and searching for shopId.
#.env.local
NEXT_PUBLIC_SHOPIFY_SHOP_ID=YOUR_SHOP_ID_HERE
2. Setting Up the useShopifyAnalytics Hook
Ensure you are establishing cookies with user consent for analytics compliance:
// ./lib/shopify/hooks/use-shopify-analytics.ts
import { usePathname } from 'next/navigation';
import {
AnalyticsEventName,
getClientBrowserParameters,
sendShopifyAnalytics,
ShopifyAnalyticsProduct,
ShopifyPageViewPayload,
ShopifySalesChannel,
useShopifyCookies
} from '@shopify/hydrogen-react';
import { currency, defaultLanguage } from 'lib/constants';
const SHOP_ID = process.env.NEXT_PUBLIC_SHOPIFY_SHOP_ID!;
type SendPageViewPayload = {
pageType?: string;
products?: ShopifyAnalyticsProduct[];
collectionHandle?: string;
searchString?: string;
totalValue?: number;
cartId?: string;
};
type SendAddToCartPayload = {
cartId: string;
products?: ShopifyAnalyticsProduct[];
totalValue?: ShopifyPageViewPayload['totalValue'];
};
export function useShopifyAnalytics() {
const pathname = usePathname();
// Send page view event
const sendPageView = (eventName: keyof typeof AnalyticsEventName, payload?: SendPageViewPayload) => {
return sendShopifyAnalytics({
eventName,
payload: {
...getClientBrowserParameters(),
hasUserConsent: true,
shopifySalesChannel: ShopifySalesChannel.headless,
shopId: `gid://shopify/Shop/${SHOP_ID}`,
currency,
acceptedLanguage: defaultLanguage,
...payload
}
});
};
// Send add to cart event
const sendAddToCart = ({ cartId, totalValue, products }: SendAddToCartPayload) =>
sendPageView(AnalyticsEventName.ADD_TO_CART, {
cartId,
totalValue,
products
});
// Set up cookies for Shopify analytics & enable user consent
useShopifyCookies({
hasUserConsent: true
});
return {
sendPageView,
sendAddToCart,
pathname
};
}
Modify the addItem function located in ./components/cart/actions.ts as follows:
type AddItemResponse = {
cartId?: string;
success: boolean;
message?: string;
};
export async function addItem(
prevState: any,
selectedVariantId: string | undefined
): Promise<AddItemResponse> {
let cartId = cookies().get('cartId')?.value;
let cart;
if (cartId) {
cart = await getCart(cartId);
}
if (!cartId || !cart) {
cart = await createCart();
cartId = cart.id;
cookies().set('cartId', cartId);
}
if (!selectedVariantId) {
return { success: false, message: 'Missing variant ID' };
}
try {
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
revalidateTag(TAGS.cart);
return { success: true, cartId };
} catch (e) {
return { success: false, message: 'Error adding item to cart' };
}
}
Update the AddToCart function located in ./components/cart/add-to-cart.tsx accordingly:
// ./components/cart/add-to-cart.tsx
export function AddToCart({
variants,
availableForSale
}: {
variants: ProductVariant[];
availableForSale: boolean;
}) {
const { sendAddToCart } = useShopifyAnalytics();
const [response, formAction] = useFormState(addItem, null);
const searchParams = useSearchParams();
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
the variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
const selectedVariantId = variant?.id || defaultVariantId;
the actionWithVariant = formAction.bind(null, selectedVariantId);
useEffect(() => {
if (response?.success && response.cartId) {
sendAddToCart({
cartId: response.cartId
// Optionally, pass product data and totalValue of the added item
// products[],
// totalValue
});
}
}, [response?.success, response?.cartId, sendAddToCart]);
return (
<form action={actionWithVariant}>
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
{response?.message && (
<p aria-live="polite" className="sr-only" role="status">
{response.message}
</p>
)}
</form>
);
}
Update addToCart action
add (_shopify_y and _shopify_s) cookies into the header.
//lib/shopify/index.ts
export async function addToCart(
cartId: string,
lines: { merchandiseId: string; quantity: number }[]
): Promise<Cart> {
// get shopify cookies
const shopifyY = cookies()?.get('_shopify_y')?.value;
const shopifyS = cookies()?.get('_shopify_s')?.value;
const res = await shopifyFetch<ShopifyAddToCartOperation>({
query: addToCartMutation,
variables: {
cartId,
lines
},
headers: {
...(shopifyY && shopifyS && { cookie: `_shopify_y=${shopifyY}; _shopify_s=${shopifyS};` })
},
cache: 'no-store'
});
return reshapeCart(res.body.data.cartLinesAdd.cart);
}
3. Creating the ShopifyAnalytics.tsx Component
This component triggers on every page load:
// ./components/layout/shopify-analytics.tsx
'use client';
import { useEffect } from 'react';
import { AnalyticsEventName } from '@shopify/hydrogen-react';
import { useShopifyAnalytics } from 'lib/shopify/hooks/use-shopify-analytics';
export default function ShopifyAnalytics() {
const { sendPageView, pathname } = useShopifyAnalytics();
useEffect(() => {
sendPageView(AnalyticsEventName.PAGE_VIEW);
}, [pathname, sendPageView]);
return null;
}
Incorporate this component within your layout structure:
// app/layout.tsx
...
<Suspense>
<main>{children}</main>
</Suspense>
<ShopifyAnalytics />
...
Integrate the created analytics functions into the logic of your Next.js application, ensuring that actions such as the sendPageView function are invoked whenever a user navigates to a new page.
4. Configuring ngrok
- Download and install ngrok from https://ngrok.com/.
- Set up a custom domain.
- Expose your local development server (e.g., running on port 3000) with a command like:
- Execute ngrok http --domain=YOUR_NGROK_DOMAIN 3000.
Important: For an enhanced development experience, configure a custom ngrok domain to ensure URL consistency and maintain cookies between sessions. Further details can be found within the ngrok documentation.
Additional Notes
- Conduct thorough testing of your analytics tracking post-implementation.
- Ensure compliance with privacy regulations such as GDPR by securing user consent for analytics tracking.