Web Pixel Extensions
Web pixel extensions are Shopify's privacy-first approach to customer event tracking and analytics. Instead of injecting arbitrary JavaScript into the storefront (which risks performance degradation and privacy violations), web pixels run in a sandboxed environment with controlled access to customer data. They receive standardized e-commerce events and can forward data to analytics platforms while respecting customer consent preferences.
Traditional tracking scripts (Google Tag Manager, Facebook Pixel, etc.) run with full page access, can slow down storefronts, and often collect data without proper consent. Shopify's web pixel framework addresses all three issues: sandboxed execution for performance, standardized events for consistency, and built-in consent management for privacy compliance.
How Web Pixels Work
Web pixels run inside a sandboxed Web Worker. They cannot access the DOM, read cookies, or interact with the page directly. Instead, they subscribe to a stream of standardized customer events that Shopify emits as customers browse, add items to cart, and complete purchases.
Customer Events
Shopify emits a comprehensive set of standardized e-commerce events. Your pixel subscribes to the events it needs:
| Event | Trigger | Key Data |
|---|---|---|
page_viewed | Customer views any page | URL, page title, referrer |
product_viewed | Customer views a product page | Product ID, title, price, variant |
collection_viewed | Customer views a collection | Collection ID, title |
search_submitted | Customer performs a search | Search query, result count |
product_added_to_cart | Item added to cart | Product, variant, quantity, cart total |
product_removed_from_cart | Item removed from cart | Product, variant, quantity |
cart_viewed | Customer views the cart page | Cart contents, total |
checkout_started | Customer begins checkout | Cart contents, total, discount codes |
checkout_address_info_submitted | Address form completed | Country, province (no PII) |
checkout_shipping_info_submitted | Shipping method selected | Shipping method, cost |
payment_info_submitted | Payment details entered | Payment method type (no card details) |
checkout_completed | Purchase completed | Order ID, total, line items, currency |
Customer events contain anonymized data by default. Personally identifiable information (PII) like email addresses and full names is only included when the customer has granted explicit consent. Your pixel must check event.data.customer.consent before accessing PII fields.
Creating a Web Pixel Extension
# Generate a web pixel extension
shopify app generate extension --template web_pixel --name custom-analytics
extensions/custom-analytics/
├── src/
│ └── index.ts
├── shopify.extension.toml
└── package.json
Extension Configuration
# extensions/custom-analytics/shopify.extension.toml
api_version = "2025-01"
[[extensions]]
type = "web_pixel_extension"
name = "Custom Analytics Pixel"
handle = "custom-analytics"
[extensions.settings]
[[extensions.settings.fields]]
key = "analytics_endpoint"
type = "single_line_text_field"
name = "Analytics Endpoint URL"
description = "The URL where event data will be sent"
[[extensions.settings.fields]]
key = "api_key"
type = "single_line_text_field"
name = "API Key"
description = "Your analytics platform API key"
[[extensions.settings.fields]]
key = "track_search"
type = "boolean"
name = "Track Search Events"
description = "Enable tracking of search queries"
Custom Pixel Implementation
The pixel entry point subscribes to events and processes them. Here is a full implementation that tracks key e-commerce events:
// extensions/custom-analytics/src/index.ts
import { register } from '@shopify/web-pixels-extension';
register(({ analytics, browser, settings, init }) => {
const endpoint = settings.analytics_endpoint;
const apiKey = settings.api_key;
const trackSearch = settings.track_search === 'true';
// Helper to send events to your analytics backend
async function sendEvent(eventName: string, payload: Record<string, any>) {
try {
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
},
body: JSON.stringify({
event: eventName,
timestamp: new Date().toISOString(),
shopId: init.data.shop.id,
payload,
}),
// Use keepalive to ensure the request completes
// even if the customer navigates away
keepalive: true,
});
} catch (err) {
// Silently fail -- do not disrupt the customer experience
console.error(`Failed to send ${eventName} event:`, err);
}
}
// Track page views
analytics.subscribe('page_viewed', (event) => {
sendEvent('page_view', {
url: event.data.url,
title: event.data.pageTitle,
referrer: event.data.referrer,
});
});
// Track product views
analytics.subscribe('product_viewed', (event) => {
const product = event.data.productVariant;
sendEvent('product_view', {
productId: product?.product?.id,
productTitle: product?.product?.title,
variantId: product?.id,
variantTitle: product?.title,
price: product?.price?.amount,
currency: product?.price?.currencyCode,
});
});
// Track add to cart
analytics.subscribe('product_added_to_cart', (event) => {
const cartLine = event.data.cartLine;
sendEvent('add_to_cart', {
productId: cartLine?.merchandise?.product?.id,
productTitle: cartLine?.merchandise?.product?.title,
variantId: cartLine?.merchandise?.id,
quantity: cartLine?.quantity,
price: cartLine?.merchandise?.price?.amount,
currency: cartLine?.merchandise?.price?.currencyCode,
});
});
// Track cart removals
analytics.subscribe('product_removed_from_cart', (event) => {
const cartLine = event.data.cartLine;
sendEvent('remove_from_cart', {
productId: cartLine?.merchandise?.product?.id,
variantId: cartLine?.merchandise?.id,
quantity: cartLine?.quantity,
});
});
// Track checkout started
analytics.subscribe('checkout_started', (event) => {
const checkout = event.data.checkout;
sendEvent('begin_checkout', {
totalPrice: checkout?.totalPrice?.amount,
currency: checkout?.totalPrice?.currencyCode,
lineItemCount: checkout?.lineItems?.length,
discountCodes: checkout?.discountApplications?.map(
(d: any) => d.title
),
});
});
// Track purchase completed
analytics.subscribe('checkout_completed', (event) => {
const checkout = event.data.checkout;
sendEvent('purchase', {
orderId: checkout?.order?.id,
totalPrice: checkout?.totalPrice?.amount,
subtotalPrice: checkout?.subtotalPrice?.amount,
totalTax: checkout?.totalTax?.amount,
currency: checkout?.totalPrice?.currencyCode,
lineItems: checkout?.lineItems?.map((item: any) => ({
productId: item.variant?.product?.id,
productTitle: item.variant?.product?.title,
variantId: item.variant?.id,
quantity: item.quantity,
price: item.variant?.price?.amount,
})),
shippingRate: {
price: checkout?.shippingLine?.price?.amount,
},
});
});
// Conditionally track search
if (trackSearch) {
analytics.subscribe('search_submitted', (event) => {
sendEvent('search', {
query: event.data.searchResult?.query,
resultCount: event.data.searchResult?.productVariants?.length ?? 0,
});
});
}
// Track collection views
analytics.subscribe('collection_viewed', (event) => {
sendEvent('collection_view', {
collectionId: event.data.collection?.id,
collectionTitle: event.data.collection?.title,
});
});
});
Privacy-Compliant Analytics
Shopify's web pixel framework integrates with the customer privacy consent system. Your pixel must respect consent status.
Consent-Aware Event Handling
// Enhanced pixel with consent checking
register(({ analytics, browser, settings, init }) => {
const endpoint = settings.analytics_endpoint;
// Check initial consent status
let analyticsConsent = init.customerPrivacy?.analyticsProcessingAllowed ?? false;
let marketingConsent = init.customerPrivacy?.marketingAllowed ?? false;
// Listen for consent changes (customer updates preferences)
analytics.subscribe('visitor_consent_collected', (event) => {
analyticsConsent = event.data.analyticsAllowed;
marketingConsent = event.data.marketingAllowed;
});
async function sendEvent(
eventName: string,
payload: Record<string, any>,
requiresMarketingConsent: boolean = false
) {
// Always check consent before sending data
if (!analyticsConsent) return;
if (requiresMarketingConsent && !marketingConsent) return;
await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: eventName,
timestamp: new Date().toISOString(),
consent: {
analytics: analyticsConsent,
marketing: marketingConsent,
},
payload,
}),
keepalive: true,
});
}
// Basic analytics events (require analytics consent)
analytics.subscribe('page_viewed', (event) => {
sendEvent('page_view', {
url: event.data.url,
title: event.data.pageTitle,
});
});
// Marketing-related events (require marketing consent)
analytics.subscribe('checkout_completed', (event) => {
// Send conversion data to ad platforms only with marketing consent
sendEvent(
'conversion',
{
orderId: event.data.checkout?.order?.id,
value: event.data.checkout?.totalPrice?.amount,
currency: event.data.checkout?.totalPrice?.currencyCode,
},
true // requires marketing consent
);
});
});
Your web pixel must comply with GDPR, CCPA, and other privacy regulations. Key requirements:
- Do not collect PII without consent -- even if the event data includes it
- Honor opt-out requests -- when a customer withdraws consent, stop sending data immediately
- Data minimization -- only collect what you need. Do not forward raw event payloads to third parties
- Retention limits -- ensure your analytics backend deletes data per your privacy policy timeline
Violations can result in your app being removed from the Shopify App Store and potential legal liability.
Integration with Third-Party Analytics
Google Analytics 4
// GA4 integration via Measurement Protocol
register(({ analytics, settings }) => {
const measurementId = settings.ga4_measurement_id;
const apiSecret = settings.ga4_api_secret;
const GA4_ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`;
function sendToGA4(eventName: string, params: Record<string, any>) {
fetch(GA4_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
client_id: generateClientId(), // Generate a privacy-safe client ID
events: [{ name: eventName, params }],
}),
keepalive: true,
});
}
analytics.subscribe('product_viewed', (event) => {
const product = event.data.productVariant;
sendToGA4('view_item', {
currency: product?.price?.currencyCode,
value: parseFloat(product?.price?.amount || '0'),
items: [{
item_id: product?.product?.id,
item_name: product?.product?.title,
item_variant: product?.title,
price: parseFloat(product?.price?.amount || '0'),
}],
});
});
analytics.subscribe('product_added_to_cart', (event) => {
const cartLine = event.data.cartLine;
sendToGA4('add_to_cart', {
currency: cartLine?.merchandise?.price?.currencyCode,
value: parseFloat(cartLine?.merchandise?.price?.amount || '0') * (cartLine?.quantity || 1),
items: [{
item_id: cartLine?.merchandise?.product?.id,
item_name: cartLine?.merchandise?.product?.title,
quantity: cartLine?.quantity,
price: parseFloat(cartLine?.merchandise?.price?.amount || '0'),
}],
});
});
analytics.subscribe('checkout_completed', (event) => {
const checkout = event.data.checkout;
sendToGA4('purchase', {
transaction_id: checkout?.order?.id,
currency: checkout?.totalPrice?.currencyCode,
value: parseFloat(checkout?.totalPrice?.amount || '0'),
tax: parseFloat(checkout?.totalTax?.amount || '0'),
shipping: parseFloat(checkout?.shippingLine?.price?.amount || '0'),
items: checkout?.lineItems?.map((item: any) => ({
item_id: item.variant?.product?.id,
item_name: item.variant?.product?.title,
quantity: item.quantity,
price: parseFloat(item.variant?.price?.amount || '0'),
})),
});
});
});
function generateClientId(): string {
// Generate a random client ID that does not contain PII
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
(c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
);
}
Meta Conversions API
// Meta (Facebook) Conversions API integration
register(({ analytics, settings }) => {
const pixelId = settings.meta_pixel_id;
const accessToken = settings.meta_access_token;
const META_ENDPOINT = `https://graph.facebook.com/v18.0/${pixelId}/events`;
function sendToMeta(eventName: string, customData: Record<string, any>) {
fetch(META_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [{
event_name: eventName,
event_time: Math.floor(Date.now() / 1000),
action_source: 'website',
custom_data: customData,
}],
access_token: accessToken,
}),
keepalive: true,
});
}
analytics.subscribe('product_viewed', (event) => {
const product = event.data.productVariant;
sendToMeta('ViewContent', {
content_ids: [product?.product?.id],
content_type: 'product',
content_name: product?.product?.title,
value: parseFloat(product?.price?.amount || '0'),
currency: product?.price?.currencyCode,
});
});
analytics.subscribe('product_added_to_cart', (event) => {
const cartLine = event.data.cartLine;
sendToMeta('AddToCart', {
content_ids: [cartLine?.merchandise?.product?.id],
content_type: 'product',
value: parseFloat(cartLine?.merchandise?.price?.amount || '0'),
currency: cartLine?.merchandise?.price?.currencyCode,
});
});
analytics.subscribe('checkout_completed', (event) => {
const checkout = event.data.checkout;
sendToMeta('Purchase', {
content_ids: checkout?.lineItems?.map(
(item: any) => item.variant?.product?.id
),
content_type: 'product',
value: parseFloat(checkout?.totalPrice?.amount || '0'),
currency: checkout?.totalPrice?.currencyCode,
num_items: checkout?.lineItems?.length,
});
});
});
Testing and Debugging
# Start development server with pixel debugging
shopify app dev
# The CLI provides a preview URL with pixel debugging enabled
# Open the browser console to see pixel event logs
Debug Mode
register(({ analytics, settings, init }) => {
const debug = init.data.shop.myshopifyDomain.includes('dev');
function log(eventName: string, data: any) {
if (debug) {
console.log(`[Pixel Debug] ${eventName}:`, JSON.stringify(data, null, 2));
}
}
analytics.subscribe('page_viewed', (event) => {
log('page_viewed', event.data);
// ... normal processing
});
});
Use the browser's Network tab to verify that your pixel is sending requests to the correct endpoints. Shopify's dev tools also provide a "Customer Events" debugger that shows every event emitted on the page, along with which pixels received it. Access it via the ?debug_pixels=true query parameter on your dev store.
Performance Considerations
- Use
keepalive: trueon all fetch requests -- this ensures events are sent even if the customer navigates away mid-request - Batch events when possible -- instead of sending one request per event, buffer events and send them in batches every few seconds
- Minimize payload size -- only include the data your analytics platform needs
- Avoid synchronous patterns -- everything in the pixel should be non-blocking
- Handle network failures silently -- never let a tracking failure affect the customer experience
// Event batching implementation
class EventBatcher {
private queue: any[] = [];
private timer: ReturnType<typeof setTimeout> | null = null;
private readonly endpoint: string;
private readonly batchSize: number;
private readonly flushInterval: number;
constructor(endpoint: string, batchSize = 10, flushInterval = 3000) {
this.endpoint = endpoint;
this.batchSize = batchSize;
this.flushInterval = flushInterval;
}
add(event: any) {
this.queue.push(event);
if (this.queue.length >= this.batchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.flushInterval);
}
}
private flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: batch }),
keepalive: true,
}).catch(() => {
// Re-queue failed events (with a retry limit)
// In production, add exponential backoff
});
}
}
Web pixel extensions provide a secure, performant, and privacy-compliant way to track customer behavior on Shopify storefronts. By using Shopify's standardized event system, you get consistent data across all merchants without the fragility of DOM scraping or cookie tracking. Combined with consent management, your analytics integrations are ready for the privacy-first web.