TYPO3 GA4 E-commerce Tracking | OpsBlu Docs

TYPO3 GA4 E-commerce Tracking

Implement GA4 e-commerce tracking for TYPO3 shop extensions including Extbase shops, Commerce, and Aimeos

Comprehensive guide to implementing Google Analytics 4 e-commerce tracking in TYPO3 shop systems, including custom Extbase shops, Commerce extension, and Aimeos.

TYPO3 E-commerce Landscape

TYPO3 offers several e-commerce solutions:

  • Custom Extbase/Fluid Shops - Bespoke implementations
  • Commerce (EXT:commerce) - Legacy but still used
  • Aimeos - Modern, comprehensive solution
  • Cart (EXT:cart) - Lightweight shopping cart
  • Shopware Integration - External shop integration

GA4 E-commerce Events Overview

Required events for complete e-commerce tracking:

Event Trigger Required Parameters
view_item Product detail page items array
view_item_list Product listing page items array
add_to_cart Add product to cart items array, value, currency
remove_from_cart Remove from cart items array, value, currency
view_cart Cart page view items array, value, currency
begin_checkout Checkout start items array, value, currency
add_payment_info Payment method selected items array, payment_type
add_shipping_info Shipping method selected items array, shipping_tier
purchase Order completed transaction_id, items array, value, currency, tax, shipping

Method 1: Custom Extbase Shop Implementation

Product Model Enhancement

Add data attributes to your product Fluid template:

<!-- Resources/Private/Templates/Product/Show.html -->
<f:layout name="Default" />

<f:section name="main">
    <div class="product-detail"
         data-product-id="{product.uid}"
         data-product-name="{product.title}"
         data-product-price="{product.price}"
         data-product-category="{product.category.title}">

        <h1>{product.title}</h1>
        <p class="price">{product.price -> f:format.currency(currencySign: '€')}</p>

        <f:form action="addToCart" controller="Cart" object="{product}">
            <f:form.hidden name="product" value="{product.uid}" />
            <f:form.submit value="Add to Cart" class="add-to-cart-btn" />
        </f:form>
    </div>

    <script>
        // Track product view
        gtag('event', 'view_item', {
            currency: 'EUR',
            value: {product.price},
            items: [{
                item_id: '{product.uid}',
                item_name: '{product.title -> f:format.htmlentities()}',
                item_category: '{product.category.title -> f:format.htmlentities()}',
                price: {product.price},
                quantity: 1
            }]
        });
    </script>
</f:section>

Product Listing Tracking

<!-- Resources/Private/Templates/Product/List.html -->
<f:layout name="Default" />

<f:section name="main">
    <div class="product-list">
        <f:for each="{products}" as="product" iteration="iterator">
            <div class="product-item" data-product-id="{product.uid}">
                <h3>{product.title}</h3>
                <p class="price">{product.price -> f:format.currency(currencySign: '€')}</p>
                <f:link.action action="show" arguments="{product: product}">
                    View Details
                </f:link.action>
            </div>
        </f:for>
    </div>

    <script>
        // Track product list view
        gtag('event', 'view_item_list', {
            item_list_id: 'product_list_{category.uid}',
            item_list_name: '{category.title -> f:format.htmlentities()}',
            items: [
                <f:for each="{products}" as="product" iteration="iterator">
                {
                    item_id: '{product.uid}',
                    item_name: '{product.title -> f:format.htmlentities()}',
                    item_category: '{product.category.title -> f:format.htmlentities()}',
                    price: {product.price},
                    index: {iterator.cycle}
                }<f:if condition="{iterator.isLast}"><f:else>,</f:else></f:if>
                </f:for>
            ]
        });
    </script>
</f:section>

Add to Cart Tracking

Controller Implementation

<?php
namespace Vendor\Shop\Controller;

use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class CartController extends ActionController
{
    public function addToCartAction(\Vendor\Shop\Domain\Model\Product $product, int $quantity = 1)
    {
        // Add product to session cart
        $cart = $this->cartService->getCart();
        $cart->addItem($product, $quantity);
        $this->cartService->updateCart($cart);

        // Prepare GA4 tracking data
        $trackingData = [
            'item_id' => $product->getUid(),
            'item_name' => $product->getTitle(),
            'item_category' => $product->getCategory()->getTitle(),
            'price' => $product->getPrice(),
            'quantity' => $quantity,
            'currency' => 'EUR',
            'value' => $product->getPrice() * $quantity
        ];

        // Add GA4 event to page footer
        $GLOBALS['TSFE']->additionalFooterData['ga4_add_to_cart'] = "
        <script>
            gtag('event', 'add_to_cart', {
                currency: '{$trackingData['currency']}',
                value: {$trackingData['value']},
                items: [{
                    item_id: '{$trackingData['item_id']}',
                    item_name: '" . htmlspecialchars($trackingData['item_name']) . "',
                    item_category: '" . htmlspecialchars($trackingData['item_category']) . "',
                    price: {$trackingData['price']},
                    quantity: {$trackingData['quantity']}
                }]
            });
        </script>";

        $this->redirect('show', 'Cart');
    }

    public function removeFromCartAction(int $itemId)
    {
        $cart = $this->cartService->getCart();
        $item = $cart->getItemById($itemId);

        if ($item) {
            $product = $item->getProduct();

            // Track removal
            $GLOBALS['TSFE']->additionalFooterData['ga4_remove_from_cart'] = "
            <script>
                gtag('event', 'remove_from_cart', {
                    currency: 'EUR',
                    value: " . ($product->getPrice() * $item->getQuantity()) . ",
                    items: [{
                        item_id: '{$product->getUid()}',
                        item_name: '" . htmlspecialchars($product->getTitle()) . "',
                        price: {$product->getPrice()},
                        quantity: {$item->getQuantity()}
                    }]
                });
            </script>";

            $cart->removeItem($itemId);
            $this->cartService->updateCart($cart);
        }

        $this->redirect('show');
    }
}

Cart View Tracking

<!-- Resources/Private/Templates/Cart/Show.html -->
<f:layout name="Default" />

<f:section name="main">
    <div class="cart-view">
        <h1>Shopping Cart</h1>

        <f:if condition="{cart.items}">
            <f:then>
                <table class="cart-items">
                    <f:for each="{cart.items}" as="item">
                        <tr>
                            <td>{item.product.title}</td>
                            <td>{item.quantity}</td>
                            <td>{item.product.price -> f:format.currency(currencySign: '€')}</td>
                            <td>{item.totalPrice -> f:format.currency(currencySign: '€')}</td>
                        </tr>
                    </f:for>
                </table>

                <p class="cart-total">Total: {cart.total -> f:format.currency(currencySign: '€')}</p>

                <f:link.action action="checkout" controller="Checkout" class="btn-checkout">
                    Proceed to Checkout
                </f:link.action>
            </f:then>
            <f:else>
                <p>Your cart is empty.</p>
            </f:else>
        </f:if>
    </div>

    <f:if condition="{cart.items}">
        <script>
            // Track cart view
            gtag('event', 'view_cart', {
                currency: 'EUR',
                value: {cart.total},
                items: [
                    <f:for each="{cart.items}" as="item" iteration="iterator">
                    {
                        item_id: '{item.product.uid}',
                        item_name: '{item.product.title -> f:format.htmlentities()}',
                        item_category: '{item.product.category.title -> f:format.htmlentities()}',
                        price: {item.product.price},
                        quantity: {item.quantity}
                    }<f:if condition="{iterator.isLast}"><f:else>,</f:else></f:if>
                    </f:for>
                ]
            });
        </script>
    </f:if>
</f:section>

Checkout Process Tracking

<!-- Resources/Private/Templates/Checkout/Index.html -->
<f:layout name="Default" />

<f:section name="main">
    <div class="checkout-page">
        <h1>Checkout</h1>

        <f:form action="processOrder" controller="Checkout">
            <!-- Shipping information -->
            <fieldset>
                <legend>Shipping Information</legend>
                <f:form.textfield property="shippingAddress.firstName" />
                <f:form.textfield property="shippingAddress.lastName" />
                <!-- More fields -->
            </fieldset>

            <!-- Payment method selection -->
            <fieldset>
                <legend>Payment Method</legend>
                <f:form.radio property="paymentMethod" value="credit_card" id="payment-cc" />
                <label for="payment-cc">Credit Card</label>

                <f:form.radio property="paymentMethod" value="paypal" id="payment-paypal" />
                <label for="payment-paypal">PayPal</label>

                <f:form.radio property="paymentMethod" value="invoice" id="payment-invoice" />
                <label for="payment-invoice">Invoice</label>
            </fieldset>

            <!-- Shipping method selection -->
            <fieldset>
                <legend>Shipping Method</legend>
                <f:form.radio property="shippingMethod" value="standard" id="ship-standard" data-cost="5.99" />
                <label for="ship-standard">Standard (5-7 days) - €5.99</label>

                <f:form.radio property="shippingMethod" value="express" id="ship-express" data-cost="12.99" />
                <label for="ship-express">Express (2-3 days) - €12.99</label>
            </fieldset>

            <f:form.submit value="Complete Order" />
        </f:form>
    </div>

    <script>
        // Track begin_checkout
        gtag('event', 'begin_checkout', {
            currency: 'EUR',
            value: {cart.total},
            items: [
                <f:for each="{cart.items}" as="item" iteration="iterator">
                {
                    item_id: '{item.product.uid}',
                    item_name: '{item.product.title -> f:format.htmlentities()}',
                    price: {item.product.price},
                    quantity: {item.quantity}
                }<f:if condition="{iterator.isLast}"><f:else>,</f:else></f:if>
                </f:for>
            ]
        });

        // Track payment method selection
        document.querySelectorAll('input[name="paymentMethod"]').forEach(function(radio) {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    gtag('event', 'add_payment_info', {
                        currency: 'EUR',
                        value: {cart.total},
                        payment_type: this.value,
                        items: [
                            <f:for each="{cart.items}" as="item" iteration="iterator">
                            {
                                item_id: '{item.product.uid}',
                                item_name: '{item.product.title -> f:format.htmlentities()}',
                                price: {item.product.price},
                                quantity: {item.quantity}
                            }<f:if condition="{iterator.isLast}"><f:else>,</f:else></f:if>
                            </f:for>
                        ]
                    });
                }
            });
        });

        // Track shipping method selection
        document.querySelectorAll('input[name="shippingMethod"]').forEach(function(radio) {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    gtag('event', 'add_shipping_info', {
                        currency: 'EUR',
                        value: {cart.total},
                        shipping_tier: this.value,
                        items: [
                            <f:for each="{cart.items}" as="item" iteration="iterator">
                            {
                                item_id: '{item.product.uid}',
                                item_name: '{item.product.title -> f:format.htmlentities()}',
                                price: {item.product.price},
                                quantity: {item.quantity}
                            }<f:if condition="{iterator.isLast}"><f:else>,</f:else></f:if>
                            </f:for>
                        ]
                    });
                }
            });
        });
    </script>
</f:section>

Purchase Confirmation Tracking

<?php
namespace Vendor\Shop\Controller;

class CheckoutController extends ActionController
{
    public function processOrderAction()
    {
        $cart = $this->cartService->getCart();
        $order = $this->orderService->createOrder($cart, $this->request->getArguments());

        // Build items array for GA4
        $items = [];
        foreach ($cart->getItems() as $item) {
            $product = $item->getProduct();
            $items[] = [
                'item_id' => $product->getUid(),
                'item_name' => $product->getTitle(),
                'item_category' => $product->getCategory()->getTitle(),
                'price' => $product->getPrice(),
                'quantity' => $item->getQuantity()
            ];
        }

        // Track purchase
        $GLOBALS['TSFE']->additionalFooterData['ga4_purchase'] = "
        <script>
            gtag('event', 'purchase', {
                transaction_id: '{$order->getOrderNumber()}',
                affiliation: 'TYPO3 Shop',
                value: {$order->getTotal()},
                tax: {$order->getTax()},
                shipping: {$order->getShippingCost()},
                currency: 'EUR',
                coupon: '" . ($order->getCouponCode() ?: '') . "',
                items: " . json_encode($items) . "
            });
        </script>";

        // Clear cart
        $this->cartService->clearCart();

        $this->view->assign('order', $order);
    }
}

Method 2: Aimeos E-commerce Extension

Aimeos is a comprehensive e-commerce solution for TYPO3.

Installation

composer require aimeos/aimeos-typo3

TypoScript Configuration

plugin.tx_aimeos {
    settings {
        client {
            html {
                catalog {
                    detail {
                        # Add GA4 tracking to product detail
                        template = EXT:your_sitepackage/Resources/Private/Extensions/Aimeos/detail.html
                    }
                }
                basket {
                    standard {
                        # Add GA4 tracking to basket
                        template = EXT:your_sitepackage/Resources/Private/Extensions/Aimeos/basket.html
                    }
                }
            }
        }
    }
}

Product Detail Template Override

Create custom template extending Aimeos:

<!-- Resources/Private/Extensions/Aimeos/detail.html -->
{extends "catalog/detail/body-standard.html"}

{block name="catalog/detail/body"}
    {parent}

    <script>
        (function() {
            const product = {
                id: '{$detailProductItem->getId()}',
                name: '{$detailProductItem->getName()}',
                price: {$detailProductItem->getPrice()->getValue()},
                category: '{$detailProductItem->getCategories()->first()->getName()}'
            };

            gtag('event', 'view_item', {
                currency: '{$detailProductItem->getPrice()->getCurrencyId()}',
                value: product.price,
                items: [{
                    item_id: product.id,
                    item_name: product.name,
                    item_category: product.category,
                    price: product.price
                }]
            });

            // Track add to cart
            document.querySelector('.btn-action[name="add"]')?.addEventListener('click', function() {
                const quantity = parseInt(document.querySelector('input[name="b_prod[0][quantity]"]')?.value || 1);

                gtag('event', 'add_to_cart', {
                    currency: '{$detailProductItem->getPrice()->getCurrencyId()}',
                    value: product.price * quantity,
                    items: [{
                        item_id: product.id,
                        item_name: product.name,
                        price: product.price,
                        quantity: quantity
                    }]
                });
            });
        })();
    </script>
{/block}

Aimeos Order Completion Hook

Create a decorator for order tracking:

<?php
namespace Vendor\Extension\Service\Decorator;

class GoogleAnalyticsDecorator extends \Aimeos\MShop\Service\Decorator\Base
{
    public function process(\Aimeos\MShop\Order\Item\Iface $basket, array $params = [])
    {
        $result = $this->getProvider()->process($basket, $params);

        // Add GA4 purchase tracking
        $items = [];
        foreach ($basket->getProducts() as $product) {
            $items[] = [
                'item_id' => $product->getProductId(),
                'item_name' => $product->getName(),
                'price' => $product->getPrice()->getValue(),
                'quantity' => $product->getQuantity()
            ];
        }

        $GLOBALS['TSFE']->additionalFooterData['ga4_purchase'] = sprintf(
            '<script>gtag("event", "purchase", %s);</script>',
            json_encode([
                'transaction_id' => $basket->getId(),
                'value' => $basket->getPrice()->getValue(),
                'tax' => $basket->getPrice()->getTaxValue(),
                'shipping' => $basket->getPrice()->getCosts(),
                'currency' => $basket->getPrice()->getCurrencyId(),
                'items' => $items
            ])
        );

        return $result;
    }
}

Method 3: Cart Extension (EXT:cart)

Lightweight shopping cart for TYPO3.

TypoScript Setup

plugin.tx_cart {
    settings {
        googleAnalytics {
            enabled = 1
            measurementId = {$analytics.ga4.measurementId}
        }
    }
}

Add Signal Slot for Tracking

<?php
namespace Vendor\Extension\Slots;

use Extcode\Cart\Domain\Model\Cart\Product;

class CartSlot
{
    public function afterAddToCart(Product $product)
    {
        $GLOBALS['TSFE']->additionalFooterData['ga4_add_to_cart'] = sprintf(
            '<script>gtag("event", "add_to_cart", %s);</script>',
            json_encode([
                'currency' => 'EUR',
                'value' => $product->getGross(),
                'items' => [[
                    'item_id' => $product->getSku(),
                    'item_name' => $product->getTitle(),
                    'price' => $product->getPrice(),
                    'quantity' => $product->getQuantity()
                ]]
            ])
        );
    }
}

Register in ext_localconf.php:

$signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
    \TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class
);
$signalSlotDispatcher->connect(
    \Extcode\Cart\Utility\CartUtility::class,
    'afterAddToCartFinisher',
    \Vendor\Extension\Slots\CartSlot::class,
    'afterAddToCart'
);

Enhanced E-commerce with User Data

Customer Lifetime Value

[frontend.user.isLoggedIn == true]
page.jsFooterInline.300 = TEXT
page.jsFooterInline.300.dataWrap (
    // Set customer value
    gtag('set', 'user_properties', {
        'customer_ltv': '{TSFE:fe_user|user|lifetime_value}',
        'customer_type': '{TSFE:fe_user|user|customer_type}',
        'member_since': '{TSFE:fe_user|user|crdate}'
    });
)
[END]

Product Affinity Tracking

// Track product affinity based on category views
const categoryViews = JSON.parse(localStorage.getItem('typo3_category_views') || '{}');

gtag('set', 'user_properties', {
    'product_affinity': Object.keys(categoryViews)
        .sort((a, b) => categoryViews[b] - categoryViews[a])
        .slice(0, 3)
        .join(',')
});

Refund Tracking

<?php
public function refundAction(\Vendor\Shop\Domain\Model\Order $order)
{
    // Process refund
    $this->orderService->processRefund($order);

    // Track refund in GA4
    $items = [];
    foreach ($order->getItems() as $item) {
        $items[] = [
            'item_id' => $item->getProduct()->getUid(),
            'item_name' => $item->getProduct()->getTitle(),
            'price' => $item->getPrice(),
            'quantity' => $item->getQuantity()
        ];
    }

    $GLOBALS['TSFE']->additionalFooterData['ga4_refund'] = sprintf(
        '<script>gtag("event", "refund", %s);</script>',
        json_encode([
            'transaction_id' => $order->getOrderNumber(),
            'value' => $order->getTotal(),
            'currency' => 'EUR',
            'items' => $items
        ])
    );
}

Testing E-commerce Tracking

1. Enable GA4 DebugView

page.jsFooterInline.999 = TEXT
page.jsFooterInline.999.value (
    gtag('config', '{$analytics.ga4.measurementId}', {
        'debug_mode': true
    });
)

2. Test Each Event

  1. View Item: Navigate to product detail page
  2. Add to Cart: Add product and check network tab for collect request
  3. Begin Checkout: Start checkout process
  4. Purchase: Complete test order

3. Validate in GA4

  • Admin → DebugView: See events in real-time
  • Reports → Monetization → Ecommerce purchases: Check purchase data

Next Steps