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/:
| File | Role |
|---|---|
tracking/trackingmanager.js | Singleton manager: init(), the Cookiebot consent gate (hasConsent()), the event queue + replay, the track* API, auto-registers adapters. |
tracking/adapters/base.js | Base adapter class (common interface, isReady(), debug helpers). |
tracking/adapters/meta.js · ga4.js · pinterest.js | The platform adapters → fbq() / gtag() / pintrk(). |
custom.js | Calls trackingManager.init({...}) on load; fires trackSearch. |
configurator.js, bcmyquotes.js | Fire product/quote events through the manager. |
Server-side / template:
| File | Role |
|---|---|
system_overrides/BcBehaviorTracking.php | Builds the initial GTM dataLayer (getGtmDataLayerData()) pushed into window.dataLayer in the page head. |
docroot/templates/betacalco2/index.php | The 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.
- The module loader (RequireJS / AMD). The template preloads
kenedo/external/requirejs-2.3.6/require.jscom_configbox/assets/main.js(the RequireJS config that exposescbrequire/define) andcustom.js.custom.jscbrequirestrackingmanager.js, which in turncbrequires each adapter (configbox/custommaps to the customization JS dir). This loads our code — not the platform SDKs.
- 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 bytemplates/betacalco2/index.php. GTM readswindow.dataLayer(seeded server-side byBcBehaviorTracking::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. - The CMP — Cookiebot. The consent-management platform loads early, shows the banner, writes the
CookieConsentcookie, and firesCookiebotOnAccept/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.) - Microsoft Clarity (session recording), consent-gated in the template. On a Cookiebot consent event,
templates/betacalco2/index.phploads Clarity (clarity.ms/tag/…) only ifstatisticsconsent is granted, then toggles it withclarity('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.
Consent (Cookiebot — the CMP)
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 category | Events gated on it |
|---|---|
statistics | trackViewContent (the view / content event) |
marketing | trackAddToCart, 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 method | Meta Pixel | GA4 | |
|---|---|---|---|
trackViewContent | ViewContent | view_item | pagevisit |
trackAddToCart | AddToCart | add_to_cart | addtocart |
trackBeginCheckout | InitiateCheckout | begin_checkout | checkout |
trackLead | Lead | generate_lead | lead |
trackSearch | Search | search | search |
trackCustomEvent | custom | custom | custom |
Where events fire today
Line numbers drift, so these are described by file + action (grep the method name to find them):
configurator.js—trackViewContentwhen the configurator loads;trackAddToCartwhen a configured item is saved (readsconfigurator.getConfiguratorData('productId'|'productName'|'productCategory')).bcmyquotes.js—trackAddToCarton copy-position and update-position;trackBeginCheckoutwhen the quote-request modal opens;trackLeadon successful quote submission.custom.js—trackSearchfrom 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)
- Create
tracking/adapters/tiktok.jsextendingbase.js; implementisReady()and thetrack*methods, mapping to the platform SDK. - Add it to the adapter list in
trackingmanager.js:{ path: 'configbox/custom/tracking/adapters/tiktok', name: 'TikTok' }. - 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
CookieConsentcookie), then SDK presence (typeof fbq…), thenisReady(). - 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-idinstead ofproduct-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.