Developing a Custom Web Pixel for the Convert Shopify App
Market-Specific Goal Tracking for Shopify Checkout Events with Convert Attribution
| Owner: | George F. Crewe |
IN THIS ARTICLE YOU WILL
Overview
This guide explains how to create a Shopify Custom Web Pixel that enables market-specific goal tracking while maintaining full experiment and variation attribution when using the Convert Shopify Experience App.
Custom Web Pixels are necessary when you need:
- Separate goals for different markets (e.g., "Begin Checkout Canada" vs. "USA")
- Custom conversion events not covered by the app's built-in tracking
- Advanced attribution logic that combines multiple data sources
Understanding the Technical Challenge
The Problem
The Convert Shopify App operates as an "App Embed" within Shopify's ecosystem, which creates a secure sandbox environment for Shopify Web Pixels. This sandbox cannot directly access:
- Standard browser cookies set on the main page
- The convert JavaScript object from the main Convert tracking script
- Variables from other scripts or domains
The Solution
The Convert Shopify App bridges this gap by storing all essential experiment metadata in Shopify Cart Attributes, specifically a base64-encoded attribute called __data. Your Custom Web Pixel can read this data and send conversions to Convert.com with full attribution.
Where Experiment Metadata Is Stored
When a visitor participates in a Convert experiment, the Shopify App stores their experiment context in cart attributes that persist through checkout.
The __data Attribute (Primary Source)
Location: event.data.checkout.attributes[__data]
This is a base64-encoded JSON object containing:
{
"visitorId": "1738012108903-0.269766...",
"bucketing": [
{
"experienceId": "100341351",
"variationId": "1003177123",
"goals": []
}
],
"segments": {
"country": "CA",
"browser": "CH",
"devices": ["DESK"],
"source": "direct",
"visitorType": "new"
},
"accountId": "123456",
"projectId": "789012",
"currency": "CAD"
}
Key Fields:
|
Field |
Description |
Use Case |
|
visitorId |
Unique visitor identifier across sessions |
User recognition |
|
bucketing |
Array of experiment participation records |
Attribution mapping |
|
segments.country |
2-letter country code (CA, US, GB, etc.) |
Market routing |
|
segments.device |
Device type array (DESK, MOB, TAB) |
Device segmentation |
|
accountId |
Your Convert account ID |
API authentication |
|
projectId |
Your Convert project ID |
API endpoint construction |
Supplemental Cart Attributes
Each active experiment is also stored individually as:
- Attribute Name: experience_{experienceId}
- Value: {variationId}
For example: experience_100341351 = 1003177123
Step-by-Step: Creating a Market-Specific Custom Web Pixel
Step 1: Create Market-Specific Goals in Convert
Before building the pixel, set up your goals in Convert:
- Log into Convert.com dashboard
- Navigate to Goals & Events > Goals
- Click "Create Goal" for each market:
- Example: "Begin Checkout - Canada"
- Example: "Begin Checkout - USA"
- Example: "Begin Checkout - UK"
- Copy the Goal ID for each (found in goal settings)
Goal IDs look like: 1234567890
Create a reference table:
const MARKET_GOALS = {
'CA': 'YOUR_CANADA_GOAL_ID', // e.g., '1738912345'
'US': 'YOUR_USA_GOAL_ID', // e.g., '1738912346'
'GB': 'YOUR_UK_GOAL_ID', // e.g., '1738912347'
'DE': 'YOUR_GERMANY_GOAL_ID', // e.g., '1738912348'
'FR': 'YOUR_FRANCE_GOAL_ID', // e.g., '1738912349'
};
Step 2: Disable Built-In Goal Tracking
To prevent double-tracking, configure the Shopify App's built-in goal:
- Go to Shopify Admin > Apps > Convert Shopify Experience
- Open App Settings
- Find the Goal Configuration section
- Leave the checkout_started field empty (uncheck or remove)
- Save settings
Why? The built-in Web Pixel fires automatically for all visitors. If you're using a Custom Pixel for market-specific goals, disable the corresponding built-in goal.
Step 3: Add Custom Web Pixel in Shopify
- In Shopify Admin, go to Settings > Customer Events
- Scroll to Web Pixels section
- Click "Add custom pixel"
- Configure the pixel:
- Name: "Convert Market-Specific Tracking" (or your preferred name)
- Events: Select checkout_started (and any others you need)
- Pixel Code: Paste the code below
Full Custom Pixel Code Template
analytics.subscribe('checkout_started', async (event) => {
const attributes = event.data?.checkout?.attributes || [];
// Extract the __data cart attribute
const dataAttr = attributes.find(a => a.key === '__data');
if (!dataAttr) return;
try {
// Decode the base64-encoded JSON
const shopifyData = JSON.parse(atob(dataAttr.value));
// Destructure key values
const {
visitorId,
bucketing,
segments,
accountId,
projectId
} = shopifyData;
// Determine the market-specific goal ID
const country = segments?.country; // "CA", "US", "GB", etc.
// Map countries to goal IDs
const MARKET_GOALS = {
'CA': 'YOUR_CANADA_GOAL_ID',
'US': 'YOUR_USA_GOAL_ID',
'GB': 'YOUR_UK_GOAL_ID',
'DE': 'YOUR_GERMANY_GOAL_ID',
'FR': 'YOUR_FRANCE_GOAL_ID',
// Add more markets as needed
};
const goalId = MARKET_GOALS[country];
if (!goalId) {
console.log(`No goal configured for country: ${country}`);
return; // Skip if no goal mapped for this market
}
// Build the experiment-to-variation attribution map
const bucketingData = Object.fromEntries(
bucketing.map(b => [b.experienceId, b.variationId])
);
// Prepare the tracking payload
const payload = {
accountId,
projectId,
enrichData: true,
source: 'shopifya',
visitors: [{
visitorId,
segments,
events: [{
eventType: 'conversion',
data: {
goalId,
bucketingData // Full experiment attribution
}
}]
}]
};
// Send to Convert API
await fetch(
`https://${projectId}.metrics.convertexperiments.com/v1/track/${accountId}/${projectId}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
}
);
console.log(`Market-specific conversion tracked successfully:`, {
country,
goalId,
visitorId
});
} catch (error) {
console.error('Error tracking market-specific conversion:', error);
// Optionally report to monitoring service
}
});
Step 4: Test the Pixel
- Set up a test flow:
- Add item to cart
- Proceed to checkout
- Simulate a checkout start (you don't need to complete purchase)
- Verify in browser console:
- Open DevTools on checkout page
- Look for logs: Market-specific conversion tracked successfully
- Confirm the correct country and goal ID were used
- Check Convert dashboard:
- Go to Reporting > Conversions
- Verify the conversion appears under your market-specific goal
Check Audiences to see if segment data is populated
Supporting Multiple Events
You can subscribe to various Shopify customer events. Here's how to adapt the pixel for different use cases:
Event Types Available
|
Event |
When It Fires |
Typical Use |
|
checkout_started |
User begins checkout flow |
Begin tracking |
|
payment_info_submitted |
Payment information entered |
Payment success |
|
checkout_completed |
Order confirmation page |
Purchase completion |
|
order_created |
Order successfully created |
Final conversion |
Example: Track Both Start and Completion
// Track checkout starts (for funnel analysis)
analytics.subscribe('checkout_started', async (event) => {
await trackConversion(event, 'CHECKOUT_START_GOAL_ID');
});
// Track completed purchases
analytics.subscribe('checkout_completed', async (event) => {
await trackConversion(event, 'PURCHASE_GOAL_ID');
});
// Centralized tracking function
async function trackConversion(event, goalId) {
const attributes = event.data?.checkout?.attributes || [];
const dataAttr = attributes.find(a => a.key === '__data');
if (!dataAttr) return;
const shopifyData = JSON.parse(atob(dataAttr.value));
const { visitorId, bucketing, segments, accountId, projectId } = shopifyData;
const bucketingData = Object.fromEntries(
bucketing.map(b => [b.experienceId, b.variationId])
);
await fetch(
`https://${projectId}.metrics.convertexperiments.com/v1/track/${accountId}/${projectId}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
accountId,
projectId,
enrichData: true,
source: 'shopifya',
visitors: [{
visitorId,
segments,
events: [{
eventType: 'conversion',
data: { goalId, bucketingData }
}]
}]
})
}
);
}
Adding Custom Data to Segments
If you need to pass additional context beyond what the Shopify App provides, extend the segments object before sending:
const enrichedSegments = {
...segments,
// Add custom data available at checkout time
cartTotal: event.data?.checkout?.total_price || null,
cartItemQuantity: event.data?.checkout?.line_items?.length || null,
customerEmail: event.data?.checkout?.email || null,
marketingConsent: event.data?.checkout?.marketing_consent?.opt_in_level || null,
referralSource: document.referrer || null,
};
Then include enrichedSegments in your tracking payload instead of raw segments.
Handling Edge Cases
Case 1: Missing __data Attribute
Scenario: Visitor arrives via direct URL without going through a Convert landing page.
Solution: Check for attribute existence before processing:
const dataAttr = attributes.find(a => a.key === '__data');
if (!dataAttr) {
// No experiment data — still track but mark as organic
console.log('No experiment data found, tracking without attribution');
// Optional: track with empty bucketingData
}
Case 2: Failed API Request
Scenario: Network error or Convert API unavailable.
Solution: Implement retry logic with exponential backoff:
async function sendToConvert(url, payload, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
Case 3: Multiple Active Experiments
Scenario: Visitor is participating in 3+ simultaneous experiments.
Solution: The bucketing array already handles this — it contains all active experiments. Just ensure you preserve the full array when creating bucketingData:
// Correct: preserves all experiments
const bucketingData = Object.fromEntries(
bucketing.map(b => [b.experienceId, b.variationId])
);
// INCORRECT: only takes first experiment
const bucketingData = {
[bucketing[0].experienceId]: bucketing[0].variationId
};
Alternative Approach: Reporting Only (No Custom Pixel)
If you only need market-level breakdowns in reports (not separate goals), you don't need a Custom Web Pixel:
What the App Already Provides
The built-in tracking already sends segments.country with every conversion. You can use Convert's interface to filter and segment:
- Go to Reports > Conversions
- Add a column for Country (from segments)
- Apply a filter for specific countries
- Compare metrics across markets
When this works:
- ✅ You want to compare Canada vs. USA performance in one goal
- ✅ You're comfortable using Convert's native segmentation
- ✅ You don't need separate KPI dashboards per market
When you need Custom Pixel:
- ❌ You need distinct goals in reporting/alerting systems
- ❌ You require automated notifications per market
- ❌ You're using external tools that query Convert API by goal ID
Debugging Tips
Check Pixel Execution
// Add debug logging
console.log('Custom Pixel fired!', {
checkout: event.data?.checkout,
attributes: event.data?.checkout?.attributes?.map(a => a.key)
});
Inspect Cart Attributes
In browser DevTools console during checkout:
// Shopify provides analytics global
analytics.ready(() => {
// This won't show checkout data yet (not submitted), but good for debugging setup
console.log('Analytics ready');
});
Validate API Response
Wrap the fetch call in error handling to see server responses:
try {
const response = await fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) });
const result = await response.json();
console.log('Convert API response:', result);
} catch (error) {
console.error('API failed:', error);
}
Quick Reference
|
Need |
Solution |
Key Code |
|
Market-specific goals |
Custom Web Pixel + MARKET_GOALS map |
See template above |
|
Country detection |
segments.country field in __data |
const country = segments?.country; |
|
Experiment attribution |
bucketingData map from bucketing array |
Object.fromEntries(bucketing.map(...)) |
|
Multiple markets |
Extend MARKET_GOALS object |
'XX': 'YOUR_GOAL_ID' |
|
Different events |
Subscribe to payment_info_submitted, checkout_completed |
analytics.subscribe('...') |
|
Reporting only |
Use built-in tracking + Convert segmentation |
No custom code needed |
|
Prevent double-tracking |
Disable checkout_started in app config |
Uncheck goal in Shopify App settings |