Skip to main content

Analytics / conversion tracking

Audience: developers & AI agents · Scope: the unified front-end tracking system — the manager/adapter pattern, how the scripts load (RequireJS, GTM), and the Cookiebot consent gate · Last reviewed: 2026-07-11

TL;DR: Application code makes one platform-agnostic call (e.g. tracking.trackAddToCart(data)); a singleton Tracking Manager fans it out to every registered adapter (Meta Pixel, GA4, Pinterest). The platform SDKs are loaded by Google Tag Manager (GTM); the manager queues events until an adapter's SDK is ready, and sends nothing unless the visitor granted the matching Cookiebot consent (the CMP). Adding a platform = one adapter file.

Architecture

Application code (configurator.js, bcmyquotes.js, custom.js)
│ ONE call: tracking.trackViewContent(data)

Tracking Manager (tracking/trackingmanager.js — singleton, an AMD module)
│ • checks the Cookiebot consent cookie • enriches data (brand/currency defaults)
│ • queues events until adapters are ready • fans out to every enabled adapter
├──────────────┬───────────────┬───────────────
▼ ▼ ▼
Meta adapter GA4 adapter Pinterest adapter (tracking/adapters/*.js, extend base.js)
▼ ▼ ▼
fbq() gtag() pintrk() ← the SDKs are loaded by Google Tag Manager

The adapter pattern keeps application code platform-agnostic: change an event once and every platform updates; add/remove a platform without touching application code.

Files

Front-end, under docroot/components/com_configbox/data/customization/assets/javascript/:

FileRole
tracking/trackingmanager.jsSingleton manager: init(), the Cookiebot consent gate (hasConsent()), the event queue + replay, the track* API, auto-registers adapters.
tracking/adapters/base.jsBase adapter class (common interface, isReady(), debug helpers).
tracking/adapters/meta.js · ga4.js · pinterest.jsThe platform adapters → fbq() / gtag() / pintrk().
custom.jsCalls trackingManager.init({...}) on load; fires trackSearch.
configurator.js, bcmyquotes.jsFire product/quote events through the manager.

Server-side / template:

FileRole
system_overrides/BcBehaviorTracking.phpBuilds the initial GTM dataLayer (getGtmDataLayerData()) pushed into window.dataLayer in the page head.
docroot/templates/betacalco2/index.phpThe page head: seeds window.dataLayer, injects the GTM snippet ($customSettings->tag_manager_snippet), and wires Cookiebot consent to Microsoft Clarity (see the loading chain).

How it's initialized

custom.js (loaded on every page) requires the manager and calls init():

define([/* … */, 'configbox/custom/tracking/trackingmanager'], function (/* … */, trackingManager) {
trackingManager.init({ debug: true }); // init() is async; it cbrequire()s and registers the adapters
});

init(config) auto-registers the adapters from a hard-coded list in trackingmanager.js ({ path: 'configbox/custom/tracking/adapters/meta', name: 'Meta' }, plus ga4, pinterest). Config defaults: enabled: true, debug: false, defaultCurrency: 'USD', defaultBrand: 'Beta Calco'.

The loading chain (who loads what)

Four independent loaders cooperate; the manager tolerates any order via its event queue.

  1. The module loader (RequireJS / AMD). The template preloads kenedo/external/requirejs-2.3.6/require.js
    • com_configbox/assets/main.js (the RequireJS config that exposes cbrequire / define) and custom.js. custom.js cbrequires trackingmanager.js, which in turn cbrequires each adapter (configbox/custom maps to the customization JS dir). This loads our code — not the platform SDKs.
  2. Google Tag Manager (the tag loader). The GTM container snippet is not hard-coded — it is the admin setting tag_manager_snippet (ConfigBox settings), injected into the page head by templates/betacalco2/index.php. GTM reads window.dataLayer (seeded server-side by BcBehaviorTracking::getGtmDataLayerData()) and, on an All Pages trigger, loads each platform tag (Meta Pixel base code, GA4 config, Pinterest tag) → window.fbq / window.gtag / window.pintrk. So GTM owns which pixels exist; the adapters just detect and call them.
  3. The CMP — Cookiebot. The consent-management platform loads early, shows the banner, writes the CookieConsent cookie, and fires CookiebotOnAccept / CookiebotOnDecline. Both the Tracking Manager (below) and the template read that consent. (If a platform tag must not fire before consent, gate it in GTM/Cookiebot as well — the manager's gate only governs the manager's own calls, not tags GTM fires.)
  4. Microsoft Clarity (session recording), consent-gated in the template. On a Cookiebot consent event, templates/betacalco2/index.php loads Clarity (clarity.ms/tag/…) only if statistics consent is granted, then toggles it with clarity('consent') / clarity('consent', false) + clarity('stop'). Clarity is loaded directly there — not through the manager/adapters.

The manager queues every track* call and, on a short interval, replays queued events to each adapter once its isReady() is true (window.fbq/gtag/pintrk present) — so an event fired before GTM finishes loading isn't lost.

Operational caveat: because the pixels live in the GTM container, "a platform isn't firing" is usually a GTM problem (tag not published, or not triggered on All Pages), not a code problem — confirm each tag is live in the container.

Tracking is gated on Cookiebot consent. hasConsent(category) reads the CookieConsent cookie Cookiebot writes — a blob like {necessary:true,preferences:false,statistics:true,marketing:false} — and returns whether that category is true. With the category withheld, the manager logs "Not tracking because lacking marketing consent" and sends nothing.

Which event needs which category:

Consent categoryEvents gated on it
statisticstrackViewContent (the view / content event)
marketingtrackAddToCart, trackBeginCheckout, trackLead, trackSearch, trackCustomEvent (the conversion events)

Cookiebot also gates Microsoft Clarity on statistics (in the template — see the loading chain). Tests replay a real consent cookie rather than disabling Cookiebot — see ../testing/guide.md.

Using the API

tracking.trackViewContent({ productId: 5, productName: 'Axial Fan 24', category: 'Fans' });
tracking.trackAddToCart({ productId: 5, productName: 'Axial Fan 24', category: 'Fans', quantity: 2 });
tracking.trackBeginCheckout({ numItems: 3, contents: [{ id: 5, name: 'Axial Fan', quantity: 2 }] });
tracking.trackLead({ quoteId: 'Q-2024-001', numItems: 3, contents: [] });
tracking.trackSearch({ searchString: 'industrial fan' });
tracking.trackCustomEvent('ProductConfigured', { productId: 5, option: 'Motor', value: '3-phase' });

productId is required and must be the product id / SKU, the same across quotes for the same product — not the per-line position id. No pricing/value is sent (marketing decision: prices aren't shown to users), so omit price/value.

Event mapping

Manager methodMeta PixelGA4Pinterest
trackViewContentViewContentview_itempagevisit
trackAddToCartAddToCartadd_to_cartaddtocart
trackBeginCheckoutInitiateCheckoutbegin_checkoutcheckout
trackLeadLeadgenerate_leadlead
trackSearchSearchsearchsearch
trackCustomEventcustomcustomcustom

Where events fire today

Line numbers drift, so these are described by file + action (grep the method name to find them):

  • configurator.jstrackViewContent when the configurator loads; trackAddToCart when a configured item is saved (reads configurator.getConfiguratorData('productId'|'productName'|'productCategory')).
  • bcmyquotes.jstrackAddToCart on copy-position and update-position; trackBeginCheckout when the quote-request modal opens; trackLead on successful quote submission.
  • custom.jstrackSearch from the search box.

Required data attributes

Quote position rows must carry the product identity so the quote events can read it:

<tr class="position-row"
data-position-id="<?php echo $position->id; ?>"
data-product-id="<?php echo hsc($position->getProduct()->id); ?>"
data-product-name="<?php echo hsc($position->getProduct()->name); ?>">

bcmyquotes.js reads data-product-id / data-product-name from these rows. data-product-id must be the product id/SKU, not the position id — Meta matches it to the Product Catalog for dynamic ads/retargeting, and it's the join key for per-product conversion attribution.

Adding a new platform (e.g. TikTok)

  1. Create tracking/adapters/tiktok.js extending base.js; implement isReady() and the track* methods, mapping to the platform SDK.
  2. Add it to the adapter list in trackingmanager.js: { path: 'configbox/custom/tracking/adapters/tiktok', name: 'TikTok' }.
  3. Load the platform SDK via GTM (add the tag to the container). Done — the manager auto-registers the adapter and it receives every event.

Testing & troubleshooting

console.log(typeof fbq, typeof gtag, typeof pintrk); // each should be "function" once GTM has loaded them
document.cookie.split('; ').find(function (c) { return c.indexOf('CookieConsent=') === 0; }); // the Cookiebot blob

cbrequire(['configbox/custom/tracking/trackingmanager'], function (tracking) {
tracking.adapters.forEach(function (a) { console.log(a.name, 'ready:', a.isReady()); });
});
  • Nothing fires: check Cookiebot consent (the CookieConsent cookie), then SDK presence (typeof fbq…), then isReady().
  • Only some platforms fire: that platform's tag isn't published / triggered in the GTM container.
  • Wrong product in events: a row is sending position-id instead of product-id.
  • Verify in each platform: Meta Events Manager → Test Events; GA4 → Realtime; Pinterest → Conversions; Clarity → the recordings dashboard.

Possible follow-ups

  • Add price/value if it ever becomes user-visible; capture configurator option selections; per-user-type segmentation; a Meta Product Catalog for dynamic ads.