Skip to content

Instantly share code, notes, and snippets.

@samba
Last active August 2, 2024 13:17
Show Gist options
  • Save samba/3a6c901fac1459eda80d58388c996d40 to your computer and use it in GitHub Desktop.
Save samba/3a6c901fac1459eda80d58388c996d40 to your computer and use it in GitHub Desktop.
Shopify DataLayer Checkout
{% if first_time_accessed %}
<script>
(function(dataLayer){
var customer_type = ({{customer.orders_count}} > 1) ? 'repeatcustomer' : 'newcustomer';
var discounts = "{{ order.discounts | map: 'code' | join: ',' | upcase}}";
function strip(text){
return text.replace(/\s+/, ' ').replace(/^\s+/, '').replace(/\s+$/, '');
}
function firstof(){
for(var i = 0; i < arguments.length; i++){
if(arguments[i]) return arguments[i];
}
return null;
}
var products = [];
{% for line_item in order.line_items %}
products.push({
'id': firstof(strip('{{line_item.sku}}'), strip('{{line_item.product_id}}')),
'name': strip('{{line_item.product.title}}'),
'category': strip('{{line_item.product.type}}'),
'brand': strip('{{line_item.vendor}}'),
'variant': strip('{{line_item.variant.title}}'),
'coupon': "{{ line_item.discounts | map : 'code' | join: ',' | upcase}}",
'price': {{line_item.price | times: 0.01}},
'quantity': {{line_item.quantity}}
});
{% endfor %}
dataLayer.push({
'event': 'checkoutComplete',
'customerType': customer_type,
'ecommerce': {
'currencyCode': '{{shop.currency}}',
'purchase': {
'actionField': {
'id': '{{order.order_number}}',
'affiliation': strip('Shopify {{shop.name}}'),
'revenue': {{order.total_price | times: 0.01}},
'tax': {{order.tax_price | times: 0.01}},
'shipping': {{order.shipping_price | times: 0.01}},
'coupon': discounts
},
'products': products
}
}
});
setTimeout(function(){
// Clear the ecommerce data for subsequent hits.
dataLayer.push({ 'ecommerce': null });
}, 3);
}(window.dataLayer = window.dataLayer || []));
</script>
{% endif %}
<script>
function containsGTMStart(dl){
var i = 0;
dl.map(function(e){ if('gtm.start' in e) i++; });
return !!i;
}
(function(w,d,s,l,i){
w[l]=w[l]||[];
// attempts to prevent GTM from loading twice.
if(containsGTMStart(w[l])) return false;
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-XXXXX');
</script>
@acgorecki
Copy link

How do I include the script on my site?
What is the path within Shopify?

It looks like you'd place this in Settings > Checkout > Order Processing > Additional Scripts

@samba
Copy link
Author

samba commented May 21, 2021

So, I literally haven’t looked at this in 4 years. I’ve also pivoted to a different part of the tech industry, so I’ve forgotten a lot of details here.
Sorry for my delayed follow up.

As I recall, this script was inserted into the template on the checkout page. In practice it structured a checkout object for GTM/GA that pulled a lot of detail from Shopify’s template engine, and pushed it into GTM in a consumable way.

This does nothing for reporting product list pages or product detail pages. It’s solely for checkout.

Sorry i can’t provide more concrete instructions. Good luck!

@grace764
Copy link

So, I literally haven’t looked at this in 4 years. I’ve also pivoted to a different part of the tech industry, so I’ve forgotten a lot of details here.
Sorry for my delayed follow up.

As I recall, this script was inserted into the template on the checkout page. In practice it structured a checkout object for GTM/GA that pulled a lot of detail from Shopify’s template engine, and pushed it into GTM in a consumable way.

This does nothing for reporting product list pages or product detail pages. It’s solely for checkout.

Sorry i can’t provide more concrete instructions. Good luck!

Hey Samba Thanks it's working perfect 👍👏

@adrs168
Copy link

adrs168 commented Aug 15, 2021

So, I literally haven’t looked at this in 4 years. I’ve also pivoted to a different part of the tech industry, so I’ve forgotten a lot of details here.
Sorry for my delayed follow up.

As I recall, this script was inserted into the template on the checkout page. In practice it structured a checkout object for GTM/GA that pulled a lot of detail from Shopify’s template engine, and pushed it into GTM in a consumable way.

This does nothing for reporting product list pages or product detail pages. It’s solely for checkout.

Sorry i can’t provide more concrete instructions. Good luck!

Hi Samba,
Can I put in data layer such product collection in this event? What's the code 'collection': "{{ shop.collection }}"?
Many thanks

event': 'checkoutComplete',
'customerType': customer_type,
'ecommerce': {
'currencyCode': '{{shop.currency}}',
'purchase': {
'actionField': {
'id': '{{order.order_number}}',
'affiliation': strip('Shopify {{shop.name}}'),
'revenue': {{order.total_price | times: 0.01}},
'tax': {{order.tax_price | times: 0.01}},
'shipping': {{order.shipping_price | times: 0.01}},
'coupon': discounts

@mounaimaitelhaj
Copy link

to add the product collections you can do it by adding foreach

var products = [];
{% for line_item in order.line_items %}
products.push({
'id': firstof(strip('{{line_item.sku}}'), strip('{{line_item.product_id}}')),
'name': strip('{{line_item.product.title}}'),
'brand': strip('{{line_item.vendor}}'),
'variant': strip('{{line_item.variant.title}}'),
'coupon': "{{ line_item.discounts | map : 'code' | join: ',' | upcase}}",
'price': {{line_item.price | times: 0.01}},
{% for collection in line_item.product.collections %}
'category{{ forloop.index }}': "{{ collection.title }}",
{% endfor %}
'quantity': {{line_item.quantity}}

});
{% endfor %}

@chr1s1
Copy link

chr1s1 commented Oct 10, 2022

I was using this awesome script for months. Since 6th of october is suddently stopped working on 2 stores without any change to it. Anyone experiencing the same issues?

By running a test order with tag manager it seems that the checkoutComplete event is no fired anymore... 😞

@samba
Copy link
Author

samba commented Oct 10, 2022

@chr1s1 Sorry to hear that. Unfortunately I don’t provide any kind of support for this solution.

If I had to guess, one of two things happened:

  • Something changed in the Shopify template processor, or the input data format changed, causing a syntax error in the rendered Javascript.
  • Something changed in GTM such that the original logic of this integration is no longer triggering.

I’m not in a position to provide support for this myself. I can convert it to a proper GitHub repository if you’d like to contribute some change/improvement to it.

@chr1s1
Copy link

chr1s1 commented Oct 10, 2022

Hi @samba, I did not expect you to come up with a solution. But since I came across your script a while ago, others might have as well. So I assume they might have similar issues now 😄.

What I found out so far:

  • the first variable „customer.orders_count“ is null and therefore the entire script fails.
  • on one of my pages all of the used variables are empty
  • on the other page which uses the same script the variables are filled and the script works if I remove the first variable „customer.orders_count“
  • the website where it does not show values is on the smallest shopify plan, whereas the other one which works again is on the Shopify Plan..

@samba
Copy link
Author

samba commented Oct 10, 2022

That might be an easy fix then.

Replace: ({{customer.orders_count}} > 1) ? 'repeatcustomer' : 'newcustomer';
With: ((Number({{customer.orders_count}}) || 0) > 1) ? 'repeatcustomer' : 'newcustomer';

Please try and advise here if that works.

@chr1s1
Copy link

chr1s1 commented Oct 10, 2022

Found the reason. It seems that shopify has changed the order object a bit. On some payment methods the order object will not be present at the first view of the order detail page. If this happens the order object will be nil. Therefore the entire script fails. The solution is to use the checkout object variables instead:

{% if first_time_accessed %}
<script>
(function(dataLayer){

    var customer_type = ((Number({{customer.orders_count}}) || 0) > 1) ? 'repeatcustomer' : 'newcustomer';
    var discounts = "{{ order.discounts | map: 'code' | join: ',' | upcase}}";

    function strip(text){
        return text.replace(/\s+/, ' ').replace(/^\s+/, '').replace(/\s+$/, '');
    }

    function firstof(){
        for(var i = 0; i < arguments.length; i++){
            if(arguments[i]) return arguments[i];
        }
        return null;
    }

    var products = [];
    {% for line_item in line_items %}
    products.push({
        'item_id': firstof(strip('{{line_item.sku}}'), strip('{{line_item.product_id}}')),
        'item_name': strip('{{line_item.product.title}}'),
        'item_category': strip('{{line_item.product.type}}'),
        'item_brand': strip('{{line_item.vendor}}'),
        'item_variant': strip('{{line_item.variant.title}}'),
        'coupon': "{{ line_item.discounts | map : 'code' | join: ',' | upcase}}",
        'price': {{line_item.price | times: 0.01}},
        'quantity': {{line_item.quantity}}
    });
    {% endfor %}

    dataLayer.push({
        'event': 'checkoutComplete',
        'customerType': customer_type,
        'ecommerce': {
            'currencyCode': '{{currency}}',
            'purchase': {
                'actionField': {
                    'id': '{{order_number}}',
                    'affiliation': strip('Shopify {{shop.name}}'),
                    'revenue': {{total_price | times: 0.01}},
                    'tax': {{tax_price | times: 0.01}},
                    'shipping': {{shipping_price | times: 0.01}},
                    'coupon': discounts
                },
                'products': products
            }
        }
    });

    setTimeout(function(){
        // Clear the ecommerce data for subsequent hits.
        dataLayer.push({ 'ecommerce': null });
    }, 3);

}(window.dataLayer = window.dataLayer || []));
</script>
{% endif %}


<script>
function containsGTMStart(dl){
    var i = 0;
    dl.map(function(e){ if('gtm.start' in e) i++; });
    return !!i;
}
(function(w,d,s,l,i){
    w[l]=w[l]||[];
    // attempts to prevent GTM from loading twice.
    if(containsGTMStart(w[l])) return false;
    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-XXXXXXX');
</script>

@samba
Copy link
Author

samba commented Oct 10, 2022

@chr1s1 Would you like me to convert a proper GitHub repository from this, so you can directly update this in a PR? I'm happy to let others contribute/maintain this solution.

@chr1s1
Copy link

chr1s1 commented Oct 10, 2022

now that it works again, that might be a great solution! 😊

@grace764
Copy link

Found the reason. It seems that shopify has changed the order object a bit. On some payment methods the order object will not be present at the first view of the order detail page. If this happens the order object will be nil. Therefore the entire script fails. The solution is to use the checkout object variables instead:

{% if first_time_accessed %}
<script>
(function(dataLayer){

    var customer_type = ((Number({{customer.orders_count}}) || 0) > 1) ? 'repeatcustomer' : 'newcustomer';
    var discounts = "{{ order.discounts | map: 'code' | join: ',' | upcase}}";

    function strip(text){
        return text.replace(/\s+/, ' ').replace(/^\s+/, '').replace(/\s+$/, '');
    }

    function firstof(){
        for(var i = 0; i < arguments.length; i++){
            if(arguments[i]) return arguments[i];
        }
        return null;
    }

    var products = [];
    {% for line_item in line_items %}
    products.push({
        'item_id': firstof(strip('{{line_item.sku}}'), strip('{{line_item.product_id}}')),
        'item_name': strip('{{line_item.product.title}}'),
        'item_category': strip('{{line_item.product.type}}'),
        'item_brand': strip('{{line_item.vendor}}'),
        'item_variant': strip('{{line_item.variant.title}}'),
        'coupon': "{{ line_item.discounts | map : 'code' | join: ',' | upcase}}",
        'price': {{line_item.price | times: 0.01}},
        'quantity': {{line_item.quantity}}
    });
    {% endfor %}

    dataLayer.push({
        'event': 'checkoutComplete',
        'customerType': customer_type,
        'ecommerce': {
            'currencyCode': '{{currency}}',
            'purchase': {
                'actionField': {
                    'id': '{{order_number}}',
                    'affiliation': strip('Shopify {{shop.name}}'),
                    'revenue': {{total_price | times: 0.01}},
                    'tax': {{tax_price | times: 0.01}},
                    'shipping': {{shipping_price | times: 0.01}},
                    'coupon': discounts
                },
                'products': products
            }
        }
    });

    setTimeout(function(){
        // Clear the ecommerce data for subsequent hits.
        dataLayer.push({ 'ecommerce': null });
    }, 3);

}(window.dataLayer = window.dataLayer || []));
</script>
{% endif %}


<script>
function containsGTMStart(dl){
    var i = 0;
    dl.map(function(e){ if('gtm.start' in e) i++; });
    return !!i;
}
(function(w,d,s,l,i){
    w[l]=w[l]||[];
    // attempts to prevent GTM from loading twice.
    if(containsGTMStart(w[l])) return false;
    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-XXXXXXX');
</script>

Hi @chr1s1 does this script working for you ? because I had same issue since September

@chr1s1
Copy link

chr1s1 commented Oct 12, 2022

Yes, the adapted script from my comment above works. It is not referencing the order object, but uses the accessible checkout variables directly.

@grace764
Copy link

Yes, the adapted script from my comment above works. It is not referencing the order object, but uses the accessible checkout variables directly.

Thanks will give it a try 👍

@PMM-customs
Copy link

Hey,

Could you explain what

| times: 0.01

does?

Thanks a lot for the code!

@samba
Copy link
Author

samba commented Jul 27, 2023

IIRC the "price" attribute is (by default) an integer representing cents so representing it as (floating point) dollars requires dividing it by 100; it was here implemented as multiplying by 0.01. This gets you better currency representation in GTM that is intuitively sensible. Hope that helps!

@adventuretocode
Copy link

@samba I am have setup code but still getting the error. order_number is null.

@wwwXpert
Copy link

wwwXpert commented Oct 24, 2023

Note:
I believe this depends on checkout.liquid. Unfortunately, checkout.liquid will be deprecated on August 13, 2024 and store owners will need to upgrade to Checkout Extensibility before then.

After upgrading, you will no longer have access or be able to edit the checkout.liquid file.

Reference: https://shopify.dev/docs/themes/architecture/layouts/checkout-liquid

To add customer events via GTM, refer to this page https://help.shopify.com/en/manual/promoting-marketing/pixels/custom-pixels/gtm-tutorial

Web Pixels Overview: https://help.shopify.com/en/manual/promoting-marketing/pixels/overview

@chr1s1
Copy link

chr1s1 commented Oct 25, 2023

Note: I believe this depends on checkout.liquid. Unfortunately, checkout.liquid will be deprecated on August 13, 2024 and store owners will need to upgrade to Checkout Extensibility before then.

After upgrading, you will no longer have access or be able to edit the checkout.liquid file.

Reference: https://shopify.dev/docs/themes/architecture/layouts/checkout-liquid

To add customer events via GTM, refer to this page https://help.shopify.com/en/manual/promoting-marketing/pixels/custom-pixels/gtm-tutorial

Web Pixels Overview: https://help.shopify.com/en/manual/promoting-marketing/pixels/overview

I am not editing the checkout.liquid to run the script.

@wwwXpert
Copy link

wwwXpert commented Oct 25, 2023

Hi @chr1s1

You've got some good code here. I read your instructions above regarding where to add the code.
Unfortunately, the Order Processing -> Additional Scripts input textarea is gone.

There are now two "Additional Scripts" areas.
One is under Post-purchase page section.
The second is under Order status page section.

With checkout extensibility, the entire checkout process is sandboxed. You're only able to push customer events data once you've completed your checkout and arrive at the order status page.

Your script would be added to the Order status page section > Additional Scripts.
Correct?

@VictorBirdo
Copy link

Hi @chr1s1

Instead of CustomerType: NewCustomer

Is it possible to use a boolean:

New customer: false

Depending on the {{customer.orders_count}} ?

As in: if {{customer.orders_count}} is greater than 0, newcustomer: false
if {{customer.orders_count}} equals 0, newcustomer: true

Has Anyone tried this?

Thank you very much and kind regards

@umuthan
Copy link

umuthan commented May 9, 2024

Hi!

Is there a script that we can add to customer events (pixel)? Becuase we can't add script in the order status page since we are using the checkout extensibility.

Screenshot 2024-05-09 at 15 45 23

@justusbluemer
Copy link

@umuthan You can copy & paste the script from my article here: https://www.owntag.eu/blog/shopify-checkout-datalayer/
It's written specifically to work with Checkout Extensibility and follows the GA4-style standard dataLayer so that most tags in Google Tag Manager will work out of the box with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment