Conventions
Casingโ
- Event names:
snake_case. Useadd_to_cart, notaddToCartorAddToCart. - Parameter names:
snake_case. Useitem_id,transaction_id,currency. - These are the GA4 standard names โ the agency maps them to Meta names (
AddToCart,Purchaseโฆ) in GTM.
Data typesโ
| Expected type | OK | Not OK |
|---|---|---|
| Number (prices, values) | price: 42.00 | price: "42.00" |
| Number (quantity) | quantity: 2 | quantity: "2" |
| String (IDs, names) | item_id: "BIO-001" | item_id: 1 |
| String ISO 4217 | currency: "EUR" | currency: "โฌ" |
โ ๏ธ No thousands separator: 1299.50, never "1,299.50" or "1 299,50".
โ ๏ธ Units, not cents: 42.00 (โฌ), not 4200.
When to pushโ
| Event type | When to push |
|---|---|
view_item, view_item_list, view_cart | After the page/section renders |
add_to_cart, remove_from_cart | After the server confirms the cart mutation (not on optimistic click) |
begin_checkout | When the user lands on step 1 of checkout |
add_shipping_info, add_payment_info | After step validation (submit OK) |
purchase | Once, on the confirmation page, after DB commit |
refund | When the refund is issued (back-office) |
search | After results render |
sign_up, generate_lead | After server-side confirmation |
Product ID โ the golden ruleโ
The item_id you push must be identical to the id in the Meta Catalog feed and Google Merchant Center.
| Source | Field |
|---|---|
| Site (dataLayer) | items[].item_id |
| Meta Product Catalog | id |
| Google Merchant Center | id |
Recommended: use the main SKU (e.g. BIO-CRM-001). Not internal DB UUIDs.
For variants (size, scent): item_id: "BIO-CRM-001-50ML" + item_variant: "50ml".
purchase idempotencyโ
purchase must be pushed exactly once per order. Refreshes on the confirmation page must not re-push.
Options:
- Redirect immediately to a confirmation page that doesn't re-push on refresh.
- Persist the pushed
transaction_idinlocalStorageand skip if already there:
const txId = 'ORD-2026-00123';
const pushed = JSON.parse(localStorage.getItem('bio_pushed_tx') || '[]');
if (!pushed.includes(txId)) {
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({ event: 'purchase', ecommerce: { transaction_id: txId, /* ... */ } });
pushed.push(txId);
localStorage.setItem('bio_pushed_tx', JSON.stringify(pushed.slice(-50)));
}
Debugging during devโ
In the browser console:
window.dataLayer
// โ array of every event pushed since page load
window.dataLayer.filter(e => e.event === 'purchase')
// โ just purchases
More debug tools: Debug tools.
GTM snippetโ
The agency will provide the exact snippet. Plan to insert:
<!-- In <head>, before any other script -->
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://load.analytics.biosphereskincare.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');
</script>
<!-- Right after <body> -->
<noscript><iframe src="https://load.analytics.biosphereskincare.com/ns.html?id=GTM-XXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
โ ๏ธ The snippet uses load.analytics.biosphereskincare.com (first-party) instead of www.googletagmanager.com. Intentional โ bypasses ad blockers.