How To Track Post-Purchase Upsells in Shopify Non-Plus Checkout or Checkout Extensibility

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

πŸ“˜

If using our Shopify Source / Data Layer version 3.9 follow our updates guide:

How to Track Post-Purchase Upsells

If you are using an app like Zipify, Carthook, or others that trigger a post-purchase upsell funnel in the native Shopify checkout then please follow these instructions to update your store.

Background

Many of our customers have a combination of:

  • Server-side tracking for Facebook, Google Analytics, and potentially Google Ads
  • Client-side tracking for other channels like affiliates, Snapchat, native advertising, or display. This tracking typically lives either in a GTM web container and/or your Shopify (in checkout.liquid or thank you page script settings)

If this sounds like you then this means the implementation of your post-purchase tracking isn't just a simple copy/paste setup. Your actual implementation will differ based on things like:

  • Do you want one single "Purchase" event sent to Facebook even if a user takes 2 upsells OR do you want 3 unique "Purchase" events sent to Facebook.
  • Do you want upsell revenue tracked in GA?
  • Do you want upsell revenue tracked in your miscellaneous sources like TikTok, Snapchat, Impact, or other channels

This guide assumes the following as a baseline:

  1. You are using Elevar's Server-Side tracking that removes the dependency on the browser triggering your main purchase conversions.
  2. You are using Elevar Data Layer & pre-built tags with your GTM Web Container

πŸ“˜

If you are not using Elevar's server-side tracking and are relying on GTM web container for purchase events then this guide will still work, but you will likely miss out on some conversions being tracked.

How Upsell Funnels Work

You'll see the new post-purchase page settings in your Shopify checkout settings here:

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

As soon as the upsell window is shown (like the image above), we send a dl_purchase event in the data layer for the initial order that can be used for client-side tracking in your GTM Web container. This event replaces the standard dl_purchase event that normally only fires on the order status page.

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).

πŸ“˜

This is a little tricky at first - but the dl_upsell_purchase logic is handled in the post-purchase page script setting - NOT the final thank you page settings.

Step 1: Add Code to Post-Purchase Settings

The code below handles the scenarios where:

  • User views upsell offer page // we push a dl_purchase event
  • User takes an upsell or downsell offer // we push a dl_upsell_purchase event

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.

<script>
// If this page hasn't been seen push a dl_purchase event after the initial sale.

(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. Seperate 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' : 'dl_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,
    }
}

// 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>

Step 2: Modify the DataLayer in Order Status Scripts

In the Shopify Order status page additional script checkout settings, find
{% if first_time_accessed %} as shown below and replace with this line:

{% if first_time_accessed and post_purchase_page_accessed != true %}

Your dataLayer will have unique updated and time, version number, and your GTM container ids, do not make any other updates other than this line for the Order status page scripts.

Step 3: 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 primary (and 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.

πŸ“˜

The reason that you only need to account for dl_upsell_purchase events is because our code from the previous step will already push a dl_purchase event on the upsell offer page. The dl_purchase event is the trigger that all of our pre-built containers use.

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:

For any channel you want to track, just be sure that you don't have hard coded conversions inside your Shopify thank you page AND triggering from GTM. Otherwise you'll have duplicates.

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.

Known Limitations

  • We can't determine shipping or tax alone. We get revenue, and a subtotal. From there we have to guess what portion of the difference is tax and what portion is shipping. By default we attribute the difference in revenue and subtotal to shipping.
  • We don't have all the normal purchase data on the page that Shopify typically provides. For example no customer address information is provided.
  • Liquid syntax is not supported inside the post-purchase page settings

Troubleshooting:

  1. If you aren’t getting purchase events try removing the exclamation mark (not) from line 5. Changing this (!Shopify.wasPostPurchasePageSeen) to this (Shopify.wasPostPurchasePageSeen).
  1. Make sure you have the GTM container filled in.

Optional Advanced Settings

Create Filtered View in Google Analytics

To filter upsell orders in GA create a filter and exclude transactions ending with a hyphen + US + digits (for ex. 1234-US1). Use a filter on Transaction ID. You can make 3 views based on your preferences.

  1. Orders without upsells (prevents conversion rate inflation).
  2. All orders (no filter)
  3. Upsell orders (include filter)

^.-US[\d]$

Here's what the filter looks like in GA: