Instrument commerce events without drop-ins
If your storefront doesn’t use the AEM Commerce drop-ins — or replaces one with a custom block — you lose the automatic Adobe Client Data Layer (ACDL) instrumentation that the drop-ins provide. This tutorial shows you how to publish the same commerce events yourself using a small helper script, so that analytics, personalization, Live Search, Product Recommendations, and Adobe Experience Platform (AEP) forwarding keep working.
What You’ll Build
Section titled “What You’ll Build”By the end of this tutorial, you’ll have:
- A reusable
scripts/acdlHelper.jsfile that sets ACDL contexts and publishes events - A
product-page-viewevent fired from a custom PDP implementation - An
add-to-cartevent fired from a custom add-to-cart interaction
Prerequisites
Section titled “Prerequisites”Before starting this tutorial, make sure you have:
- A working Commerce storefront on Edge Delivery Services
- Basic understanding of JavaScript and asynchronous operations
- The following packages at version
1.17.0or later inpackage.json:
"@adobe/magento-storefront-event-collector": "^1.17.0","@adobe/magento-storefront-events-sdk": "^1.17.0"These packages are included by default in the AEM Commerce boilerplate, but the versions in an existing project may be outdated. Confirm the versions in package.json, then run npm install. If the packages are missing entirely, add them manually to package.json before running npm install.
Step 1: Add the ACDL helper script
Section titled “Step 1: Add the ACDL helper script”Create scripts/acdlHelper.js in your project, at the root of your AEM boilerplate:
your-project/└── scripts/ └── acdlHelper.js (create this file)This file centralizes context management and event publishing so you don’t have to duplicate ACDL logic across blocks.
/** * See: https://github.com/adobe/commerce-events/blob/main/packages/storefront-events-sdk/src/contexts.ts */export const contexts = { PRODUCT_CONTEXT: 'productContext', SHOPPING_CART_CONTEXT: 'shoppingCartContext',};
/** * See: https://github.com/adobe/commerce-events/blob/main/packages/storefront-events-sdk/src/events.ts */export const events = { PRODUCT_PAGE_VIEW: 'product-page-view', ADD_TO_CART: 'add-to-cart',};
export function getAdobeDataLayer() { window.adobeDataLayer = window.adobeDataLayer || []; return window.adobeDataLayer;}
/** * Sets a context in the Adobe Client Data Layer (ACDL). * Logic based on: https://github.com/adobe/commerce-events/blob/main/packages/storefront-events-sdk/src/Base.ts#L6 */function setContext(name, data) { const adobeDataLayer = getAdobeDataLayer();
// Clear existing context adobeDataLayer.push({ [name]: null, });
// Set new context adobeDataLayer.push({ [name]: data, });}
/** * Pushes an event to the Adobe Client Data Layer (ACDL). * Logic based on: https://github.com/adobe/commerce-events/blob/main/packages/storefront-events-sdk/src/Base.ts#L34 */function pushEvent(event, additionalContext) { const adobeDataLayer = getAdobeDataLayer();
adobeDataLayer.push((acdl) => { const state = acdl.getState ? acdl.getState() : {};
acdl.push({ event, eventInfo: { ...state, ...additionalContext, }, }); });}
/** * Sets the shopping cart context in the Adobe Client Data Layer (ACDL). * * @param {Array<{ * id: string, - Unique cart line item identifier * quantity: number, - Quantity in cart (min 1) * prices: { * price: { * value: number, - Numeric price value * currency?: string, - ISO 4217 currency code * } * }, * product: { * productId: number, - Numeric product ID * name: string, - Product display name * sku: string, - Product SKU * topLevelSku: string, - Parent/base product SKU * mainImageUrl?: string|null * }, * }>} items - Line items currently in the cart * * @param {{ * id?: string|null, - Cart identifier * totalQuantity?: number, - Total item quantity (derived from items if omitted) * prices?: { * subtotalExcludingTax?: { value: number, currency?: string }, * subtotalIncludingTax?: { value: number, currency?: string } * }, * possibleOnepageCheckout?: boolean, * giftMessageSelected?: boolean, * giftWrappingSelected?: boolean, * source?: string, - UI component or page that triggered the cart action * discountAmount?: number * }} [cartDetails={}] - Cart-level metadata */export function setShoppingCartContext(items, cartDetails = {}) { if (!Array.isArray(items)) { throw new TypeError('items must be an array'); }
for (const [index, item] of items.entries()) { const missingFields = ['id', 'prices', 'product', 'quantity'] .filter((field) => item[field] === undefined); if (missingFields.length) { throw new Error(`Cart item [${index}] is missing required fields: ${missingFields.join(', ')}`); } if (item.quantity < 1) { throw new Error(`Cart item [${index}]: quantity must be at least 1`); }
const missingProductFields = ['productId', 'name', 'sku'].filter((field) => item.product[field] === undefined); if (missingProductFields.length) { throw new Error(`Cart item [${index}].product is missing required fields: ${missingProductFields.join(', ')}`); } }
const totalQuantity = cartDetails.totalQuantity ?? items.reduce((sum, item) => sum + item.quantity, 0);
setContext(contexts.SHOPPING_CART_CONTEXT, { ...cartDetails, totalQuantity, items, });}
/** * Sets the product context in the Adobe Client Data Layer (ACDL). * * Schema: com.adobe.magento.entity/product/jsonschema/3-0-0 * * @param {{ * name: string, - Product display name (max 256 chars) * sku: string, - Variant SKU (1-256 chars) * topLevelSku: string, - Parent/base product SKU (1-256 chars) * productId?: number, - Numeric product ID * productType?: string|null, - Product type (e.g. 'simple', 'configurable') * categories?: string[], - Category names (max 256 chars each) * canonicalUrl?: string|null, - Canonical product URL (max 2083 chars) * mainImageUrl?: string|null, - Main product image URL (max 2083 chars) * pricing?: { * regularPrice: number, - Standard list price * currencyCode: string|null, - ISO 4217 currency code (max 3 chars) * minimalPrice?: number, - Lowest possible price * maximalPrice?: number, - Highest possible price * specialPrice?: number, - Promotional/sale price * tierPricing?: Array<{ * customerGroupId: number|null, * qty: number, * value: number * }> * } * }} product - Product data matching the ACDL product schema */export function setProductContext(product) { const requiredFields = ['name', 'sku', 'topLevelSku']; const missingFields = requiredFields.filter((field) => product[field] === undefined); if (missingFields.length) { throw new Error(`Product is missing required fields: ${missingFields.join(', ')}`); }
if (product.pricing !== undefined) { const requiredPricingFields = ['regularPrice', 'currencyCode']; const missingPricingFields = requiredPricingFields.filter((field) => product.pricing[field] === undefined); if (missingPricingFields.length) { throw new Error(`Product pricing is missing required fields: ${missingPricingFields.join(', ')}`); } }
setContext(contexts.PRODUCT_CONTEXT, product);}
// Triggered when a product is added to the cartexport function publishAddToCartEvent(items, cartDetails = {}) { setShoppingCartContext(items, cartDetails); pushEvent(events.ADD_TO_CART);}
// Triggered when a product page is viewedexport function publishProductPageViewEvent(product) { setProductContext(product); pushEvent(events.PRODUCT_PAGE_VIEW);}Step 2: Publish a product page view event
Section titled “Step 2: Publish a product page view event”Suggested file: scripts/initializers/pdp.js (the appropriate location depends on your project’s structure)
When you use the standard boilerplate, the PDP drop-in calls publishProductPageViewEvent as part of its initialization process. This ensures the event fires with a complete product context, only once the page is in a stable state.
If you’re not using the drop-in, call this function anywhere product data is available and the page view should be recorded — for example, after a successful product API response, after a client-side route change, or at the end of your own page initialization flow. The placement is up to you.
This example shows a custom PDP page publishing a page view event for each of several products using the helper function:
import { publishProductPageViewEvent } from '../acdlHelper.js';
// Sample products — replace with real product data from your API or drop-inconst products = [ { name: 'Classic Leather Sneaker', sku: 'SNK-001-WHT', topLevelSku: 'SNK-001-WHT', pricing: { regularPrice: 129.99, currencyCode: 'USD' }, }, { name: 'Running Shoe Pro', sku: 'RSP-042-BLK', topLevelSku: 'RSP-042-BLK', pricing: { regularPrice: 159.99, specialPrice: 119.99, currencyCode: 'USD' }, }, { name: 'Canvas High-Top', sku: 'CHT-007-NVY', topLevelSku: 'CHT-007-NVY', pricing: { regularPrice: 89.99, currencyCode: 'USD' }, }, { name: 'Slip-On Loafer', sku: 'SOL-019-TAN', topLevelSku: 'SOL-019-TAN', pricing: { regularPrice: 109.99, specialPrice: 79.99, currencyCode: 'USD' }, },];
products.forEach((product) => publishProductPageViewEvent(product));Step 3: Publish an add-to-cart event
Section titled “Step 3: Publish an add-to-cart event”Suggested file: blocks/product-details/product-details.js (the appropriate location depends on your project’s structure)
Call this function after a successful response from your add-to-cart API, passing the item being added to the cart:
import { publishAddToCartEvent } from '../../scripts/acdlHelper.js';
const sampleCartItem = { id: 'SNK-001-WHT-cart-1', quantity: 1, prices: { price: { value: 129.99, currency: 'USD' }, }, product: { productId: 1001, name: 'Classic Leather Sneaker', sku: 'SNK-001-WHT', topLevelSku: 'SNK-001-WHT', mainImageUrl: 'https://example.com/images/snk-001-wht.jpg', },};
// Called on button click — in production this fires after your add-to-cart API respondspublishAddToCartEvent([sampleCartItem]);Step 4: Test your implementation
Section titled “Step 4: Test your implementation”- Open your browser’s developer tools and check the Console tab.
- Confirm
window.adobeDataLayerexists and contains the contexts and events you pushed. - Trigger a product page view and an add-to-cart action, and verify each pushes the expected
productContext,shoppingCartContext, andevententries. - If AEP forwarding is configured, use the AEP Debugger Events view to confirm the events arrive — see Analytics instrumentation.
Implementation Checklist
Section titled “Implementation Checklist”When adding ACDL tracking to a new page or interaction:
- Copy
scripts/acdlHelper.jsinto your project’sscripts/directory. - Ensure
storefrontInstanceContextandpageContextare set before any publish call. - On product page load, call
publishProductPageViewEventwith the resolved product data. - On successful add-to-cart, call
publishAddToCartEventwith the full updated cart, not just the item that was added. - On cart update (quantity change, removal), call
setShoppingCartContextdirectly to keep the context in sync without firing an event. - On variant or option selection change, call
setProductContextso the context always reflects the currently selected variant.
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
Events push successfully but
eventInfois missing storefront or page dataConfirm
storefrontInstanceContextandpageContextare pushed to the ACDL before anypublish*call runs. See page-view in the Custom events reference for when these are normally seeded. -
setShoppingCartContextorsetProductContextthrows a missing field errorCheck the JSDoc in
scripts/acdlHelper.jsfor the required fields onitems,product, andpricing. Each function validates its input and throws before pushing an incomplete context. -
Events aren’t appearing in Adobe Experience Platform
Verify AEP forwarding is configured — see Adobe Experience Platform. Events still push to
window.adobeDataLayereven if AEP forwarding isn’t enabled, so check the browser console first before assuming the helper script is broken.
Getting Help
Section titled “Getting Help”If you encounter issues not covered here:
- Review the Custom events reference for the full list of events and required contexts
- Review Analytics instrumentation for ACDL validation and debugging steps
- Consult the Commerce Events GitHub repository for the underlying SDK source
Summary
Section titled “Summary”In this tutorial, you learned how to:
- Add a reusable
scripts/acdlHelper.jsfile that sets ACDL contexts and publishes events - Publish a
product-page-viewevent with a complete product context - Publish an
add-to-cartevent with the full updated cart - Validate your implementation using the browser console and the AEP Debugger
You now have a pattern for instrumenting commerce events on any custom block or page that bypasses the AEM Commerce drop-ins.