Skip to main content

Conventions

Casingโ€‹

  • Event names: snake_case. Use add_to_cart, not addToCart or AddToCart.
  • Parameter names: snake_case. Use item_id, transaction_id, currency.
  • These are the GA4 standard names โ€” the agency maps them to Meta names (AddToCart, Purchaseโ€ฆ) in GTM.

Data typesโ€‹

Expected typeOKNot OK
Number (prices, values)price: 42.00price: "42.00"
Number (quantity)quantity: 2quantity: "2"
String (IDs, names)item_id: "BIO-001"item_id: 1
String ISO 4217currency: "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 typeWhen to push
view_item, view_item_list, view_cartAfter the page/section renders
add_to_cart, remove_from_cartAfter the server confirms the cart mutation (not on optimistic click)
begin_checkoutWhen the user lands on step 1 of checkout
add_shipping_info, add_payment_infoAfter step validation (submit OK)
purchaseOnce, on the confirmation page, after DB commit
refundWhen the refund is issued (back-office)
searchAfter results render
sign_up, generate_leadAfter 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.

SourceField
Site (dataLayer)items[].item_id
Meta Product Catalogid
Google Merchant Centerid

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_id in localStorage and 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.