How to Track Post Purchase Upsells

Learn how to update your tracking to ensure maximum conversion tracking coverage for your upsells in the native Shopify checkout.

How Upsell Funnels Work

If you have an upsell app such as Rebuy, CartHook, or Zipify installed on your Shopify Store you have the ability to offer additional products after the checkout has completed. This post-purchase page is presented to your customer after the payment is processed before the thank you page.

If you have an upsell funnel enabled, after a purchase is made, an upsell offer is shown to the customer like this:

Then, if a customer takes the upsell, we fire a new event called dl_upsell_purchase. This happens before the final thank you page loads.

The purchase information sent on this event refers only to the upsell products (think of it as a completely separate purchase).

Next, when your customer reaches the final thank you page we fire the dl_purchase event. If you user never reaches the thank you page we've got you covered with your server-side purchase event triggered by the order being created in Shopify.

If you want to add the upsell event tracking in your destinations follow these next steps.

Step 1: Add Code to Post-Purchase Settings

In Shopify navigate to Settings, and then Select Checkout Settings. Scroll to the Post-purchase page section. You'll add the code as instructed below to this section.

The code below pushes a dl_upsell_purchase event when a user takes an upsell or downsell offer.

Copy the script below into your Shopify post-purchase page checkout settings (shown in the previous image above). Be sure to update the GTM-xxxxx near the top of the script with your Web Container ID.

<!-- Begin Elevar Upsell Tracking -->
<script>
// Fires an dl_upsell_purchase event when customer takes upsell offer

(function (w, d, s, l, i) {
    w[l] = w[l] || []; w[l].push({
        'gtm.start':
            new Date().getTime(), event: 'gtm.js'
    }); var f = d.getElementsByTagName(s)[0],
        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
            'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-XXX');

var upsellCount = 0;
(function() {
// EVENT HOOKS -----------------------------------------------------------
if (!Shopify.wasPostPurchasePageSeen) {
    onCheckout(window.Shopify.order, window.Shopify);
}

Shopify.on('CheckoutAmended', function (newOrder, initialOrder) {
    onCheckoutAmended(newOrder, initialOrder, window.Shopify);
});
// END EVENT HOOKS -------------------------------------------------------

// UTILS -----------------------------------------------------------------
// Function called after original order is placed, pre upsell.
function onCheckout(initialOrder, shopifyObject) {
    window.dataLayer = window.dataLayer || [];
    pushDLPurchase(initialOrder, initialOrder.lineItems, false, null, shopifyObject);
}

// Function called when upsell is taken. Separate the new/upsell
// items from the items in the initial order and then send a purchase event
// for just the new items.
function onCheckoutAmended(upsellOrder, initialOrder, shopifyObject) {
    // identify which items were added to the initial order, if any.
    upsellCount++;
    // The line item id is unique for order items, even if the items contained are the same.
    // We can use this to seperate out items from the initial order from the upsell.
    var initialItemIds = initialOrder.lineItems.map(function (line) { return line.id; });
    var addedItems = upsellOrder.lineItems.filter(
        function (line) { return initialItemIds.indexOf(line.id) < 0; }
    );
    // if no new items were added skip tracking
    if (addedItems.length === 0) return;
    pushDLPurchase(upsellOrder, addedItems, true, initialOrder, shopifyObject);
}

function pushDLPurchase(order, addedItems, isUpsell, initialOrder, shopifyObject) {
    window.dataLayer.push({
        'event': isUpsell ? 'dl_upsell_purchase' : '',
        'event_id': getOrderId(order.id, isUpsell),
        'user_properties': getUserProperties(order),
        'ecommerce': {
            'purchase': {
                'actionField': getActionField(order, isUpsell, initialOrder, addedItems, shopifyObject),
                'products': getLineItems(addedItems),
            },
            'currencyCode': order.currency,
        },
    });
}
// Returns a user properties object
function getUserProperties(data) {
    return {
        'customer_id': data.customer.id,
        'customer_email': data.customer.email,
        'customer_first_name': data.customer.firstName,
        'customer_last_name': data.customer.lastName,
        'customer_order_count': data.customer.ordersCount,
    }
}

// Gets line items in purchase
function getLineItems(lineItems) {
    return lineItems.map(function (item) {
        return {
            'category': item.product.type,
            'variant_id': item.variant.id.toString(),
            'product_id': Number(item.product.id).toString(),
            'id': item.variant.sku,
            // We don't get variant title details
            'variant': item.title,
            'name': item.title,
            'price': item.price.toString(),
            'quantity': item.quantity.toString(),
            // Not available
            // 'brand': orderItem.brand,
        }
    });
}

function getActionField(order, isUpsell, initialOrder, addedItems, shopifyObject) {
    var revenue = isUpsell ? getAdditionalRevenue(order, initialOrder) : order.totalPrice;
    var subtotal = isUpsell ? getAdditionalSubtotal(order, initialOrder) : order.subtotalPrice;
    try {
        affiliation = new URL(shopifyObject.pageUrl).hostname;
    } catch (e){
        affiliation = '';
    }
    return {
        'action': "purchase",
        'affiliation': affiliation,
        // This is the longer order id that shows in the url on an order page
        'id': getOrderId(order.id, isUpsell).toString(),
        // This should be the #1240 that shows in order page.
        'order_name': getOrderId(order.number, isUpsell).toString(),
        // This is total discount. Dollar value, not percentage
        // On the first order we can look at the discounts object. On upsells, we can't.
        // This needs to be a string.
        'discount_amount': getDiscountAmount(order, isUpsell, addedItems),
        // We can't determine shipping & tax. For the time being put the difference between subtotal and rev in shipping
        'shipping': (parseFloat(revenue) - parseFloat(subtotal)).toString(),
        'tax': '0',
        'revenue': revenue,
        'sub_total': subtotal,
    };
}

function getDiscountAmount(shopifyOrder, isUpsell, addedItems) {
    if (shopifyOrder.discounts === null || typeof shopifyOrder.discounts === 'undefined') return '0';
    if (shopifyOrder.discounts.length === 0) return '0';
    // If this isn't an upsell we can look at the discounts object.
    if (!isUpsell) {
        // Collect all the discounts on the first order.
        return shopifyOrder.discounts.reduce(function (acc, discount) {
            return acc += parseFloat(discount.amount);
        }, 0).toFixed(2).toString();
    // If this an upsell we have to look at the line item discounts
    // The discount block provided doesn't only applies to the first order.
    } else {
        return addedItems.reduce(function (acc, addedItem) {
            return acc += parseFloat(addedItem.lineLevelTotalDiscount);
        }, 0).toFixed(2).toString();
    }

}

function getOrderId(orderId, isUpsell) {
    return isUpsell ? orderId.toString() + '-US' + upsellCount.toString() : orderId;
}


function getAdditionalRevenue(newOrder, initialOrder) {
    return (parseFloat(newOrder.totalPrice) - parseFloat(initialOrder.totalPrice)).toFixed(2);
}

function getAdditionalSubtotal(newOrder, initialOrder) {
    return (parseFloat(newOrder.subtotalPrice) - parseFloat(initialOrder.subtotalPrice)).toFixed(2);
}

function test() {
    onCheckoutAmended(newOrder, initialOrder);
}

try {
    module.exports = exports = {
        onCheckoutAmended: onCheckoutAmended,
        onCheckout: onCheckout,
        resetUpsellCount: function(){upsellCount = 0;},
    };
} catch (e) { }

})();
</script>
<!-- End Elevar Upsell Tracking -->

Step 2: Import Pre-Built Container(s)

First let's configure upsell revenue and events for GA and Facebook:

Download our Upsell Purchases pre-built container from your Elevar dashboard that ultimately looks like this inside of GTM:

In the default setup we attach a Facebook Upsell Purchase tag to the dl_upsell_purchase event. This sends a custom conversion event so we don't inflate FB Purchase conversions.

We also attach a custom GA event to the upsell that hyphenates the original order id (for ex. order 1234-US1). This lets you create a view in GA that filters out upsells and prevents inflated conversion rates.

Sending upsell purchase events to non GA & Facebook channels

If you are already using Elevar's pre-built container for other channels then you only need to account for the dl_upsell_purchase events.

For example, let's say you have Google Ads configured in GTM for primary purchase events, but you want to send a Google Ads conversion for upsell purchases as well.

You can create a new conversion in Google Ads and assign to the same upsell_purchase trigger used by Facebook:

Step 3: Publish Updates & QA

One big limitation with the new post-purchase page settings is that you can't use GTM preview mode since it's sandboxed JavaScript. This means you'll need to publish your container, place a test order, and verify using dev tools in your browser OR inside your events manager.

Will I miss out on purchase tracking if my customer doesn't reach the Thank You Page?

You may find that many of your users don't navigate to the thank you page when you present a post-purchase upsell. If you are using our server-side tracking for available destinations this is not an issue as the server-side purchase trigger is based on the order creation within Shopify to trigger the server-side event to send. This trigger still occurs even if the customer closes out at the post-purchase step.

We recommend leaving your dl_purchase to fire on the thank_you page for a few reasons:

  1. The purchase data available on the post-purchase is limited and will prevent you from capturing some information like new vs returning users.
  2. There is a 500ms time limit for all tracking script to process on the post-purchase page, this prevents most web tags from firing which then leads to additional customizations for the tags that don't fire to get them to fire on the thank you page.
  3. It's much easier this way! 💥