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
- View Item: Navigate to product detail page
- Add to Cart: Add product and check network tab for
collectrequest - Begin Checkout: Start checkout process
- Purchase: Complete test order
3. Validate in GA4
- Admin → DebugView: See events in real-time
- Reports → Monetization → Ecommerce purchases: Check purchase data