Instrument checkout analytics for a Luma storefront
This guide shows how to build a custom Adobe Commerce module that publishes the placeOrder event on the checkout success page for a Luma-based storefront. After you complete this guide, Adobe Commerce can receive the placeOrder event that Product Recommendations and Live Search use for revenue and conversion metrics.
Adobe Commerce empties the cart server-side before the success page renders, so cart line items are unavailable when the page loads. This module uses a two-part approach:
- A mixin snapshots cart data to
localStorageimmediately before checkout redirect. - A success-page component restores that data into SDK context, then publishes the event.
data-variant=warning
data-slots=text
Overview
You create a Vendor_CheckoutAnalytics module with these files:
registration.phpetc/module.xmlview/frontend/requirejs-config.jsview/frontend/layout/checkout_onepage_success.xmlview/frontend/templates/checkout-success.phtmlview/frontend/web/js/action/place-order-mixin.jsview/frontend/web/js/view/checkout-success.jsplaceOrderview/frontend/web/js/noopSdk.jsview/frontend/web/js/noopCollector.jsview/frontend/web/js/noopDs.jsPrerequisites
Before you begin, confirm the following:
- Adobe Commerce 2.4.x with a Luma-based or Luma-derived frontend (RequireJS and Asynchronous Module Definition (AMD))
- PHP 8.1+ and Adobe Commerce CLI access.
- Browser access to CDN resources on
cdn.jsdelivr.net. - Your
environmentIdandviewIdfrom your Adobe Commerce SaaS configuration. - The Storefront Events SDK and Event Collector are available from the CDN (this guide loads them via RequireJS).
Implementation steps
Follow these steps in order:
- Create the module structure
- Define the module
- Register the module
- Create fallback modules
- Create the place-order mixin
- Create the checkout success component
- Register RequireJS configuration
- Add the success page layout update
- Add the success page template
- Enable the module
- Configure the storefront instance context
- Confirm module files
- Verify the implementation
Create the module structure
Run the following commands from your Adobe Commerce root:
mkdir -p app/code/Vendor/CheckoutAnalytics/etc
mkdir -p app/code/Vendor/CheckoutAnalytics/view/frontend/layout
mkdir -p app/code/Vendor/CheckoutAnalytics/view/frontend/templates
mkdir -p app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/action
mkdir -p app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/view
Define the module
Create app/code/Vendor/CheckoutAnalytics/etc/module.xml. This declares the module and specifies a soft load-order dependency on Magento_Checkout, ensuring the mixin targets already-registered AMD modules.
<?xml version="1.0"?>
<!--
Vendor_CheckoutAnalytics module declaration.
Declares a soft dependency on Magento_Checkout so this module loads after it,
ensuring our mixins target already-registered AMD modules.
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_CheckoutAnalytics">
<sequence>
<module name="Magento_Checkout"/>
</sequence>
</module>
</config>
Register the module
Create app/code/Vendor/CheckoutAnalytics/registration.php at the module root. This registers the module with the Magento component registry.
<?php
/**
* Vendor_CheckoutAnalytics
*
* Registers the module with the Magento component registry.
*/
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_CheckoutAnalytics',
__DIR__
);
Create fallback modules
Create three empty AMD modules that serve as local fallbacks when the CDN is unreachable. RequireJS automatically falls back to these if any CDN URL in requirejs-config.js fails to load, ensuring the checkout flow is never blocked.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopSdk.js:
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/* eslint-disable */
define(function () { });
File: app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopCollector.js:
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/* eslint-disable */
define(function () { });
File: app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopDs.js
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/* eslint-disable */
define(function () { });
Create the place-order mixin
Create the place-order mixin. This mixin intercepts Magento_Checkout/js/action/place-order before the redirect so cart and storefront data can be snapshotted to localStorage. Magento clears the cart server-side before the success page loads, making this the only opportunity to capture that data.
For background on the mixin pattern, see JavaScript Mixins.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/action/place-order-mixin.js
/**
* place-order-mixin.js — Vendor_CheckoutAnalytics
*
* Intercepts the place-order action before the redirect so we can snapshot
* cart + storefront data to localStorage. Magento clears the cart server-side
* before the success page loads, so this is the only opportunity to capture it.
*/
define([
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function (wrapper, quote) {
'use strict';
var STORAGE_KEY = 'mse_checkout_cart_data';
function getImageUrl(item) {
if (item.thumbnail) { return item.thumbnail; }
if (item.product && item.product.thumbnail_url) { return item.product.thumbnail_url; }
return '';
}
function persistCartData() {
try {
var quoteItems = quote.getItems();
var totals = quote.getTotals()(); // KO observable — call it to get value
var cartId = quote.getQuoteId ? quote.getQuoteId() : '';
var cc = window.checkoutConfig || {};
var items = (quoteItems || []).map(function (item) {
return {
productSku: item.sku || '',
productName: item.name || '',
qty: item.qty || 1,
offerPrice: parseFloat(item.price) || 0,
currencyCode: (totals && totals.quote_currency_code) || '',
productImageUrl: getImageUrl(item)
};
});
localStorage.setItem(STORAGE_KEY, JSON.stringify({
cartId: cartId,
items: items,
grandTotal: totals ? parseFloat(totals.grand_total) || 0 : 0,
subTotal: totals ? parseFloat(totals.subtotal) || 0 : 0,
taxTotal: totals ? parseFloat(totals.tax_amount) || 0 : 0,
discountAmount: totals ? parseFloat(totals.discount_amount) || 0 : 0,
currencyCode: totals ? (totals.quote_currency_code || '') : '',
storefrontInstance: {
storeCode: cc.storeCode || '',
storeViewCode: cc.activeStore || cc.storeViewCode || '',
websiteCode: cc.websiteCode || '',
environmentId: cc.environmentId || '',
storeId: cc.storeId || null,
websiteId: cc.websiteId || null,
storeGroupId: cc.storeGroupId || null
}
}));
} catch (e) {
console.error('[CheckoutAnalytics] Failed to persist cart data:', e);
}
}
return function (originalAction) {
console.log('[CheckoutAnalytics] place-order mixin initialized');
return wrapper.wrap(originalAction, function (originalFn, paymentData, messageContainer) {
console.log('[CheckoutAnalytics] place-order mixin invoked, persisting cart data');
persistCartData();
return originalFn(paymentData, messageContainer);
});
};
});
Create the checkout success component
Create the success-page JavaScript component. This is initialized using the x-magento-init on the checkout success page. It restores the cart snapshot saved by the mixin, sets MSE contexts, and fires mse.publish.placeOrder(). The MSE SDK and Collector are declared as AMD dependencies so they are guaranteed to have executed before this callback runs.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/web/js/view/checkout-success.js
/**
* checkout-success.js — Vendor_CheckoutAnalytics
*
* Initialized via x-magento-init on the checkout_onepage_success page.
* Restores the cart snapshot saved by place-order-mixin.js, sets MSE contexts,
* and fires mse.publish.placeOrder().
*
* The SDK and Collector are loaded as AMD dependencies via requirejs-config.js paths.
* By the time this callback runs they have already executed and attached to
* window.magentoStorefrontEvents.
*/
define([
'magentoStorefrontEvents',
'magentoStorefrontEventCollector'
], function () {
'use strict';
var STORAGE_KEY = 'mse_checkout_cart_data';
function getPersistedCartData() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch (e) {
console.error('[CheckoutAnalytics] Could not parse persisted cart data:', e);
return null;
}
}
function getOrderId() {
var orderId = (window.checkoutConfig || {}).orderId;
if (orderId) {
return String(orderId);
}
// Fallback: Luma renders the increment ID inside .order-number > strong
var el = document.querySelector('.order-number strong');
return (el && el.textContent) ? el.textContent.trim() : '';
}
function publishPlaceOrder(mse, cartData) {
var cc = window.checkoutConfig || {};
var items = (cartData && cartData.items) || [];
mse.context.setStorefrontInstance({
environment: 'Testing',
environmentId: 'YOUR_ENV_ID',
baseCurrencyCode: 'USD',
storeViewCurrencyCode: 'USD',
viewId: 'YOUR_VIEW_ID',
storefrontTemplate: 'LUMA_BRIDGE',
storeUrl: ''
});
mse.context.setShoppingCart({
cartId: (cartData && cartData.cartId) || '',
items: items.map(function (item, ix) {
return {
id: String(ix),
product: {
name: item.productName || '',
sku: item.productSku || '',
mainImageUrl: item.productImageUrl || ''
},
quantity: item.qty || 1,
prices: { price: { value: item.offerPrice || 0 } }
};
}),
totalQuantity: items.length,
prices: {
subtotalExcludingTax: { value: cartData ? cartData.subTotal : 0 },
subtotalIncludingTax: { value: cartData ? cartData.subTotal + cartData.taxTotal : 0 },
}
});
mse.context.setOrder({
orderId: getOrderId(),
grandTotal: (cartData && cartData.grandTotal) || 0,
subTotal: (cartData && cartData.subTotal) || 0,
taxTotal: (cartData && cartData.taxTotal) || 0,
discountAmount: (cartData && cartData.discountAmount) || 0,
currencyCode: (cartData && cartData.currencyCode) || ''
});
mse.context.setPage({
pageType: 'checkout',
eventType: 'visibilityHidden',
maxXOffset: 0,
maxYOffset: 0,
minHeight: 0,
minWidth: 0,
referrerUrl: document.referrer || '',
ping: { pageInfos: [] }
});
var shopperId = 'guest';
if (cc.customerData && cc.customerData.id) {
shopperId = String(cc.customerData.id);
}
mse.context.setShopper({ shopperId: shopperId });
mse.publish.pageView();
mse.publish.placeOrder();
//Set purchaseHistory in localStorgage for use in recommendations requests
//Catalog view matches the viewId set in setStorefrontInstance above
const key = `CatalogView1:purchaseHistory`;
const purchasedProducts = shoppingCartContext.items.map((item) => item.product.sku);
const purchaseHistory = JSON.parse(window.localStorage.getItem(key) || '[]');
purchaseHistory.push({ date: new Date().toISOString(), items: purchasedProducts });
window.localStorage.setItem(key, JSON.stringify(purchaseHistory.slice(-20)));
localStorage.removeItem(STORAGE_KEY);
}
return function () {
console.log('[CheckoutAnalytics] checkout-success component initialized');
var cartData = getPersistedCartData();
if (!cartData) {
console.warn('[CheckoutAnalytics] No cart snapshot in localStorage — ' +
'placeOrder event will fire without item-level detail.');
}
var mse = window.magentoStorefrontEvents;
if (!mse) {
console.error('[CheckoutAnalytics] MSE SDK not available on window — cannot fire placeOrder event.');
return;
}
console.log('[CheckoutAnalytics] cartData:', cartData);
publishPlaceOrder(mse, cartData);
};
});
Register RequireJS configuration
Create the requirejs-config.js configuration file. This registers the place-order mixin and defines RequireJS paths for the MSE SDK, Collector, and data services base. Each path entry is an array — if the first CDN URL fails to load, RequireJS automatically falls back to the local noop module, so the checkout flow is never blocked.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/requirejs-config.js
/**
* RequireJS configuration for Vendor_CheckoutAnalytics.
*
* - place-order-mixin: snapshots cart to localStorage before the order redirect.
* - paths: loads the MSE SDK and Collector as AMD dependencies with CDN + noop fallbacks.
* The success-page component (checkout-success.js) declares these as dependencies so
* they are guaranteed to execute before its callback runs.
*/
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/place-order': {
'Vendor_CheckoutAnalytics/js/action/place-order-mixin': true
}
}
},
paths: {
magentoStorefrontEvents: [
'https://cdn.jsdelivr.net/npm/@adobe/magento-storefront-events-sdk@1/dist/index',
'Vendor_CheckoutAnalytics/js/noopSdk'
],
magentoStorefrontEventCollector: [
'https://cdn.jsdelivr.net/npm/@adobe/magento-storefront-event-collector@1/dist/index',
'Vendor_CheckoutAnalytics/js/noopCollector'
],
dataServicesBase: [
'https://acds-events.adobe.io/v7/ds.min',
'Magento_DataServices/js/noopDs'
]
}
};
Add the success page layout update
Create the layout XML file that injects the analytics block into the checkout success page. The checkout_onepage_success handle is dispatched by Magento_Checkout only on that page, ensuring this update is scoped correctly.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/layout/checkout_onepage_success.xml
<?xml version="1.0"?>
<!--
Injects our analytics block into the checkout success page.
The handle `checkout_onepage_success` is dispatched by
Magento_Checkout/Controller/Onepage/Success::execute() before rendering,
so this layout update is guaranteed to run only on that page.
We append a block inside `checkout.success` (the main success container)
so it renders after the order confirmation content. The block is
display-less — it only outputs a <script> initialisation tag.
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Framework\View\Element\Template"
name="vendor.checkout_analytics.success"
template="Vendor_CheckoutAnalytics::checkout-success.phtml"
after="-"/>
</referenceContainer>
</body>
</page>
Add the success page template
Create the .phtml template. This file has no visible HTML — its sole purpose is to bootstrap the JavaScript component via the x-magento-init pattern so RequireJS loads and runs it after the page DOM is ready. The * selector initializes the component against the document body.
File: app/code/Vendor/CheckoutAnalytics/view/frontend/templates/checkout-success.phtml
<?php
/**
* Vendor_CheckoutAnalytics — checkout success analytics initialisation.
*
* This template is injected on the checkout_onepage_success layout handle.
* It contains no visible HTML — its sole purpose is to bootstrap our JS
* component via the x-magento-init pattern so RequireJS loads and runs it
* after the page DOM is ready.
*
* The `*` selector tells Magento to initialise the component against the
* document body (no specific DOM element required).
*
* @var \Magento\Framework\View\Element\Template $block
*/
?>
<script type="text/x-magento-init">
{
"*": {
"Vendor_CheckoutAnalytics/js/view/checkout-success": {}
}
}
</script>
Enable the module
Run the following commands from your Adobe Commerce root:
bin/magento module:enable Vendor_CheckoutAnalytics
Run setup upgrade to register the module in app/etc/config.php and run any required schema or data upgrades:
bin/magento setup:upgrade
Compile dependency injection:
bin/magento setup:di:compile
data-variant=note
data-slots=text
registration.php, so this step completes quickly. It is still required to rebuild the global DI map so Magento picks up the new module correctly.Deploy static content (the -f flag forces deployment in any application mode):
bin/magento setup:static-content:deploy -f
For production mode, specify your locale explicitly:
bin/magento setup:static-content:deploy en_US -f
Flush the cache:
bin/magento cache:flush
Configure the storefront instance context
Open view/frontend/web/js/view/checkout-success.js and update the setStorefrontInstance call with your environment values:
mse.context.setStorefrontInstance({
environment: 'Production', // 'Testing' or 'Production'
environmentId: 'YOUR_ENV_ID', // Tenant ID associated with ACO/EDS storefront
baseCurrencyCode: 'USD',
storeViewCurrencyCode: 'USD',
viewId: 'YOUR_VIEW_ID', // Catalog View ID associated with ACO/EDS storefront
storefrontTemplate: 'LUMA_BRIDGE',
storeUrl: 'https://your-store.example.com'
});
data-variant=important
data-slots=text
environmentId value and viewId values need to align with the configuration of your EDS storefront. Using a placeholder value causes events to be routed to the wrong data stream or dropped entirely.After editing the file, redeploy static content and flush the cache:
bin/magento setup:static-content:deploy -f && bin/magento cache:flush
Confirm module files
After you complete the steps above, confirm that your module directory contains the following files:
app/code/Vendor/CheckoutAnalytics/registration.phpapp/code/Vendor/CheckoutAnalytics/etc/module.xmlapp/code/Vendor/CheckoutAnalytics/view/frontend/requirejs-config.jsapp/code/Vendor/CheckoutAnalytics/view/frontend/layout/checkout_onepage_success.xmlapp/code/Vendor/CheckoutAnalytics/view/frontend/templates/checkout-success.phtmlapp/code/Vendor/CheckoutAnalytics/view/frontend/web/js/action/place-order-mixin.jsapp/code/Vendor/CheckoutAnalytics/view/frontend/web/js/view/checkout-success.jsapp/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopSdk.jsapp/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopCollector.jsapp/code/Vendor/CheckoutAnalytics/view/frontend/web/js/noopDs.js
Verify the implementation
Cart snapshot on the checkout page
Confirm the mixin is capturing cart data before the redirect:
- Open your store in a browser with the DevTools console open.
- Add one or more products to the cart and proceed to checkout.
- In the console, confirm that the mixin initialized:
[CheckoutAnalytics] place-order mixin initialized. - Place the order. Before the page redirects, confirm:
[CheckoutAnalytics] place-order mixin invoked, persisting cart data. - In DevTools, open Application > Local Storage and confirm that a key named
mse_checkout_cart_dataexists with a JSON payload containing cart items, totals, and storefront instance fields.
Event published on the success page
Confirm the placeOrder event fires correctly after the redirect:
- After the redirect to the success page, open the console and confirm the following log messages appear:
[CheckoutAnalytics] checkout-success component initialized
[CheckoutAnalytics] cartData: { cartId: "...", items: [...], ... }
[CheckoutAnalytics] Publishing placeOrder event
- Confirm that the
mse_checkout_cart_datakey has been removed from Local Storage after the event fires. - To confirm that the event was received by the MSE SDK, run the following code in the console before placing a test order:
window.magentoStorefrontEvents.subscribe.placeOrder(function (event) {
console.log('placeOrder event received:', event);
});
data-variant=note
data-slots=text
window.magentoStorefrontEvents is undefined on the success page, the CDN scripts failed to load. Check the Network tab in DevTools for failed requests to cdn.jsdelivr.net. The noop fallback modules prevent JavaScript errors but do not set window.magentoStorefrontEvents, so no events fire.Troubleshooting
The place-order mixin is not running
Confirm that the mixin is registered by checking the compiled RequireJS config in the browser:
require.s.contexts._.config.config.mixins
// Should contain an entry for 'Magento_Checkout/js/action/place-order'
If the entry is missing, re-run static content deployment and flush the cache.
mse_checkout_cart_data is empty or missing on the success page
Missing or empty results mean that persistCartData() ran but the quote returned no items. This issue can happen if the active payment method bypasses the standard place-order action. Check whether your payment method uses a custom place-order action — if so, add a second mixin targeting that module.
The placeOrder event fires but contexts are missing or incorrect
Inspect the context state directly in the console:
var mse = window.magentoStorefrontEvents;
console.log(mse.context.getShoppingCart());
console.log(mse.context.getOrder());
console.log(mse.context.getStorefrontInstance());
Cross-reference each field against the MSE context reference.
Static content is not updating after editing JS files
In developer mode, RequireJS serves files directly from app/code so edits are reflected immediately. In production mode, redeploy static content and flush the cache after every JS change:
bin/magento setup:static-content:deploy -f && bin/magento cache:flush