E-commerce platform

Server-Side Tracking for nopCommerce

Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your nopCommerce 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 nopCommerce dataLayerGA4 · Meta CAPI · Google Ads
Why it matters for nopCommerce

The pixel alone is leaking your data.

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

Recover conversions lost to ITP & ad-blockers

On nopCommerce, 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 nopCommerce 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 nopCommerce 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 nopCommerce

Paste the TrakSolv loader and the nopCommerce 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 nopCommerce

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

    Install Tracking Code Manager (recommended) or open your theme files

    Navigate

    nopCommerce Admin → Configuration → Widgets → Tracking Code Manager

    The free 'Tracking Code Manager' widget (foxnetsoft) gives you Header / Footer code boxes that don't require theme edits. Alternative: edit your theme's Themes/<YourTheme>/Views/Shared/_Root.cshtml directly — riskier if you ever update the theme.

  2. 2

    Loader — head half

    Loader • Head

    Tracking Code Manager → Header code field → paste the Loader Head snippet

  3. 3

    Loader — body half + DataLayer

    Loader • Body

    Tracking Code Manager → Footer code field → paste the Loader Body snippet FIRST

  4. 4

    DataLayer snippet

    DataLayer

    Tracking Code Manager → Footer code field → append the DataLayer right after the Body

    The DataLayer auto-detects page-type from URL + default-theme DOM selectors, and intercepts /addproducttocart and /removefromcart AJAX calls for live cart events.

    HTML
    <!-- Traksolv · nopCommerce DataLayer — paste in Tracking Code Manager footer or _Root.cshtml -->
    <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') ||
               textOf('.currency-code') || 'USD';
      }
    
      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:    Number(o.value) || 0,
              currency: o.currency || currency(),
              items:    Array.isArray(o.items) ? o.items : []
            }
          });
        } catch (e) {}
      };
    
      function pageType() {
        var path = (location.pathname || '').toLowerCase();
        if (/\/checkout\/completed/.test(path) || /\/orderdetails\//.test(path)) return 'order';
        if (/\/(onepagecheckout|checkout)/.test(path)) return 'checkout';
        if (/^\/cart$/.test(path) || /\/cart\?/.test(path) || /\/shoppingcart/.test(path)) return 'cart';
        if (d.querySelector('.product-essential, .product-details-page')) return 'product';
        if (d.querySelector('.category-page, .manufacturer-page, .item-grid')) return 'category';
        if (/^\/search/.test(path)) return 'search';
        return 'other';
      }
      w.tcPageType = pageType;
    
      // ── PDP — view_item ──
      function fireViewItem() {
        try {
          var pid = attrOf('input[name="productId"]', 'value') ||
                    attrOf('[data-productid]', 'data-productid') ||
                    attrOf('meta[itemprop="productID"]', 'content') ||
                    (location.pathname || '').slice(1);
          if (!pid) return;
          if (!once('tc_view_item_' + pid)) return;
          var name = textOf('.product-name h1, h1[itemprop="name"], .product-name');
          var price = money(textOf('.prices .actual-price, .product-price .price, [itemprop="price"]') ||
                            attrOf('meta[itemprop="price"]', 'content'));
          var sku  = textOf('.sku-number, [itemprop="sku"]');
          var brand = textOf('.manufacturers a, [itemprop="brand"]');
          push('view_item', String(pid), {
            ecommerce: {
              currency: currency(),
              value:    price,
              items: [{
                item_id:    String(pid),
                item_name:  name,
                item_brand: brand || undefined,
                item_sku:   sku || undefined,
                price:      price,
                quantity:   1
              }]
            }
          });
        } catch (e) {}
      }
    
      function fireViewItemList() {
        try {
          var cards = d.querySelectorAll('.item-grid .item-box, .product-grid .item-box, .product-list .item-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('.product-title a, h2 a, a.product-title');
            var pid = c.getAttribute('data-productid') ||
                      (link && link.href ? link.href.split('/').pop().split('?')[0] : '');
            if (!pid) continue;
            items.push({
              item_id:   String(pid).slice(-64),
              item_name: textOf('.product-title, h2, a.product-title', c),
              price:     money(textOf('.actual-price, .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 readCartRows() {
        var rows = d.querySelectorAll('table.cart .cart-item-row, .order-summary-content tr.cart-item-row, .opc .cart-item-row');
        var items = []; var total = 0;
        for (var i = 0; i < rows.length; i++) {
          var r = rows[i];
          var name = textOf('.product-name a, .product-name', r);
          var price = money(textOf('.unit-price .product-unit-price, .product-unit-price', r));
          var qty = parseInt((textOf('.qty input, input.qty-input, .quantity', r) ||
                              attrOf('input[name*="quantity"]', 'value', r) || '1').replace(/\D/g, '') || '1', 10) || 1;
          var pid = attrOf('input[name*="itemquantity"]', 'name', r).split('_').pop() ||
                    attrOf('a[data-productid]', 'data-productid', 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 d2 = readCartRows();
          if (!d2.items.length) return;
          var key = location.pathname + '.' + d2.items.length;
          if (!once('tc_' + eventName + '_' + key)) return;
          push(eventName, key.slice(0, 64), {
            ecommerce: { currency: currency(), value: d2.total, items: d2.items }
          });
        } catch (e) {}
      }
    
      function firePurchase() {
        try {
          var orderId = textOf('.order-completed .order-number strong') ||
                        textOf('.order-number strong, .order-number') ||
                        (location.pathname.match(/\/checkout\/completed\/?(\d+)?/) || [])[1] ||
                        (location.pathname.match(/\/orderdetails\/(\d+)/) || [])[1] ||
                        String(w.OrderId || '');
          orderId = String(orderId).replace(/[^0-9A-Za-z\-_]/g, '');
          if (!orderId) return;
          if (!once('tc_purchase_' + orderId)) return;
    
          // Read order detail rows if present (only on /orderdetails/{id} —
          // /checkout/completed often just shows order number + 'thank you').
          var rows = d.querySelectorAll('.order-details-area .data-table tr.cart-item-row, table.data-table tr.cart-item-row');
          var items = []; var total = 0;
          for (var i = 0; i < rows.length; i++) {
            var r = rows[i];
            var name = textOf('.product a, .product-name a, .product', r);
            var price = money(textOf('.product-unit-price', r));
            var qty = parseInt((textOf('.product-quantity', r) || '1').replace(/\D/g, '') || '1', 10) || 1;
            items.push({ item_id: name || ('item.' + i), item_name: name, price: price, quantity: qty });
            total += price * qty;
          }
          if (!total) total = money(textOf('.order-total-value, .order-total .value, .cart-total-right .product-price'));
          push('purchase', String(orderId), {
            ecommerce: {
              transaction_id: String(orderId),
              affiliation:    'nopCommerce',
              value:          total,
              currency:       currency(),
              items:          items
            }
          });
        } catch (e) {}
      }
    
      // ── Live cart events: fetch interceptor on nopCommerce cart endpoints ──
      (function () {
        if (!w.fetch || w._tcNopCartPatched) return;
        w._tcNopCartPatched = true;
        var orig = w.fetch.bind(w);
        w.fetch = function (input, init) {
          var url = (typeof input === 'string') ? input : (input && input.url) || '';
          var method = ((init && init.method) || (typeof input === 'object' && input && input.method) || 'GET').toUpperCase();
          var isAdd = /\/addproducttocart\//i.test(url || '');
          var isRem = method === 'POST' && /\/(removefromcart|shoppingcart)/i.test(url || '');
          var p = orig(input, init);
          if (!isAdd && !isRem) return p;
          return p.then(function (res) {
            try {
              // Add: URL path carries productId — /addproducttocart/details/{productId}/{cartType}
              if (isAdd) {
                var m = url.match(/\/addproducttocart\/(?:details|catalog)\/(\d+)(?:\/(\d+))?(?:\/(\d+))?/i);
                var pid = m && m[1];
                var qty = parseInt(m && m[3] || '1', 10) || 1;
                if (pid) {
                  // Read product info from the page if it's the PDP.
                  var name = textOf('.product-name h1, h1[itemprop="name"]') || ('product.' + pid);
                  var price = money(textOf('.prices .actual-price, [itemprop="price"]') || attrOf('meta[itemprop="price"]', 'content'));
                  push('add_to_cart', String(pid) + '.' + Date.now(), {
                    ecommerce: {
                      currency: currency(),
                      value:    price * qty,
                      items: [{ item_id: String(pid), item_name: name, price: price, quantity: qty }]
                    }
                  });
                }
              } else if (isRem) {
                // nopCommerce's removeFromCart doesn't echo what was removed —
                // emit a minimal remove_from_cart with a synthetic key so
                // downstream tools can see SOMETHING fired.
                push('remove_from_cart', 'cart.' + Date.now(), {
                  ecommerce: { currency: currency(), value: 0, items: [] }
                });
              }
            } catch (e) {}
            return res;
          });
        };
      })();
    
      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();
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot);
      else boot();
    })(window, document);
    </script>
    <!-- End Traksolv nopCommerce 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 clear nopCommerce cache

    Save

    Save → System → Maintenance → Clear cache

    nopCommerce aggressively caches view output — without clearing the cache the snippets may not appear immediately.

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 nopCommerce, answered

Does server-side tracking work with nopCommerce?

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

How do I install TrakSolv on nopCommerce?

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

What can I track server-side on nopCommerce?

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 nopCommerce 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 nopCommerce conversions — start free.

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

WhatsApp
Server-Side Tracking for nopCommerce — TrakSolv | Traksolv