Shopify customer event pixel
Track revenue, checkout steps and product-level goals in Shopify with a custom pixel.
With a custom pixel (Shopify Customer events), ABlyft collects Shopify's client-side events through the Web Pixel API. This also lets you follow the steps of the checkout, not only the purchase. See Shopify's documentation on customer events and pixels for background.
Customer events can be blocked and are less reliable than the webhook. See the comparison before you combine both.
Set it up
- In Shopify, open Settings → Customer events.
- Choose Add custom pixel and give it a name, for example
ABlyft Tracking. - Paste the code below and change the
projectIdto the ID of your ABlyft project. This is required. - Adjust the
eventNameof each goal in the code if it differs from the API names in ABlyft. - Add the pixel, save it and connect it.
To track purchases and similar events, check the Customer privacy settings of the pixel in Shopify. Events are only sent if the setting allows it for the visitor.
Code to copy
Settings at the top of the code:
| Constant | Meaning |
|---|---|
projectId | The ID of your ABlyft project. Replace the placeholder. |
trackingCurrency | The currency in which revenue is tracked. Default "EUR". Orders in another currency are converted. |
maxOrderValueFallback | false by default. If you enter a number, for example 250.50, a revenue above it is capped to that value. |
/*
IMPORTANT: Edit the Project ID
*/
const projectId = 123456789; // ← Your project id
//Further options (do not change if not necessary)
const trackingCurrency = "EUR"; // ← currency you want to track in
const maxOrderValueFallback = false; // ← if a number (i.e., 250.50) is set, a revenue is capped to that
analytics.subscribe("all_events", async ({ name, data, context }) => {
if (!(name.startsWith("checkout_") || name === "product_added_to_cart")) {
return;
}
console.log("ABlyft - Shopify pixel event received:", name);
const snippetVersion = 2.30;
const d = document;
const w = window;
w.insideShopifyPixel = true;
w.ablyftAllowIframe = true;
w.shopifyEventContext = context;
let j = d.createElement("script");
j.async = true;
j.src = `https://cdn.ablyft.com/s/${projectId}.js`;
j.onload = async () => {
const url = context.window.location.href;
const { checkout } = data;
let events = [{ eventType:"custom", eventName:`shopify_event_${name}`}];
//currency conversion
try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("ABlyft - Timeout loading currency script"));
}, 1000); // 1 sec timeout
const fxScript = document.createElement("script");
fxScript.src = "https://cdn.shopify.com/s/javascripts/currencies.js";
fxScript.onload = () => {
clearTimeout(timeout);
if (typeof Currency?.convert === "function") {
resolve();
} else {
reject(new Error("ABlyft - Currency.convert impossible"));
}
};
fxScript.onerror = () => {
clearTimeout(timeout);
reject(new Error("ABlyft - Currency script could not be loaded"));
};
document.head.appendChild(fxScript);
});
} catch (error) {
console.warn("ABlyft - Currency conversion not available:", error);
}
//event handling
if(name === "checkout_completed"){
const { amount: rawAmount, currencyCode: sourceCurrency } = checkout.totalPrice;
const count = checkout.lineItems.reduce((sum, {quantity}) => sum + quantity, 0);
let revenue = rawAmount;
let conversionFactor = 1;
if(sourceCurrency !== trackingCurrency) {
try {
if (typeof Currency?.convert === "function") {
revenue = Currency.convert(rawAmount, sourceCurrency, trackingCurrency);
conversionFactor = revenue / rawAmount;
console.log(`ABlyft - Currency conversion successful: from ${sourceCurrency} ${rawAmount} to ${trackingCurrency} ${parseFloat(revenue.toFixed(2))}`);
} else {
throw new Error("ABlyft - Currency.convert not available");
}
} catch (e) {
console.warn("ABlyft - Currency conversion failed – using rawAmount", e);
revenue = rawAmount;
}
}
if(typeof maxOrderValueFallback === "number"){
if(revenue > maxOrderValueFallback) revenue = maxOrderValueFallback;
}
//use your own eventNames here
events = events.concat([
{ eventType:"revenue", eventName:"revenue", eventValue: parseFloat(revenue.toFixed(2)) },
{ eventType:"custom", eventName:"products-count", eventValue: count },
{ eventType:"custom", eventName:"purchase" }
]);
//custom product-related goals
let lineItemsMap = {};
checkout.lineItems.forEach(function(item) {
const productId = item.variant?.product?.id || item.id;
const amount =
item.finalLinePrice?.amount ??
item.totalPrice?.amount ??
item.linePrice?.amount ??
item.discountedTotalPrice?.amount ??
item.originalTotalPrice?.amount ??
0;
const revenue = Number(amount) || 0;
if (!lineItemsMap[productId]) {
lineItemsMap[productId] = { productId, revenue: 0 };
}
lineItemsMap[productId].revenue += revenue;
});
let lineItemsArray = Object.values(lineItemsMap);
lineItemsArray.forEach(function(item) {
events.push({ eventType: "revenue", eventName: "revenue-product-" + item.productId, eventValue: parseFloat((item.revenue*conversionFactor).toFixed(2)) });
events.push({ eventType: "custom", eventName: "purchase-product-" + item.productId });
});
}
console.log("ABlyft - Events to send:", events);
let storageType = w.ablyft.get("data").storage;
if(storageType === "default") storageType = "cookie";
async function getStorageItem(key) {
if(storageType === "cookie"){
return await browser.cookie.get(key);
}
return await browser[storageType].getItem(key);
}
async function setStorageItem(key, value) {
if(storageType === "cookie"){
return await browser.cookie.set(key, value);
}
return await browser[storageType].setItem(key, value);
}
let userData = await getStorageItem("ablyft_uvs");
const user = JSON.parse(userData);
let visitor_type = "new";
if (user && user.sessions && user.sessions > 1) {
visitor_type = "returning";
}
const attributes = {
device_type: w.ablyft.get("deviceType"),
visitor_type,
url,
user_agent: w.navigator.userAgent
};
let experimentsBucketData = await getStorageItem("ablyft_exps");
if(!experimentsBucketData){
console.log("ABlyft - Empty user bucketing - aborting");
return;
}
const experimentsBucket = JSON.parse(experimentsBucketData);
const experimentIds = Object.keys(experimentsBucket);
let variationIds = [];
for (let i = 0; i < experimentIds.length; i++) { variationIds.push(experimentsBucket[experimentIds[i]]); }
function createLockId() {
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let text = "";
for (let i = 0; i < 10; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); }
return text;
}
const goals = w.ablyft.get("goals");
const tgoalsData = await getStorageItem("ablyft_tgoals");
const triggeredGoals = tgoalsData ? JSON.parse(tgoalsData) : {};
const postObj = {
projectId: projectId,
source: "shopify-webpixel",
snippetVersion,
user: {
attributes,
experimentsBucket
},
events: events.filter(event => {
const goalObject = goals.find(goal => goal.api_name === event.eventName);
if(!goalObject) return false;
return !(goalObject.counting_method === "one" && triggeredGoals[goalObject.id]);
}).map(event => {
const goalObject = goals.find(goal => goal.api_name === event.eventName);
return {
type: event.eventType,
value: event.eventValue,
timestamp: Math.floor(Date.now() / 1000),
locked: Math.floor(Date.now() / 1000),
lockId: createLockId(),
goal: {
id: goalObject.id,
variations: variationIds
}
};
})
}
if(postObj.events.length === 0) return;
const sent = await sendWithRetry(postObj);
if(sent){
postObj.events.forEach(event => {
const goalId = event.goal.id;
const existing = triggeredGoals[goalId] || [];
triggeredGoals[goalId] = Array.from(new Set([...existing, ...variationIds]));
});
await setStorageItem("ablyft_tgoals", JSON.stringify(triggeredGoals));
}
async function sendWithRetry(data, maxRetries = 3) {
const payload = JSON.stringify(data);
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch("https://log.ablyft.com", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: payload
});
if (response.ok) {
console.log(`ABlyft - Request successful on attempt ${attempt}`);
return true;
} else if (response.status >= 400 && response.status < 500) {
console.warn(`ABlyft - Client error (${response.status}) – no retry`);
return false;
} else {
console.warn(`ABlyft - Server error (${response.status}) – retrying...`);
}
} catch (error) {
console.warn(`ABlyft - Network error on attempt ${attempt}:`, error);
}
if (attempt < maxRetries) {
await new Promise((res) => setTimeout(res, 500)); //delay in ms
}
}
console.error("ABlyft - All retry attempts failed.");
return false;
}
};
d.head.appendChild(j);
});What the pixel sends
- It reacts to the events
product_added_to_cartand allcheckout_*events, loads your ABlyft snippet and sends the events to ABlyft. It only sends events for goals that exist in your project (matched by API name). - On
checkout_completedit sends the goalsrevenue,products-countandpurchase, and the product-level goals described below. - The visitor must already be assigned to experiments (the pixel reads the assignment from the ABlyft storage). If there is none, nothing is sent.
- A failed request is retried up to three times (not for client errors).
Other events
Create a Custom goal and use one of these names as its API name:
| API name | Fires when |
|---|---|
shopify_event_product_added_to_cart | A product is added to the cart |
shopify_event_checkout_started | The checkout is started |
shopify_event_checkout_contact_info_submitted | Contact information is submitted |
shopify_event_checkout_address_info_submitted | The address is submitted |
shopify_event_checkout_shipping_info_submitted | Shipping information is submitted |
shopify_event_checkout_completed | The checkout is completed (alternatively use purchase or revenue) |
Product-level events
For measurement per product, the pixel sends two additional events for every product in a completed order:
| API name | Meaning |
|---|---|
revenue-product-[productId] | Revenue of this product |
purchase-product-[productId] | Purchases containing this product |
Create a goal and replace the placeholder with the Shopify product ID, for example revenue-product-1234567890.
The values are summed per product, not per variant: the revenues of all variants are collected under one product ID.