Installing Google Analytics 4 on Commercetools | Blue Frog Docs

Installing Google Analytics 4 on Commercetools

Complete guide to implementing GA4 on Commercetools headless commerce with frontend frameworks

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:

  1. 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)
  2. Identify Your Frontend Stack

  3. 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

// SYS.FOOTER