Shopify custom pixel for builder to work with checkout extensibility

We’re upgrading to the checkout extensibility feature on shopify plus, and that seems to disrupt the conversion tracking through builder. When we turned on the checkout extensibility, no conversions are tracked in builder.io which destroys the point of the a/b tests we run on builder

Are there any instructions on how to create a custom pixel in shopify to track conversions in builder?

@cranetrain have you have any advancement with this? we’re in the same boat.

1 Like

Sadly nope.

It feels like it should be really doable … but haven’t figured it out and the builder suggestions aren’t relevant unfortunately

Dang, that is such a bummer. I am trying to implement a server-side call but builder does not have anything capable of doing this right now. I don’t know what they’re going to do since the deadline is fast approaching for this to be resolved as checkout.liquid is deprecated and soon all will be forced. In fact, I don’t know how they do it now for new customers that cannot get access to checkout.liquid period.

hey @cranetrain @sebizox apologies for delay, but this is fully available within Shopify pixels and Builder! It is a newer paradigm so we are in the process of updating our docs but you can see a short loom explanation here:

And an example snippet below:

UPDATE: As outlined in this doc: Builder vs. Shopify Conversions - Builder.io it is possible that sometimes events from Shopify or some other event tracker may differ, and so we have updated the code snippet below to add a more robust mechanism to capture and retry any events that may be dropped by poor network connection or unsuccessful event capture

const BUILDER_STORAGE_KEY = 'builderPendingEvents';
const API_KEY = 'Your API Key';
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;

// Helper function to safely get storage data
const getPendingEvents = () => {
    try {
        const stored = localStorage.getItem(BUILDER_STORAGE_KEY);
        return stored ? JSON.parse(stored) : [];
    } catch (e) {
        console.error('Error reading pending events:', e);
        return [];
    }
};

// Helper function to safely store data
const storePendingEvent = (eventData) => {
    try {
        const events = getPendingEvents();
        events.push(eventData);
        localStorage.setItem(BUILDER_STORAGE_KEY, JSON.stringify(events));
    } catch (e) {
        console.error('Error storing pending event:', e);
    }
};

// Helper function to remove stored events
const removePendingEvents = () => {
    try {
        localStorage.removeItem(BUILDER_STORAGE_KEY);
    } catch (e) {
        console.error('Error removing pending events:', e);
    }
};

// Function to send tracking data to Builder.io
const sendToBuilder = async (eventData, retryCount = 0) => {
    try {
        const response = await fetch(`https://cdn.builder.io/api/v1/track?apiKey=${API_KEY}`, {
            method: 'POST',
            body: JSON.stringify({ events: [eventData] }),
            headers: {
                'content-type': 'application/json',
            },
            mode: 'cors',
            keepalive: true,
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        return true;
    } catch (error) {
        console.error('Tracking error:', error);
        
        if (retryCount < MAX_RETRIES) {
            await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * (retryCount + 1)));
            return sendToBuilder(eventData, retryCount + 1);
        }
        
        // If all retries failed, store the event for later
        storePendingEvent(eventData);
        return false;
    }
};

// Main tracking function
const trackConversion = ({ checkout, amountInfo, meta = {} }) => {
    if (!checkout || !checkout.order || !checkout.order.id) {
        console.error('Invalid checkout data received');
        return;
    }

    const sessionId = document.cookie.split('; ')
        .find(row => row.startsWith('builderSessionId='))
        ?.split('=')[1] || null;
    
    const visitorId = localStorage.getItem('builderVisitorId') || null;

    if (!sessionId || !visitorId) {
        console.warn('Missing sessionId or visitorId');
    }

    const eventData = {
        type: 'conversion',
        data: {
            amount: amountInfo.amount,
            metadata: {
                sdkVersion: '3.2.10',
                url: location.href,
                orderId: checkout.order.id,
                currency: checkout.currencyCode,
                timestamp: new Date().toISOString(),
                ...meta,
            },
            ownerId: API_KEY,
            userAttributes: {
                sessionId,
                visitorId,
            },
            sessionId,
            visitorId,
        },
    };

    return sendToBuilder(eventData);
};

// Setup main event listener
analytics.subscribe('checkout_completed', (event) => {
    trackConversion({
        checkout: event.data.checkout,
        amountInfo: event.data.checkout.totalPrice,
        meta: { 
            additionalData: 'Conversion Recorded',
            eventId: event.id,
            clientId: event.clientId
        },
    });
});

// Try to send pending events on page load
window.addEventListener('load', async () => {
    const pendingEvents = getPendingEvents();
    if (pendingEvents.length > 0) {
        const results = await Promise.all(
            pendingEvents.map(event => sendToBuilder(event))
        );
        
        if (results.every(Boolean)) {
            removePendingEvents();
        }
    }
});

// Backup tracking before page unload
window.addEventListener('beforeunload', () => {
    const pendingEvents = getPendingEvents();
    if (pendingEvents.length > 0) {
        // Use synchronous localStorage to ensure data is saved
        try {
            navigator.sendBeacon(
                `https://cdn.builder.io/api/v1/track?apiKey=${API_KEY}`,
                JSON.stringify({ events: pendingEvents })
            );
            removePendingEvents();
        } catch (e) {
            console.error('Error sending beacon:', e);
        }
    }
});
1 Like

Here’s the latest optimized version of your Builder.io conversion tracking script with minor enhancements, inline comments for clarity, and improved safety checks. The core logic is preserved, and functionality remains unchanged.

const BUILDER_STORAGE_KEY = 'builderPendingEvents';
const API_KEY = 'YOUR_API_KEY'
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;

// Helper function to safely get storage data
const getPendingEvents = () => {
  try {
    const stored = localStorage.getItem(BUILDER_STORAGE_KEY);
    return stored ? JSON.parse(stored) : [];
  } catch (e) {
    console.error('Error reading pending events:', e);
    return [];
  }
};

// Helper function to safely store data
const storePendingEvent = (eventData) => {
  try {
    const events = getPendingEvents();
    events.push(eventData);
    localStorage.setItem(BUILDER_STORAGE_KEY, JSON.stringify(events));
  } catch (e) {
    console.error('Error storing pending event:', e);
  }
};

// Helper function to remove stored events
const removePendingEvents = () => {
  try {
    localStorage.removeItem(BUILDER_STORAGE_KEY);
  } catch (e) {
    console.error('Error removing pending events:', e);
  }
};

// Function to send tracking data to Builder.io
const sendToBuilder = async (eventData, retryCount = 0) => {
  try {
    console.log("Sending to Builder...", eventData);
    const response = await fetch(
      `https://cdn.builder.io/api/v1/track?apiKey=${API_KEY}`,
      {
        method: 'POST',
        body: JSON.stringify({ events: [eventData] }),
        headers: {
          'content-type': 'application/json',
        },
        mode: 'cors',
        keepalive: true,
      }
    );

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    console.log("Builder response:", response);
    return true;
  } catch (error) {
    console.error('Tracking error:', error);

    if (retryCount < MAX_RETRIES) {
      await new Promise((resolve) =>
        setTimeout(resolve, RETRY_DELAY * (retryCount + 1))
      );
      return sendToBuilder(eventData, retryCount + 1);
    }

    storePendingEvent(eventData);
    return false;
  }
};

// Main tracking function
const trackConversion = ({ amountInfo, meta = {} }) => {
  console.log("Track Conversion Started...");
  
  const sessionId = document.cookie
    .split('; ')
    .find((row) => row.startsWith('builderSessionId='))
    ?.split('=')[1] || null;

  const variationId = document.cookie
    .split('; ')
    .find((row) => row.startsWith('builder.tests='))
    ?.split('=')[1] || null;

  const visitorId = localStorage.getItem('builderVisitorId') || null;

  console.log("Session ID:", sessionId);
  console.log("Variation ID:", variationId);
  console.log("Visitor ID:", visitorId);
  console.log("Amount:", amountInfo.amount);

  const eventData = {
    type: 'conversion',
    data: {
      amount: amountInfo.amount,
      metadata: {
        sdkVersion: '3.2.10',
        url: location.href,
        currency: amountInfo.currencyCode,
        timestamp: new Date().toISOString(),
        ...meta,
      },
      ownerId: API_KEY,
      userAttributes: {
        sessionId,
        visitorId,
        variationId,
      },
      sessionId,
      visitorId,
    },
  };

  console.log("Event data being sent:", eventData);
  return sendToBuilder(eventData);
};

// ✅ Updated main event listener with `order_id` or fallback to `checkout.token`
analytics.subscribe('checkout_completed', (event) => {
  console.log("Checkout Completed Event Received:", event);
  
  if (!event.data || !event.data.checkout) {
    console.error('Invalid checkout data received');
    return;
  }

  const checkout = event.data.checkout;
  console.log("Processing checkout:", checkout);

  trackConversion({
    amountInfo: checkout.totalPrice,
    meta: {
      additionalData: 'Conversion Recorded',
      eventId: event.id,
      clientId: event.clientId,
      order_id: checkout.order?.id || `checkout_token:${checkout.token}`,
    },
  });
});

// Try to send pending events on page load
window.addEventListener('load', async () => {
  const pendingEvents = getPendingEvents();
  if (pendingEvents.length > 0) {
    console.log("Processing pending events:", pendingEvents);
    const results = await Promise.all(
      pendingEvents.map((event) => sendToBuilder(event))
    );

    if (results.every(Boolean)) {
      removePendingEvents();
    }
  }
});

// Backup tracking before page unload
window.addEventListener('beforeunload', () => {
  const pendingEvents = getPendingEvents();
  if (pendingEvents.length > 0) {
    try {
      navigator.sendBeacon(
        `https://cdn.builder.io/api/v1/track?apiKey=${API_KEY}`,
        JSON.stringify({ events: pendingEvents })
      );
      removePendingEvents();
    } catch (e) {
      console.error('Error sending beacon:', e);
    }
  }
});