How To Configure Post Purchase Upsell Tracking in Shopify Checkout
Learn how to implement conversion tracking for your Zipify, Carthook, or other upsell funnels in the new unified Shopify checkout experience.
Shopify now supports upsell funnels and subscriptions in the standard checkout.
Aka “Unified Checkout”
This means if you want to add a post-purchase upsell to checkout you don’t need to use an off-site app.
These now all function in the native Shopify checkout now as well (Zipify, Carthook, etc).
The hope is that this will provide a more seamless user experience for your customers.
However this change adds complexity into how you track your conversions.
Here’s why:
- Historically you’ve likely used the “thank you” page inside your Checkout settings to paste various tracking scripts.
- However this final thank you page will no longer be displayed on all of your customer purchase events. Why? The user has to physically accept an Upsell item OR click the “no thanks” option and wait for the page to load. Data tells us the combination of these two happens less than 50% of the time because the user is satisfied with their main purchase and closes out their session
- Shopify has stated known tracking limitations in their docs which include:
Purchase events: Third-party analytic services that use the Shopify Pixel API (such as Google Analytics, Facebook, Pinterest and Snap) report only the purchase event and value for the initial purchase.
Analytics: Third-party analytics services that use the ScriptTag
REST Admin API or GraphQL Admin API resource, or Additional Scripts have incomplete conversion data, because they’re only triggered on the thank you page.
Here are commons upsell use-cases:
- A user who completes main purchase, sees the upsell offer, but decides to exit their session
- A user who completes main purchase, sees the upsell offer, clicks the “no thanks”, and views the thank you order status page
- A user who completes main purchase, takes the upsell offer, and views the thank you order status page
- A user who completes main purchase, takes the upsell offer, views another upsell offer but declines, and views the thank you order status page
After working with many customers using Carthook and Zipify like Snow Teeth Whitening and Magic Spoon, we’ve found one of the most critical questions to answer that drives your setup is this:
Do you rely on cost-per-acquisition metrics or total revenue (including upsell revenue)?
The answer to this question will change your requirements.
For example, if you rely on CPA then you don’t want to trigger a “Purchase” event to Facebook on the main purchase AND subsequent upsell purchases.
Let’s walk through this new setup and your options for tracking.
New Post Purchase Upsell Flow in Shopify
You’ll see the new post-purchase page settings in your Shopify checkout settings here:
The post-purchase page is similar to the order status page, except that is is sandboxed Javascript only.
This means:
- Shopify liquid syntax is not supported
- GTM won’t work in preview mode, and it’s fairly difficult to test if you aren’t fairly technical
Here is what an Upsell page looks like after clicking “Place Order” on the main purchase:
Here is what an order looks like when a user takes an upsell:
At this point the customer has 3 options:
- Exit page — this might be the majority of your customers based on data we see with other upsell funnels
- Take the upsell and choose Pay Now
- Click Decline this offer
If the user chooses option 1 then the only conversion tracking data that is triggered will be:
- Integrations using Shopify webhooks. This is how Elevar’s server-side integration works for Google Analytics, Facebook, etc.
- Native Shopify integrations that use the Shopify Pixel API – Google Analytics, Facebook, Pinterest (as noted in their docs)
- Conversion scripts placed in the post-purchase tracking settings
So if you don’t have either of the three options above configured then you’ll be missing conversions in certain channels.
For example if you:
- Have an upsell funnel, and,
- Require conversion tracking for Impact Radius, Google Ads, Grin, or other marketing & affiliate partners, and,
- Still have these scripts in your final “thank you order status” page settings only, and,
- Don’t migrate these channels to server side tracking OR post-purchase scripts setting
Then you’ll miss tracking these conversions.
How To Configure Post Purchase Upsell Tracking on Shopify
This guide assumes the following as a baseline:
- You are using server-side tracking (Elevar or GTM server-side) and specifically Elevar’s webhook integration that removes the dependency on the browser triggering your main purchase conversions.
- 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.
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 at the bottom 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. 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) { } (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'); })(); </script>
Step 2: Update Code in Thank You Page Settings
There is a feature that Shopify makes available to try and prevent duplicate tracking.
It’s a “unless post purchase page accessed” flag similar to below
If you are on Shopify Plus then your “thank you” page data layer (from Elevar) is set in your checkout.liquid template.
Otherwise the Elevar data layer is copied into your thank you page Checkout settings for non-plus stores. .
You’ll want to wrap all of your tracking code in this if you don’t want the code to fire if it was already triggered during an upsell funnel.
{% if first_time_accessed and post_purchase_page_accessed != true %} // tag code to be run if upsell pages have not taken care of sending purchase data. {% endif %}
The way this works is this:
- If the user already viewed an upsell offer (which triggered the dl_purchase event), then don’t execute this code
- Otherwise if the user did not view an upsell offer and was sent right to the thank you page, then execute this code
This guide assumes that you are utilizing Elevar’s server-side integrations for Facebook and Google Analytics which handles sending 100% of your main purchase events without having to modify any code.
But you need to account for:
- Sending upsell revenue and events to GA and Facebook
- Sending primary (and upsell) purchase events to your non GA & Facebook channels
It’s also important to note that this first time access logic that Shopify provides has been shown not to work in checkout.liquid template. We believe it’s a bug – so be sure to test for yourself.
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.
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.
- Orders without upsells (prevents conversion rate inflation).
- All orders (no filter)
- Upsell orders (include filter)
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 4: 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
- The final thank you page script settings will not contain data from any upsells added to the order. Only the main product. This is why handling upsells taken in the offer needs to be configured in the Post Purchase settings in the Shopify checkout settings
What To Do Next
If you’ve historically relied on your thank you page scripts and are using an upsell funnel then migrating your tracking to use either:
- Post-purchase tracking script setting
- Or webhook integrations (like Elevar)
Is highly recommended.
Need a solution quickly?
Book a call with us here or create your own Elevar account today to get started.
Leave a Reply