Installing Google Analytics 4 on Commercetools
This guide covers implementing Google Analytics 4 (GA4) on Commercetools headless commerce storefronts. Since Commercetools is API-first, GA4 implementation happens in your frontend application.
Prerequisites
Before implementing GA4 on your Commercetools storefront:
Create a GA4 Property in Google Analytics
- Sign in to analytics.google.com
- Create a new GA4 property
- Copy your Measurement ID (format:
G-XXXXXXXXXX)
Identify Your Frontend Stack
Commercetools Setup
- API client configured
- Product catalog populated
- Cart/checkout flow implemented
Method 1: Next.js Implementation
Best for: Server-side rendered React storefronts
Install Dependencies
npm install @commercetools/platform-sdk
Create Analytics Context
// contexts/AnalyticsContext.tsx
'use client';
import { createContext, useContext, useEffect, ReactNode } from 'react';
import Script from 'next/script';
interface AnalyticsContextType {
trackEvent: (eventName: string, params?: Record<string, any>) => void;
trackEcommerce: (eventName: string, ecommerceData: any) => void;
}
const AnalyticsContext = createContext<AnalyticsContextType | null>(null);
declare global {
interface Window {
gtag: (...args: any[]) => void;
dataLayer: any[];
}
}
export function AnalyticsProvider({
children,
measurementId
}: {
children: ReactNode;
measurementId: string;
}) {
useEffect(() => {
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
window.gtag('js', new Date());
window.gtag('config', measurementId, {
send_page_view: false // We'll handle page views manually
});
}, [measurementId]);
const trackEvent = (eventName: string, params?: Record<string, any>) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, params);
}
};
const trackEcommerce = (eventName: string, ecommerceData: any) => {
if (typeof window !== 'undefined' && window.gtag) {
// Clear previous ecommerce data
window.gtag('event', eventName, {
ecommerce: null
});
// Push new ecommerce data
window.gtag('event', eventName, {
ecommerce: ecommerceData
});
}
};
return (
<AnalyticsContext.Provider value={{ trackEvent, trackEcommerce }}>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
strategy="afterInteractive"
/>
{children}
</AnalyticsContext.Provider>
);
}
export function useAnalytics() {
const context = useContext(AnalyticsContext);
if (!context) {
throw new Error('useAnalytics must be used within AnalyticsProvider');
}
return context;
}
Add Provider to Layout
// app/layout.tsx
import { AnalyticsProvider } from '@/contexts/AnalyticsContext';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AnalyticsProvider measurementId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!}>
{children}
</AnalyticsProvider>
</body>
</html>
);
}
Track Product Views
// components/ProductPage.tsx
'use client';
import { useEffect } from 'react';
import { useAnalytics } from '@/contexts/AnalyticsContext';
import { Product } from '@commercetools/platform-sdk';
interface ProductPageProps {
product: Product;
}
export function ProductPage({ product }: ProductPageProps) {
const { trackEcommerce } = useAnalytics();
useEffect(() => {
const variant = product.masterData.current.masterVariant;
const price = variant.prices?.[0];
trackEcommerce('view_item', {
currency: price?.value.currencyCode || 'USD',
value: price ? price.value.centAmount / 100 : 0,
items: [{
item_id: product.id,
item_name: product.masterData.current.name['en-US'],
item_variant: variant.sku,
price: price ? price.value.centAmount / 100 : 0,
quantity: 1
}]
});
}, [product, trackEcommerce]);
return (
// Your product page JSX
);
}
Track Add to Cart
// hooks/useCart.ts
import { useAnalytics } from '@/contexts/AnalyticsContext';
import { LineItem } from '@commercetools/platform-sdk';
export function useCart() {
const { trackEcommerce } = useAnalytics();
const addToCart = async (productId: string, variantId: number, quantity: number) => {
// Add to Commercetools cart
const updatedCart = await commercetoolsClient.addLineItem(productId, variantId, quantity);
// Find the added item
const addedItem = updatedCart.lineItems.find(
item => item.productId === productId && item.variant.id === variantId
);
if (addedItem) {
trackEcommerce('add_to_cart', {
currency: addedItem.price.value.currencyCode,
value: (addedItem.price.value.centAmount / 100) * quantity,
items: [{
item_id: addedItem.productId,
item_name: addedItem.name['en-US'],
item_variant: addedItem.variant.sku,
price: addedItem.price.value.centAmount / 100,
quantity: quantity
}]
});
}
return updatedCart;
};
return { addToCart };
}
Track Purchase
// app/checkout/success/page.tsx
'use client';
import { useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { useAnalytics } from '@/contexts/AnalyticsContext';
export default function CheckoutSuccess() {
const { trackEcommerce } = useAnalytics();
const searchParams = useSearchParams();
const orderId = searchParams.get('orderId');
useEffect(() => {
if (orderId) {
// Fetch order from Commercetools
fetchOrder(orderId).then(order => {
trackEcommerce('purchase', {
transaction_id: order.orderNumber,
value: order.totalPrice.centAmount / 100,
tax: order.taxedPrice?.totalTax.centAmount
? order.taxedPrice.totalTax.centAmount / 100
: 0,
shipping: order.shippingInfo?.price.centAmount
? order.shippingInfo.price.centAmount / 100
: 0,
currency: order.totalPrice.currencyCode,
items: order.lineItems.map(item => ({
item_id: item.productId,
item_name: item.name['en-US'],
item_variant: item.variant.sku,
price: item.price.value.centAmount / 100,
quantity: item.quantity
}))
});
});
}
}, [orderId, trackEcommerce]);
return <div>Thank you for your order!</div>;
}
Method 2: Vue/Nuxt Implementation
Best for: Vue-based storefronts
Install Dependencies
npm install vue-gtag-next
Configure Plugin
// plugins/gtag.client.ts
import VueGtag from 'vue-gtag-next';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueGtag, {
property: {
id: useRuntimeConfig().public.gaMeasurementId,
},
});
});
Create Composable
// composables/useEcommerceTracking.ts
import { useGtag } from 'vue-gtag-next';
export function useEcommerceTracking() {
const { event } = useGtag();
const trackViewItem = (product: any) => {
event('view_item', {
ecommerce: {
currency: product.price.currencyCode,
value: product.price.centAmount / 100,
items: [{
item_id: product.id,
item_name: product.name,
price: product.price.centAmount / 100,
}]
}
});
};
const trackAddToCart = (item: any, quantity: number) => {
event('add_to_cart', {
ecommerce: {
currency: item.price.currencyCode,
value: (item.price.centAmount / 100) * quantity,
items: [{
item_id: item.productId,
item_name: item.name,
price: item.price.centAmount / 100,
quantity: quantity
}]
}
});
};
const trackPurchase = (order: any) => {
event('purchase', {
ecommerce: {
transaction_id: order.orderNumber,
value: order.totalPrice.centAmount / 100,
currency: order.totalPrice.currencyCode,
items: order.lineItems.map((item: any) => ({
item_id: item.productId,
item_name: item.name,
price: item.price.centAmount / 100,
quantity: item.quantity
}))
}
});
};
return {
trackViewItem,
trackAddToCart,
trackPurchase
};
}
Method 3: Server-Side Tracking
Best for: Accurate conversion tracking, ad blocker resilience
Commercetools Subscription Setup
Create a subscription in Commercetools for order events:
{
"key": "ga4-order-tracking",
"destination": {
"type": "GoogleCloudPubSub",
"projectId": "your-gcp-project",
"topic": "commercetools-orders"
},
"messages": [
{
"resourceTypeId": "order",
"types": ["OrderCreated", "OrderStateChanged"]
}
]
}
Cloud Function Handler
// functions/trackOrder.ts
import * as functions from '@google-cloud/functions-framework';
import fetch from 'node-fetch';
const GA4_MEASUREMENT_ID = process.env.GA4_MEASUREMENT_ID;
const GA4_API_SECRET = process.env.GA4_API_SECRET;
interface OrderMessage {
order: {
id: string;
orderNumber: string;
customerId: string;
totalPrice: {
centAmount: number;
currencyCode: string;
};
lineItems: Array<{
productId: string;
name: { 'en-US': string };
variant: { sku: string };
price: { value: { centAmount: number } };
quantity: number;
}>;
};
}
functions.cloudEvent('trackOrder', async (cloudEvent: any) => {
const message = JSON.parse(
Buffer.from(cloudEvent.data.message.data, 'base64').toString()
) as OrderMessage;
const order = message.order;
const payload = {
client_id: order.customerId || `order_${order.id}`,
events: [{
name: 'purchase',
params: {
transaction_id: order.orderNumber,
value: order.totalPrice.centAmount / 100,
currency: order.totalPrice.currencyCode,
items: order.lineItems.map(item => ({
item_id: item.productId,
item_name: item.name['en-US'],
item_variant: item.variant.sku,
price: item.price.value.centAmount / 100,
quantity: item.quantity
}))
}
}]
};
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`,
{
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
}
);
if (!response.ok) {
console.error('GA4 tracking failed:', await response.text());
}
});
E-commerce Events Reference
Required Events for GA4 E-commerce
| Event | When to Fire | Required Parameters |
|---|---|---|
view_item_list |
Product listing pages | items[] |
view_item |
Product detail pages | items[], value, currency |
add_to_cart |
Item added to cart | items[], value, currency |
remove_from_cart |
Item removed from cart | items[], value, currency |
view_cart |
Cart page viewed | items[], value, currency |
begin_checkout |
Checkout started | items[], value, currency |
add_shipping_info |
Shipping selected | items[], shipping_tier |
add_payment_info |
Payment entered | items[], payment_type |
purchase |
Order completed | transaction_id, items[], value, currency |
Commercetools Data Mapping
// utils/ga4Mapping.ts
import { LineItem, Order, ProductProjection } from '@commercetools/platform-sdk';
export function mapProductToGA4Item(product: ProductProjection) {
const variant = product.masterVariant;
const price = variant.prices?.[0];
return {
item_id: product.id,
item_name: product.name['en-US'] || product.name['en'],
item_brand: product.masterVariant.attributes?.find(
a => a.name === 'brand'
)?.value,
item_category: product.categories?.[0]?.obj?.name?.['en-US'],
item_variant: variant.sku,
price: price ? price.value.centAmount / 100 : 0,
};
}
export function mapLineItemToGA4Item(item: LineItem) {
return {
item_id: item.productId,
item_name: item.name['en-US'] || item.name['en'],
item_variant: item.variant.sku,
price: item.price.value.centAmount / 100,
quantity: item.quantity,
};
}
export function mapOrderToGA4Purchase(order: Order) {
return {
transaction_id: order.orderNumber || order.id,
value: order.totalPrice.centAmount / 100,
currency: order.totalPrice.currencyCode,
tax: order.taxedPrice?.totalTax?.centAmount
? order.taxedPrice.totalTax.centAmount / 100
: 0,
shipping: order.shippingInfo?.price?.centAmount
? order.shippingInfo.price.centAmount / 100
: 0,
items: order.lineItems.map(mapLineItemToGA4Item),
};
}
Validation and Testing
1. GA4 DebugView
Enable debug mode in your implementation:
// Enable debug mode
window.gtag('config', 'G-XXXXXXXXXX', {
debug_mode: true
});
Then view events in GA4 → Configure → DebugView.
2. Browser Console Validation
// Check GA4 is loaded
console.log(window.gtag);
console.log(window.dataLayer);
// Test event
window.gtag('event', 'test_event', { test_param: 'test_value' });
3. Network Tab
Filter by google-analytics.com or googletagmanager.com to see hits.
Common Issues
Missing E-commerce Data
Cause: Async data not available when tracking fires
Solution:
// Wait for product data before tracking
useEffect(() => {
if (product && product.masterData) {
trackEcommerce('view_item', formatProduct(product));
}
}, [product]);
Duplicate Purchase Events
Cause: Thank you page revisits
Solution:
// Use sessionStorage to prevent duplicates
useEffect(() => {
const tracked = sessionStorage.getItem(`purchase_tracked_${orderId}`);
if (!tracked && order) {
trackEcommerce('purchase', formatOrder(order));
sessionStorage.setItem(`purchase_tracked_${orderId}`, 'true');
}
}, [order, orderId]);
Currency Mismatches
Cause: Commercetools uses centAmount (e.g., 1000 = $10.00)
Solution: Always divide by 100:
const price = item.price.value.centAmount / 100;
Next Steps
- Configure E-commerce Tracking - Advanced e-commerce setup
- GTM Setup - Tag Manager approach
- Troubleshooting - Debug tracking issues
Related Resources
- GA4 Fundamentals - Universal GA4 concepts
- E-commerce Tracking Issues - Common e-commerce problems
- Server-Side Tracking - Measurement Protocol guide