Analytics Architecture on BigCommerce
BigCommerce is a SaaS ecommerce platform that uses the Stencil theme engine with Handlebars templates. Analytics tracking integrates through three mechanisms:
- Script Manager is BigCommerce's built-in interface for adding tracking scripts globally or per-page-type, with placement control (head, footer) and consent category support
- Stencil Handlebars templates expose product, category, and cart data through the
{{inject}}helper andjsContextobject, making structured data available to client-side scripts - Webhooks fire server-side HTTP callbacks on events like order creation, cart updates, and customer registration for server-side analytics
- Storefront GraphQL provides client-side access to cart contents, customer data, and product details without additional API calls
BigCommerce controls the checkout flow. On standard (non-headless) stores, the checkout page uses BigCommerce's optimized checkout, which limits custom script injection. Analytics on the checkout and order confirmation pages requires specific approaches.
For headless stores using the BigCommerce API with a custom frontend (Next.js, Gatsby, etc.), all analytics scripts live in the frontend framework, not in BigCommerce. The patterns below apply to Stencil-based storefronts.
Installing Tracking Scripts
Via Script Manager (Recommended)
Script Manager is the primary method for adding analytics tags to BigCommerce stores. In the BigCommerce admin:
- Navigate to Channel Manager > Script Manager
- Click Create a Script
- Configure placement, page scope, and script content
For GTM, create a script with these settings:
- Placement: Head
- Location: All pages
- Script category: Analytics
- Script type: Script
<!-- GTM Head Script (Script Manager) -->
<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://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');</script>
Create a second script for the noscript fallback with Placement: Footer.
Script Manager scripts are injected by BigCommerce's server before the page reaches the browser, so they persist across theme updates and do not require template file editing.
Via Stencil Theme Templates
For more control over script placement, add tracking code directly in the theme's Handlebars templates:
{{!-- templates/layout/base.html --}}
<!DOCTYPE html>
<html>
<head>
{{> head}}
{{{head.scripts}}}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'page_type': '{{page_type}}',
'currency_code': '{{currency_selector.active_currency_code}}'
});
</script>
</head>
<body>
{{{header}}}
{{{body}}}
{{{footer}}}
{{{foot.scripts}}}
</body>
</html>
Data Layer with Stencil Context
BigCommerce's Stencil framework uses the {{inject}} helper to pass server-side data to client-side JavaScript via the jsContext object. This is the proper way to build a data layer from product and category data.
Product Page Data Layer
{{!-- templates/pages/product.html --}}
{{inject "productId" product.id}}
{{inject "productName" product.title}}
{{inject "productPrice" product.price.without_tax.value}}
{{inject "productSku" product.sku}}
{{inject "productBrand" product.brand.name}}
{{inject "productCategory" product.category}}
{{inject "productAvailability" product.availability}}
<script>
var context = this.context || JSON.parse('{{jsContext}}');
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'items': [{
'item_id': context.productSku,
'item_name': context.productName,
'price': parseFloat(context.productPrice),
'item_brand': context.productBrand,
'item_category': context.productCategory
}]
}
});
</script>
Category Page Data Layer
{{!-- templates/pages/category.html --}}
{{inject "categoryName" category.name}}
{{inject "categoryId" category.id}}
<script>
var context = this.context || JSON.parse('{{jsContext}}');
window.dataLayer = window.dataLayer || [];
var items = [];
{{#each category.products}}
items.push({
'item_id': '{{sku}}',
'item_name': '{{name}}',
'price': {{price.without_tax.value}},
'item_list_name': context.categoryName,
'index': {{@index}}
});
{{/each}}
dataLayer.push({
'event': 'view_item_list',
'ecommerce': {
'item_list_name': context.categoryName,
'items': items
}
});
</script>
Ecommerce Tracking
Add to Cart
BigCommerce's Stencil framework fires a cart-item-add event through the storefront API. Hook into this in your theme's JavaScript:
// assets/js/theme/global/cart-analytics.js
import utils from '@bigcommerce/stencil-utils';
export default function trackCartEvents() {
$('body').on('ajax-cart-item-add', function(event, data) {
window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'items': [{
'item_id': data.product_id,
'item_name': data.name || '',
'quantity': data.quantity || 1
}]
}
});
});
}
Order Confirmation (Purchase Event)
The order confirmation page is rendered by BigCommerce's checkout system. Use the Storefront Analytics data that BigCommerce exposes on the confirmation page:
{{!-- templates/pages/order-confirmation.html --}}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': '{{checkout.order.id}}',
'value': {{checkout.order.total.value}},
'currency': '{{checkout.order.currency_code}}',
'tax': {{checkout.order.tax_total.value}},
'shipping': {{checkout.order.shipping_cost_total.value}},
'items': [
{{#each checkout.order.items}}
{
'item_id': '{{sku}}',
'item_name': '{{name}}',
'price': {{price.value}},
'quantity': {{quantity}}
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
});
</script>
Server-Side Purchase Tracking via Webhooks
For reliable purchase tracking that does not depend on the browser, use BigCommerce webhooks to send events via the GA4 Measurement Protocol:
// webhook-handler.js (your server)
app.post('/webhooks/bigcommerce/order-created', async (req, res) => {
const orderId = req.body.data.id;
// Fetch full order from BigCommerce API
const order = await fetch(
`https://api.bigcommerce.com/stores/${STORE_HASH}/v2/orders/${orderId}`,
{ headers: { 'X-Auth-Token': BC_API_TOKEN } }
).then(r => r.json());
// Send to GA4 Measurement Protocol
await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXX&api_secret=${API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: order.customer_message || 'server',
events: [{
name: 'purchase',
params: {
transaction_id: String(order.id),
value: parseFloat(order.total_inc_tax),
currency: order.currency_code
}
}]
})
}
);
res.sendStatus(200);
});
Register the webhook in BigCommerce under Settings > API Accounts or via the Management API with the store/order/created scope.
Checkout Tracking Limitations
BigCommerce's optimized checkout is a separate application from the Stencil storefront. Key constraints:
- Script Manager scripts with "All pages" scope do load on checkout pages, but scripts scoped to specific page types may not
- Custom Handlebars templates do not apply to the checkout flow (except the order confirmation page)
- Checkout SDK (BigCommerce's open-source checkout) allows full customization including analytics, but requires building a custom checkout
- Checkout page events (begin_checkout, add_payment_info, add_shipping_info) are best tracked via Script Manager scripts that listen for DOM changes on the checkout page
For the begin_checkout event, add a Script Manager script that fires when the checkout URL is detected:
<script>
if (window.location.pathname.includes('/checkout')) {
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'begin_checkout'
});
}
</script>
Common Errors
| Error | Cause | Fix |
|---|---|---|
| Scripts not loading on checkout | Script Manager scope excludes checkout pages | Set script scope to "All pages" or specifically include "Checkout" |
| Duplicate purchase events | Both client-side and webhook tracking fire | Use one method or deduplicate by transaction_id in GTM |
| Product data undefined in data layer | Missing {{inject}} helpers in Stencil template |
Add inject helpers before the script block in the template |
| Cart event not firing | Using native form submit instead of AJAX cart | Listen for ajax-cart-item-add event, not form submission |
| Price shows with tax when it should not | Using product.price.value instead of product.price.without_tax.value |
Use the explicit tax-included or tax-excluded price object |
| Scripts removed after theme update | Code added directly to theme files gets overwritten | Use Script Manager instead of editing theme templates |
| Order confirmation data empty | Accessing order data before checkout object is available | Wrap data layer push in a DOM ready check or use setTimeout |
| Webhook fires but GA4 shows no data | Missing or wrong api_secret in Measurement Protocol call |
Generate API secret in GA4 Admin > Data Streams > Measurement Protocol |
| Multi-currency prices incorrect | Data layer captures display currency, not base currency | Use {{currency_selector.active_currency_code}} and convert in GA4 |
| Storefront GraphQL returns null | API token missing or CORS not configured for custom domain | Verify storefront API token and allowed origins in BigCommerce settings |
Performance Considerations
- Script Manager placement: Use "Footer" placement for non-critical analytics scripts. Only GTM's head snippet needs "Head" placement for early loading
- Stencil bundle size: Analytics JavaScript added to theme bundles increases the main JS file size. Use Script Manager for analytics code to keep it separate from the theme bundle
- Lazy loading: BigCommerce's Stencil framework supports
{{region}}helpers for lazy-loaded content. Defer analytics initialization for below-the-fold product carousels - CDN caching: BigCommerce serves storefront pages through Akamai CDN. Injected Script Manager scripts are included in the cached response, so they do not add extra network requests
- Image optimization: Use BigCommerce's built-in image optimization (
{{getImageSrcset}}helper) to serve properly sized images. Unoptimized product images are the top cause of poor LCP on BigCommerce stores - Third-party tag audit: BigCommerce stores commonly accumulate review widgets, chat tools, and retargeting pixels. Audit total third-party script weight with Chrome DevTools' Coverage tab