E-commerce platform

Server-Side Tracking for Shopware 6

Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your Shopware 6 store and forwards clean, deduplicated conversions to GA4, Meta CAPI, Google Ads and more. Live in minutes, flat monthly pricing.

No credit card to startFlat monthly pricingNative Shopware 6 dataLayerGA4 · Meta CAPI · Google Ads
Why it matters for Shopware 6

The pixel alone is leaking your data.

Browser-side tracking on Shopware 6 loses a growing share of conversions. Here's what moving to a server-side setup fixes.

Recover conversions lost to ITP & ad-blockers

On Shopware 6, browser-side pixels are throttled by Safari/Firefox ITP, capped 7-day cookies, and blocked outright by ad-blockers and consent walls. Server-side tracking moves the measurement off the browser, so purchases and add-to-carts that the pixel silently drops are recovered and reported.

First-party data, served from your own domain

TrakSolv hosts your Google Tag Manager server container on a subdomain of your Shopware 6 site. Requests come from a first-party context — they survive 3rd-party cookie deprecation, last far longer than client-side cookies, and look like your own traffic, not a tracker.

Clean, deduplicated conversions to every ad platform

The Shopware 6 dataLayer fires every GA4 enhanced-ecommerce event with a stable event_id, so your server-side container can forward purchases and add-to-carts to GA4, Meta CAPI, Google Ads Enhanced Conversions, TikTok and more — deduplicated against the browser pixel, with higher match quality.

Step by step

How to set up server-side tracking on Shopware 6

Paste the TrakSolv loader and the Shopware 6 dataLayer snippet into your store's custom-code area, save, and you're done. Every step below has the exact snippet with a copy button.

Install guide

How to install on Shopware 6

This is your install. Every step below has its exact snippet with a Copy button — go step by step. The Loader card above and DataLayer card below are reference copies of the same snippets.

  1. 1

    Open your storefront theme settings

    Navigate

    Shopware Admin → Settings → Sales Channels → your storefront → Theme → Customize

    Look for the 'Custom JavaScript' / 'Additional Code' field. If your theme doesn't expose one, override Resources/views/storefront/base.html.twig directly.

  2. 2

    Loader — head half

    Loader • Head

    Theme → Custom HTML/JS (Header) → paste the Loader Head snippet

  3. 3

    Loader — body half + DataLayer

    Loader • Body

    Theme → Custom HTML/JS (Footer) → paste the Loader Body snippet FIRST

  4. 4

    DataLayer snippet

    DataLayer

    Same Footer field → append the DataLayer snippet right after the Body

    The DataLayer subscribes to document.$emitter for addToCart / removeLineItem events — Shopware's native storefront event bus.

    HTML
    <!-- Traksolv · Shopware 6 DataLayer — Theme Custom JS / base.html.twig -->
    <script>
    (function (w, d) {
      w.dataLayer = w.dataLayer || [];
      var DEBUG = false;
      try {
        var q = new URLSearchParams(location.search);
        if (q.get('tcDebug') === '1') { DEBUG = true; try { localStorage.setItem('tcDebug', '1'); } catch (e) {} }
        else if (q.get('tcDebug') === '0') { DEBUG = false; try { localStorage.removeItem('tcDebug'); } catch (e) {} }
        else { try { if (localStorage.getItem('tcDebug') === '1') DEBUG = true; } catch (e) {} }
      } catch (e) {}
      w.tcDebug = DEBUG;
      if (DEBUG && w.console && w.console.log) {
        try { w.console.log('%c[Traksolv]%c dataLayer debug ON — every push will log here. Disable with ?tcDebug=0', 'color:#22c55e;font-weight:600;background:#0a1f0c;padding:2px 6px;border-radius:3px', ''); } catch (e) {}
      }
      function log() {
        if (!DEBUG || !w.console || !w.console.log) return;
        try { w.console.log.apply(w.console, ['[Traksolv]'].concat([].slice.call(arguments))); } catch (e) {}
      }
      function push(eventName, eventId, params) {
        try {
          if (params && params.ecommerce !== undefined) w.dataLayer.push({ ecommerce: null });
          var ev = { event: eventName, event_id: eventName + '.' + eventId };
          if (params) for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) ev[k] = params[k];
          w.dataLayer.push(ev);
          log('push ' + eventName, ev);
        } catch (e) {}
      }
      function once(key) {
        try { if (sessionStorage.getItem(key)) return false; sessionStorage.setItem(key, '1'); return true; }
        catch (e) { return true; }
      }
      function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; }
      function textOf(sel, root) { var el = (root || d).querySelector(sel); return el ? (el.textContent || '').trim() : ''; }
      function attrOf(sel, attr, root) { var el = (root || d).querySelector(sel); return el ? (el.getAttribute(attr) || '') : ''; }
      function currency() {
        return attrOf('meta[itemprop="priceCurrency"]', 'content') ||
               attrOf('[data-currency-iso]', 'data-currency-iso') ||
               attrOf('[data-currency-symbol]', 'data-currency-symbol') ||
               'EUR';
      }
    
      w.tcFirePurchase = function (o) {
        try {
          if (!o || !o.orderId) return;
          if (!once('tc_purchase_' + o.orderId)) return;
          push('purchase', String(o.orderId), {
            ecommerce: {
              transaction_id: String(o.orderId),
              value:    o.value || 0,
              currency: o.currency || currency(),
              items:    Array.isArray(o.items) ? o.items : []
            }
          });
        } catch (e) {}
      };
    
      // Shopware sets data-route on the body tag for every page.
      // Real route names: frontend.detail.page, frontend.navigation.page,
      // frontend.checkout.cart.page, frontend.checkout.confirm.page,
      // frontend.checkout.finish.page.
      function pageType() {
        var route = (d.body && d.body.getAttribute('data-route')) || '';
        if (/checkout\.finish/.test(route))                return 'order';
        if (/checkout\.confirm/.test(route))               return 'checkout';
        if (/checkout\.cart/.test(route))                  return 'cart';
        if (/detail/.test(route))                           return 'product';
        if (/navigation|listing|search/.test(route))        return 'category';
        return 'other';
      }
      w.tcPageType = pageType;
    
      // ── PDP — view_item ──
      function fireViewItem() {
        try {
          var form = d.querySelector('form[data-add-to-cart], .product-detail-buy form');
          var pid  = (form && form.querySelector('input[name="product-id"], input[name="lineItems[0][referencedId]"], input[name="lineItems[][referencedId]"]'));
          pid = pid ? pid.value : '';
          pid = pid || attrOf('meta[itemprop="productID"]', 'content') ||
                      textOf('[itemprop="sku"]') ||
                      attrOf('.product-detail-ordernumber-content', 'data-product-id');
          if (!pid) return;
          if (!once('tc_view_item_' + pid)) return;
          var name = textOf('.product-detail-name, h1[itemprop="name"], h1.product-name');
          var price = money(attrOf('meta[itemprop="price"]', 'content') ||
                            textOf('.product-detail-price'));
          var brand = textOf('.product-detail-manufacturer-link, [itemprop="brand"]');
          push('view_item', String(pid), {
            ecommerce: {
              currency: currency(),
              value:    price,
              items: [{
                item_id:    String(pid),
                item_name:  name,
                item_brand: brand || undefined,
                price:      price,
                quantity:   1
              }]
            }
          });
        } catch (e) {}
      }
    
      function fireViewItemList() {
        try {
          var cards = d.querySelectorAll('.cms-element-product-listing .product-box, .product-box');
          if (!cards.length) return;
          if (!once('tc_view_item_list_' + location.pathname)) return;
          var items = [];
          for (var i = 0; i < Math.min(cards.length, 25); i++) {
            var c = cards[i];
            var link = c.querySelector('a.product-name, a[href*="/detail/"], a[href*="/p/"]');
            var pid = c.getAttribute('data-product-id') ||
                      textOf('[itemprop="sku"]', c) ||
                      (link && link.href) || '';
            if (!pid) continue;
            items.push({
              item_id:   String(pid).slice(-64),
              item_name: textOf('.product-name, h3, [itemprop="name"], a.product-name', c),
              price:     money(attrOf('meta[itemprop="price"]', 'content', c) || textOf('.product-price', c)),
              index:     i
            });
          }
          if (!items.length) return;
          push('view_item_list', location.pathname, {
            ecommerce: {
              item_list_id:   location.pathname,
              item_list_name: textOf('h1') || location.pathname,
              items:          items
            }
          });
        } catch (e) {}
      }
    
      function readLineItems(rowSel) {
        var rows = d.querySelectorAll(rowSel);
        var items = []; var total = 0;
        for (var i = 0; i < rows.length; i++) {
          var r = rows[i];
          var name = textOf('.line-item-label, .line-item-product-name, .checkout-product-name, a', r);
          var price = money(textOf('.line-item-total-price, .line-item-price, .checkout-product-total', r));
          var qty = parseInt((textOf('.line-item-quantity-container input, .line-item-quantity', r) ||
                              attrOf('input[name*="quantity"]', 'value', r) || '1')
                              .replace(/\D/g, '') || '1', 10) || 1;
          var pid = r.getAttribute('data-product-id') ||
                    attrOf('input[name*="referencedId"]', 'value', r) ||
                    name;
          items.push({ item_id: String(pid), item_name: name, price: price, quantity: qty });
          total += price * qty;
        }
        return { items: items, total: total };
      }
    
      function fireCart(eventName) {
        try {
          var data = readLineItems('.line-item, .checkout-product, .cart-item');
          if (!data.items.length) return;
          var key = location.pathname + '.' + data.items.length;
          if (!once('tc_' + eventName + '_' + key)) return;
          push(eventName, key.slice(0, 64), {
            ecommerce: { currency: currency(), value: data.total, items: data.items }
          });
        } catch (e) {}
      }
    
      function firePurchase() {
        try {
          var orderId = textOf('.finish-ordernumber strong') ||
                        textOf('.finish-ordernumber') ||
                        attrOf('[data-order-id]', 'data-order-id') ||
                        attrOf('input[name="orderId"]', 'value') ||
                        '';
          if (!orderId) {
            var ld = d.querySelector('script[type="application/ld+json"]');
            if (ld) try { var p = JSON.parse(ld.textContent); if (p && p.orderNumber) orderId = p.orderNumber; } catch (e) {}
          }
          // Strip "Order number: " label prefix some themes ship.
          orderId = orderId.replace(/^[^0-9A-Za-z]+/, '');
          orderId = orderId || ('order.' + Date.now());
          if (!once('tc_purchase_' + orderId)) return;
          var data = readLineItems('.checkout-product, .finish-product-table .line-item, .finish-product, .line-item');
          var total = money(textOf('.finish-total-amount, .checkout-total-amount, [data-order-total]'));
          if (!total) total = data.total;
          push('purchase', String(orderId), {
            ecommerce: {
              transaction_id: String(orderId),
              value:    total,
              currency: currency(),
              items:    data.items
            }
          });
        } catch (e) {}
      }
    
      // ── Hook Shopware plugin emitters (per-plugin pattern) ──
      // For each AddToCart form on the page, subscribe to its plugin
      // instance's emitter. AddToCart is initialised on form[data-add-to-cart].
      function hookAddToCartPlugins() {
        try {
          if (!w.PluginManager || typeof w.PluginManager.getPluginInstanceFromElement !== 'function') return false;
          var forms = d.querySelectorAll('form[data-add-to-cart]');
          if (!forms.length) return false;
          for (var i = 0; i < forms.length; i++) {
            (function (form) {
              try {
                var inst = w.PluginManager.getPluginInstanceFromElement(form, 'AddToCart');
                if (!inst || !inst.$emitter || typeof inst.$emitter.subscribe !== 'function') return;
                inst.$emitter.subscribe('beforeFormSubmit', function (ev) {
                  try {
                    var formData = (ev && ev.detail && ev.detail.formData) || null;
                    var pid = (formData && (formData.get('lineItems[' + (formData.get('lineItems[][referencedId]') ? '' : '0') + '][referencedId]') ||
                                            formData.get('lineItems[][referencedId]'))) ||
                              (form.querySelector('input[name*="referencedId"]') || {}).value || '';
                    var qty = parseInt((form.querySelector('input[name*="quantity"]') || {}).value || '1', 10) || 1;
                    var name = textOf('.product-detail-name, h1[itemprop="name"]') ||
                               textOf('.product-name', form.closest('.product-box')) ||
                               textOf('.product-name', form) || '';
                    var price = money(attrOf('meta[itemprop="price"]', 'content') ||
                                      textOf('.product-detail-price') ||
                                      textOf('.product-price', form.closest('.product-box')));
                    push('add_to_cart', String(pid || 'item') + '.' + Date.now(), {
                      ecommerce: {
                        currency: currency(),
                        value:    price * qty,
                        items: [{ item_id: String(pid || name || 'item'), item_name: name, price: price, quantity: qty }]
                      }
                    });
                  } catch (e) {}
                });
              } catch (e) {}
            })(forms[i]);
          }
          return true;
        } catch (e) { return false; }
      }
    
      // OffCanvasCart plugin lives on .header-cart-btn (and any data-offcanvas-cart anchor).
      function hookOffCanvasCart() {
        try {
          if (!w.PluginManager || typeof w.PluginManager.getPluginInstanceFromElement !== 'function') return false;
          var btn = d.querySelector('[data-offcanvas-cart], .header-cart-btn');
          if (!btn) return false;
          var inst = w.PluginManager.getPluginInstanceFromElement(btn, 'OffCanvasCart');
          if (!inst || !inst.$emitter || typeof inst.$emitter.subscribe !== 'function') return false;
    
          inst.$emitter.subscribe('onRemoveProductFromCart', function (ev) {
            try {
              // The event target is the remove-button inside the offcanvas line-item row.
              var btn = (ev && ev.detail && ev.detail.event && ev.detail.event.target) || null;
              var row = btn && btn.closest && btn.closest('.line-item, .cart-item, .js-pseudo-modal .line-item');
              var name = row ? textOf('.line-item-label, .line-item-product-name, a', row) : '';
              var price = row ? money(textOf('.line-item-total-price, .line-item-price', row)) : 0;
              var qty = 1;
              var pid = (row && (row.getAttribute('data-product-id') ||
                                 attrOf('input[name*="referencedId"]', 'value', row))) || name || 'item';
              push('remove_from_cart', String(pid) + '.' + Date.now(), {
                ecommerce: {
                  currency: currency(),
                  value:    price * qty,
                  items: [{ item_id: String(pid), item_name: name, price: price, quantity: qty }]
                }
              });
            } catch (e) {}
          });
    
          inst.$emitter.subscribe('offCanvasOpened', function () {
            // Treat each offcanvas-open as a view_cart impression.
            try {
              var rows = d.querySelectorAll('.offcanvas .line-item, .offcanvas-body .line-item');
              if (!rows.length) return;
              var items = []; var total = 0;
              for (var i = 0; i < rows.length; i++) {
                var r = rows[i];
                var name = textOf('.line-item-label, a', r);
                var price = money(textOf('.line-item-total-price, .line-item-price', r));
                var qty = parseInt((textOf('.line-item-quantity', r) || '1').replace(/\D/g, '') || '1', 10) || 1;
                var pid = r.getAttribute('data-product-id') ||
                          attrOf('input[name*="referencedId"]', 'value', r) || name;
                items.push({ item_id: String(pid), item_name: name, price: price, quantity: qty });
                total += price * qty;
              }
              push('view_cart', 'offcanvas.' + Date.now(), {
                ecommerce: { currency: currency(), value: total, items: items }
              });
            } catch (e) {}
          });
          return true;
        } catch (e) { return false; }
      }
    
      function attachEmitters() {
        var attempts = 0;
        var iv = setInterval(function () {
          var ok1 = hookAddToCartPlugins();
          var ok2 = hookOffCanvasCart();
          if ((ok1 && ok2) || ++attempts > 40) clearInterval(iv);
        }, 250);
      }
    
      function boot() {
        var t = pageType();
        if (t === 'product')  fireViewItem();
        if (t === 'category') fireViewItemList();
        if (t === 'cart')     fireCart('view_cart');
        if (t === 'checkout') fireCart('begin_checkout');
        if (t === 'order')    firePurchase();
        attachEmitters();
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot);
      else boot();
    })(window, document);
    </script>
    <!-- End Traksolv Shopware DataLayer -->
    
    <!-- Traksolv · DataLayer enrichment — customer lifecycle + remarketing + attribution -->
    <script>
    (function (w, d) {
      function readLT() {
        try {
          var raw = localStorage.getItem('tc_lifetime');
          var p = raw ? JSON.parse(raw) : null;
          return {
            orderCount:    (p && p.orderCount)    || 0,
            lifetimeValue: (p && p.lifetimeValue) || 0,
            firstOrderTs:  (p && p.firstOrderTs)  || null,
            lastOrderTs:   (p && p.lastOrderTs)   || null,
            lastOrderId:   (p && p.lastOrderId)   || null
          };
        } catch (e) {
          return { orderCount: 0, lifetimeValue: 0, firstOrderTs: null, lastOrderTs: null, lastOrderId: null };
        }
      }
      function writeLT(lt) {
        try { localStorage.setItem('tc_lifetime', JSON.stringify(lt)); } catch (e) {}
      }
      function tier(v) {
        if (v >= 1000) return 'vip';
        if (v >= 250)  return 'mid';
        if (v > 0)     return 'low';
        return 'guest';
      }
      function readCookie(n) {
        try {
          var m = d.cookie.match(new RegExp('(^|; )' + n + '=([^;]+)'));
          return m ? decodeURIComponent(m[2]) : null;
        } catch (e) { return null; }
      }
      function readAttr() {
        try { var r = readCookie('_tc_attr'); return r ? JSON.parse(r) : null; } catch (e) { return null; }
      }
    
      w.tcReadLifetime    = readLT;
      w.tcCustomerTier    = function (v) { return tier(v != null ? v : readLT().lifetimeValue); };
      w.tcReadAttribution = readAttr;
    
      // ── Wrap tcFirePurchase to add customer-lifecycle + attribution ──
      // before the underlying snippet pushes the purchase event. Idempotent
      // per orderId, so customer refreshing the thank-you page doesn't
      // inflate their lifetime counters.
      var orig = w.tcFirePurchase;
      if (typeof orig === 'function') {
        w.tcFirePurchase = function (o) {
          try {
            if (!o || !o.orderId) return orig.apply(this, arguments);
            var lt = readLT();
            var firstFire = lt.lastOrderId !== String(o.orderId);
            var orderCount    = firstFire ? lt.orderCount + 1 : lt.orderCount;
            var lifetimeValue = firstFire ? lt.lifetimeValue + (o.value || 0) : lt.lifetimeValue;
            var firstOrderTs  = lt.firstOrderTs || Date.now();
            var newCustomer   = orderCount === 1;
            var daysSinceFirst = Math.floor((Date.now() - firstOrderTs) / 86400000);
    
            if (firstFire) {
              writeLT({
                orderCount:    orderCount,
                lifetimeValue: lifetimeValue,
                firstOrderTs:  firstOrderTs,
                lastOrderTs:   Date.now(),
                lastOrderId:   String(o.orderId)
              });
            }
    
            var attr = readAttr();
            // Build the enriched order. Preserve every original field; add
            // customer + attribution on top.
            var enriched = {};
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) enriched[k] = o[k];
            enriched.new_customer            = newCustomer;
            enriched.customer_type           = newCustomer ? 'new' : 'returning';
            enriched.customer_status         = 'customer';
            enriched.customer_lifetime_value = Math.round(lifetimeValue * 100) / 100;
            enriched.customer_order_count    = orderCount;
            enriched.customer_aov            = orderCount > 0 ? Math.round((lifetimeValue / orderCount) * 100) / 100 : 0;
            enriched.purchase_number         = orderCount;
            enriched.days_since_first_order  = daysSinceFirst;
            enriched.value_tier              = tier(lifetimeValue);
            if (attr) enriched.attribution = {
              landing:   attr.u   || null,
              referrer:  attr.r   || null,
              utm:       attr.utm || null,
              click_ids: attr.cid || null
            };
            return orig.call(this, enriched);
          } catch (e) {
            try { return orig.apply(this, arguments); } catch (e2) {}
          }
        };
      }
    
      // ── tc_visitor_context — emits visitor lifecycle + remarketing on
      //    every page-view. GTM / GA4 audience triggers + Google Ads
      //    dynamic remarketing tag read these.
      function visitorContext() {
        try {
          var lt = readLT();
          var attr = readAttr();
          var pageType = (typeof w.tcPageType === 'function') ? (w.tcPageType() || 'other') : 'other';
          w.dataLayer = w.dataLayer || [];
          w.dataLayer.push({
            event:                   'tc_visitor_context',
            event_id:                'tc_visitor_context.' + Date.now() + '.' + Math.random().toString(36).slice(2, 6),
            customer_status:         lt.orderCount > 0 ? 'returning_customer' : 'guest',
            customer_type:           lt.orderCount > 0 ? 'returning' : 'guest',
            value_tier:              tier(lt.lifetimeValue),
            customer_lifetime_value: Math.round(lt.lifetimeValue * 100) / 100,
            customer_order_count:    lt.orderCount,
            days_since_first_order:  lt.firstOrderTs ? Math.floor((Date.now() - lt.firstOrderTs) / 86400000) : null,
            ecomm_pagetype:          pageType,
            attribution: attr ? {
              landing:   attr.u   || null,
              referrer:  attr.r   || null,
              utm:       attr.utm || null,
              click_ids: attr.cid || null
            } : null
          });
        } catch (e) {}
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', visitorContext);
      else visitorContext();
    })(window, document);
    </script>
    <!-- End Traksolv enrichment -->
  5. 5

    Save and rebuild the storefront theme

    Save

    Save → Theme → Save (rebuilds the storefront) → reload the storefront

    Shopware compiles the theme on save. Rebuilding takes 10-30s; refresh after.

The loader Head + Body snippets are generated for your container inside the TrakSolv dashboard. Create a free account to grab them, or see the full how-it-works walkthrough.

FAQ

Server-side tracking for Shopware 6, answered

Does server-side tracking work with Shopware 6?

Yes. TrakSolv works with Shopware 6 via a lightweight loader snippet plus a Shopware 6-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in Shopware 6's custom-code area and conversions start flowing through your own server-side GTM container.

How do I install TrakSolv on Shopware 6?

Add the TrakSolv loader (head + body) and the Shopware 6 dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most Shopware 6 merchants are live in a few minutes — no developer required.

What can I track server-side on Shopware 6?

Page views, product views, purchases and add-to-carts, begin-checkout and more — every standard GA4 enhanced-ecommerce event, forwarded server-side to GA4, Meta CAPI, Google Ads and other destinations with deduplication.

How much does server-side tracking for Shopware 6 cost?

TrakSolv uses flat monthly pricing — one event sent to many destinations still counts as one event, and paid plans never auto-pause at quota. You can start free and go live in minutes.

Recover your Shopware 6 conversions — start free.

Spin up a first-party server-side GTM container for Shopware 6 and watch your match quality climb. No credit card, live in minutes.

WhatsApp
Server-Side Tracking for Shopware 6 — TrakSolv | Traksolv