Skip to main content

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.

Why Web Pixels?

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:

EventTriggerKey Data
page_viewedCustomer views any pageURL, page title, referrer
product_viewedCustomer views a product pageProduct ID, title, price, variant
collection_viewedCustomer views a collectionCollection ID, title
search_submittedCustomer performs a searchSearch query, result count
product_added_to_cartItem added to cartProduct, variant, quantity, cart total
product_removed_from_cartItem removed from cartProduct, variant, quantity
cart_viewedCustomer views the cart pageCart contents, total
checkout_startedCustomer begins checkoutCart contents, total, discount codes
checkout_address_info_submittedAddress form completedCountry, province (no PII)
checkout_shipping_info_submittedShipping method selectedShipping method, cost
payment_info_submittedPayment details enteredPayment method type (no card details)
checkout_completedPurchase completedOrder ID, total, line items, currency
Event Data Privacy

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.

// 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
);
});
});
GDPR and Privacy Regulations

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
});
});
Testing Pixels in Development

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

  1. Use keepalive: true on all fetch requests -- this ensures events are sent even if the customer navigates away mid-request
  2. Batch events when possible -- instead of sending one request per event, buffer events and send them in batches every few seconds
  3. Minimize payload size -- only include the data your analytics platform needs
  4. Avoid synchronous patterns -- everything in the pixel should be non-blocking
  5. 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
});
}
}
Summary

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.