Google Tag Manager on Commercetools | Blue Frog Docs

Google Tag Manager on Commercetools

Implementing GTM on Commercetools headless commerce with data layer integration

Google Tag Manager on Commercetools

This guide covers implementing Google Tag Manager (GTM) on Commercetools headless storefronts. GTM provides centralized tag management for all your marketing and analytics tools.

Prerequisites

  1. Create a GTM Container

    • Sign in to tagmanager.google.com
    • Create a new container (Web)
    • Copy your Container ID (format: GTM-XXXXXXX)
  2. Frontend Application Ready

    • Commercetools SDK configured
    • E-commerce flows implemented

Installation by Framework

Next.js / React

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;

  return (
    <html lang="en">
      <head>
        <Script id="gtm-script" strategy="afterInteractive">
          {`
            (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
            'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
            })(window,document,'script','dataLayer','${GTM_ID}');
          `}
        </Script>
      </head>
      <body>
        <noscript>
          <iframe
            src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
            height="0"
            width="0"
            style={{ display: 'none', visibility: 'hidden' }}
          />
        </noscript>
        {children}
      </body>
    </html>
  );
}

Nuxt / Vue

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          children: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
            'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
            })(window,document,'script','dataLayer','${process.env.GTM_ID}');`
        }
      ],
      noscript: [
        {
          children: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.GTM_ID}"
            height="0" width="0" style="display:none;visibility:hidden"></iframe>`
        }
      ]
    }
  }
});

Data Layer Implementation

Create Data Layer Hook

// hooks/useDataLayer.ts
declare global {
  interface Window {
    dataLayer: any[];
  }
}

export function useDataLayer() {
  const push = (data: Record<string, any>) => {
    if (typeof window !== 'undefined') {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push(data);
    }
  };

  const pushEcommerce = (eventName: string, ecommerceData: any) => {
    push({ ecommerce: null }); // Clear previous ecommerce data
    push({
      event: eventName,
      ecommerce: ecommerceData
    });
  };

  return { push, pushEcommerce };
}

E-commerce Data Layer Events

// utils/commercetoolsToDataLayer.ts
import { Cart, LineItem, Order, ProductProjection } from '@commercetools/platform-sdk';

export function formatProductForDataLayer(product: ProductProjection) {
  const variant = product.masterVariant;
  const price = variant.prices?.[0];

  return {
    item_id: product.id,
    item_name: product.name['en-US'],
    item_brand: variant.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,
    currency: price?.value.currencyCode || 'USD'
  };
}

export function formatCartForDataLayer(cart: Cart) {
  return {
    currency: cart.totalPrice.currencyCode,
    value: cart.totalPrice.centAmount / 100,
    items: cart.lineItems.map((item, index) => ({
      item_id: item.productId,
      item_name: item.name['en-US'],
      item_variant: item.variant.sku,
      price: item.price.value.centAmount / 100,
      quantity: item.quantity,
      index: index
    }))
  };
}

export function formatOrderForDataLayer(order: Order) {
  return {
    transaction_id: order.orderNumber || order.id,
    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, index) => ({
      item_id: item.productId,
      item_name: item.name['en-US'],
      item_variant: item.variant.sku,
      price: item.price.value.centAmount / 100,
      quantity: item.quantity,
      index: index
    }))
  };
}

Implementation Examples

// Product List Page
import { useDataLayer } from '@/hooks/useDataLayer';
import { formatProductForDataLayer } from '@/utils/commercetoolsToDataLayer';

export function ProductListPage({ products, categoryName }: Props) {
  const { pushEcommerce } = useDataLayer();

  useEffect(() => {
    pushEcommerce('view_item_list', {
      item_list_id: categoryName.toLowerCase().replace(/\s+/g, '_'),
      item_list_name: categoryName,
      items: products.map((product, index) => ({
        ...formatProductForDataLayer(product),
        index: index
      }))
    });
  }, [products, categoryName]);

  return (/* JSX */);
}

// Product Detail Page
export function ProductDetailPage({ product }: Props) {
  const { pushEcommerce } = useDataLayer();

  useEffect(() => {
    const variant = product.masterVariant;
    const price = variant.prices?.[0];

    pushEcommerce('view_item', {
      currency: price?.value.currencyCode || 'USD',
      value: price ? price.value.centAmount / 100 : 0,
      items: [formatProductForDataLayer(product)]
    });
  }, [product]);

  return (/* JSX */);
}

// Add to Cart
export function useAddToCart() {
  const { pushEcommerce } = useDataLayer();

  const addToCart = async (product: ProductProjection, quantity: number) => {
    // API call to add to cart
    await commercetoolsAddToCart(product.id, quantity);

    const variant = product.masterVariant;
    const price = variant.prices?.[0];

    pushEcommerce('add_to_cart', {
      currency: price?.value.currencyCode || 'USD',
      value: price ? (price.value.centAmount / 100) * quantity : 0,
      items: [{
        ...formatProductForDataLayer(product),
        quantity: quantity
      }]
    });
  };

  return { addToCart };
}

// Checkout Success
export function CheckoutSuccessPage({ order }: Props) {
  const { pushEcommerce } = useDataLayer();

  useEffect(() => {
    // Prevent duplicate tracking
    const tracked = sessionStorage.getItem(`order_${order.id}_tracked`);
    if (!tracked) {
      pushEcommerce('purchase', formatOrderForDataLayer(order));
      sessionStorage.setItem(`order_${order.id}_tracked`, 'true');
    }
  }, [order]);

  return (/* JSX */);
}

GTM Container Configuration

Variables

Create these Data Layer Variables in GTM:

Variable Name Data Layer Variable Name
DL - Ecommerce Items ecommerce.items
DL - Ecommerce Value ecommerce.value
DL - Ecommerce Currency ecommerce.currency
DL - Transaction ID ecommerce.transaction_id
DL - Shipping ecommerce.shipping
DL - Tax ecommerce.tax

Triggers

Create Custom Event triggers:

Trigger Name Event Name
CE - view_item_list view_item_list
CE - view_item view_item
CE - add_to_cart add_to_cart
CE - remove_from_cart remove_from_cart
CE - view_cart view_cart
CE - begin_checkout begin_checkout
CE - add_shipping_info add_shipping_info
CE - add_payment_info add_payment_info
CE - purchase purchase

GA4 Configuration Tag

Tag Type: Google Analytics: GA4 Configuration
Measurement ID: G-XXXXXXXXXX
Send Page View: true

Firing Trigger: All Pages

GA4 E-commerce Event Tags

Create one tag per e-commerce event:

Tag Type: Google Analytics: GA4 Event
Configuration Tag: [Your GA4 Config Tag]
Event Name: {{Event}}

Event Parameters:
- items: {{DL - Ecommerce Items}}
- value: {{DL - Ecommerce Value}}
- currency: {{DL - Ecommerce Currency}}

Firing Trigger: [Corresponding event trigger]

For purchase events, add additional parameters:

- transaction_id: {{DL - Transaction ID}}
- shipping: {{DL - Shipping}}
- tax: {{DL - Tax}}

Server-Side GTM

For enhanced privacy and accuracy, consider Server-Side GTM:

Client Configuration

// Client sends to your server-side GTM endpoint
const GTM_SERVER_URL = 'https://gtm.yourdomain.com';

export function useServerSideDataLayer() {
  const push = async (data: Record<string, any>) => {
    // Still push to client dataLayer for debugging
    window.dataLayer?.push(data);

    // Also send to server-side GTM
    await fetch(`${GTM_SERVER_URL}/data`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...data,
        client_id: getClientId(),
        timestamp: Date.now()
      })
    });
  };

  return { push };
}

Server-Side Container Setup

  1. Create Server container in GTM
  2. Deploy to Cloud Run, App Engine, or similar
  3. Configure GA4 client to receive events
  4. Forward to GA4 API endpoint

Testing and Debugging

GTM Preview Mode

  1. Click Preview in GTM
  2. Enter your staging URL
  3. Browse your site
  4. View events in Tag Assistant

Data Layer Inspector

// Console: View current dataLayer
console.table(window.dataLayer);

// Console: Monitor dataLayer pushes
const originalPush = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function(...args) {
  console.log('dataLayer.push:', args);
  return originalPush(...args);
};

Common Issues

Events not firing:

  • Check event name matches trigger exactly
  • Verify dataLayer variable paths
  • Ensure GTM container is loading

Ecommerce data missing:

  • Clear ecommerce object before pushing new data
  • Check items array format
  • Verify price is a number (not string)

Next Steps

// SYS.FOOTER