Since Google Analytics GA4 Ecommerce data finally are available in BigQuery, I hereby share my Google Tag Manager concept for using Google Analytics Enhanced Ecommerce implementation to track GA4 Ecommerce. This means that if you have Enhanced Ecommerce implemented, you can use this setup and send Ecommerce data to App+Web using your existing implementation.
The concept I share here can be used by everyone no matter how you have implemented Enhanced Ecommerce. The only thing you will have to adjust to your needs are the GTM Triggers for your GA4 Ecommerce Tags, but I will explain how you do that.
Update 1:
Since I wrote this blog post, App+Web have changed the name to GA4 (Google Analytics 4).
Update 2:
Although using Custom Javascript Variables to map Ehanced Ecommerce to GA4 Ecommerce is OK, an easier (and better) solution is to use a pre-made GTM Template instead. You will find several examples in the Google Tag Manger Template Gallery.
I have also created a GTM Template called GA Enhanced Ecommerce to GA4 Ecommerce Converter.
This GTM Variable creates either GA4 Events or GA4 Ecommerce Items based on the Enhanced Ecommerce Object or Google Tag Manager Variables. You can also map/rename Product Scoped Dimensions & Metrics, and map Enhanced Ecommerce Checkout to GA4 Events like add_payment_info and add_shipping_info.
The template is explained in detail on Github.
GTM Variables for GA4 Ecommerce
You will have to create 11 Data Layer Variables, and 7 Custom Javascript Variables in GTM for a complete setup.
Data Layer Variables
Create the Data Layer Variables shown in the table below.
Variable Name | Data Layer Variable Name | Paramenter Name or (Comment) | Data Layer Version |
---|---|---|---|
ecom - ecommerce - DLV | ecommerce | (Enhanced Ecommerce object) | 1 |
ecom - purchase - Id - DLV | ecommerce.purchase.actionField.id | transactions_id | 2 |
ecom - purchase - revenue - DLV | ecommerce.purchase.actionField.revenue | value | 2 |
ecom - currency - DLV | ecommerce.currency | currency | 2 |
ecom - purchase - tax - DLV | ecommerce.purchase.actionField.tax | tax | 2 |
ecom - purchase - shipping - DLV | ecommerce.purchase.actionField.shipping | shipping | 2 |
ecom - purchase - coupon - DLV | ecommerce.purchase.actionField.coupon | coupon | 2 |
ecom - checkout - step - DLV | ecommerce.checkout.actionField.step | checkout_step | 2 |
ecom - checkout - option - DLV | ecommerce.checkout.actionField.option | checkout_option | 2 |
ecom - checkout_option - step - DLV | ecommerce.checkout_option.actionField.step | checkout_step | 2 |
ecom - checkout_option - option - DLV | ecommerce.checkout_option.actionField.option | checkout_option | 2 |
I’m using Data Layer Version 1 for my {{ecom – ecommerce – DLV}} Data Layer, so if you want to use Version 2 instead, you will have change some of the code in the Custom Javascript Variable {{ga4 – ecom – event – checkout – CJS}}.
Custom Javascript Variables
These 7 Custom Javascript Variables are doing the “magic” here based on the Enhanced Ecommerce object. 4 variables maps the Enhanced Ecommerce object into GA4 Events for Retail/Ecommerce, and the 3 others creates new GA4 Ecommerce objects.
If you wonder why we can’t just have 1 Variable for GA4 Events, and 1 Variable for GA4 Ecommerce object, the reason is that we are creating GA4 Events based on Enhanced Ecommerce Actions from the ecommerce object. Within the ecommerce object we could have Ecommerce Actions for Promotions, Impressions and Product Detail View at the same time depending on the implementation, and I have therfor splitted them into different Variables.
If that isn’t a problem in your implementation, you can of course reduce the number of Variables. In addition, I have created 1 Variable for the 3 different Checkout Events.
Custom Javascript Variables for creating GA4 Events
Generic GA4 Events
This variable creates the following GA4 Events:
- view_item
- select_item
- add_to_cart
- remove_from_cart
- purchase
- refund
Name the variable ga4 – ecom – event – product – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // Map Enhanced Ecommerce Actions to App+Web Ecommerce Events var eeActionMapping = { 'detail': 'view_item', 'click': 'select_item', 'add': 'add_to_cart', 'remove': 'remove_from_cart', 'purchase': 'purchase', 'refund': 'refund' } // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (getKeys(eeActionMapping).indexOf(e) > -1) return e; }); if (ecommerce && eeAction) { // Set the app_web event action return eeActionMapping[eeAction]; } } catch (error) {} } |
View Item List GA4 Event
This variable creates the following GA4 Event:
- view_item_list
Name the variable ga4 – ecom – event – view_item_list – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // Map Enhanced Ecommerce Actions to App+Web Ecommerce Events var eeActionMapping = { 'impressions': 'view_item_list' } // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (getKeys(eeActionMapping).indexOf(e) > -1) return e; }); if (ecommerce && eeAction) { // Set the app_web event action return eeActionMapping[eeAction]; } } catch (error) {} } |
Checkout App+Web Event
This variable creates the following GA4 Events:
- begin_checkout
- checkout_progress
- set_checkout_option
Name the variable ga4 – ecom – event – checkout – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // Map Enhanced Ecommerce Actions to App+Web Ecommerce Events var eeActionMapping = { 'checkout_option': 'set_checkout_option', 'checkout': 'checkout_progress' } // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (getKeys(eeActionMapping).indexOf(e) > -1) return e; }); if (ecommerce && eeAction) { // If checkout step = 1, create the 'begin_checkout' Event instead if (eeAction.toString()==='checkout' && ecommerce['checkout']['actionField']['step']==='1') { eeActionMapping = {'checkout': 'begin_checkout'} } // Set the app_web event action return eeActionMapping[eeAction]; } } catch (error) {} } |
Promotion GA4 Events
This variable creates the following GA4 Events:
- select_promotion
- view_promotion
Name the variable ga4 – ecom – event – promotion – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // Map Enhanced Ecommerce Actions to App+Web Ecommerce Events var eeActionMapping = {}; eeActionMapping = { 'promoClick': 'select_promotion', 'promoView': 'view_promotion' } // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (getKeys(eeActionMapping).indexOf(e) > -1) return e; }); if (ecommerce && eeAction) { // Set the app_web event action return eeActionMapping[eeAction]; } } catch (error) {} } |
Custom Javascript Variables for creating GA4 Ecommerce objects
Generic GA4 Ecommerce object
This variable creates the GA4 Ecommerce object for the following GA4 Events:
- view_item
- select_item
- add_to_cart
- remove_from_cart
- begin_checkout
- checkout_progress
- purchase
- refund
Name the variable ga4 – ecom – product – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // List Enhanced Ecommerce Actions // This could have been listed as an string, but an Array makes it easier to read var eeActionMapping = [ 'click', // Product Clicks 'detail', // Product Details 'add', // Add to Cart 'remove', // Remove from Cart 'checkout', // Checkout 'purchase', // Purchase 'refund' // Refund ] // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (eeActionMapping.toString().indexOf(e) > -1) return e; }); if (eeAction) { var appWebEcom = []; for(var a =0; a < eeAction.length; a++){ if (ecommerce[eeAction].products.length>0) { var eeEcom = ecommerce[eeAction].products; for(var i =0; i < eeEcom.length; i++){ var item = eeEcom[i]; appWebEcom.push({ 'item_id': item.id, 'item_name': item.name, 'item_variant': item.variant, 'item_brand': item.brand, 'item_category': (item.category && item.category.split('/')[0]) ? item.category.split('/')[0] : undefined, 'item_category2': (item.category && item.category.split('/')[1]) ? item.category.split('/')[0] : undefined, 'item_category3': (item.category && item.category.split('/')[2]) ? item.category.split('/')[0] : undefined, 'item_category4': (item.category && item.category.split('/')[3]) ? item.category.split('/')[0] : undefined, 'item_category5': (item.category && item.category.split('/')[4]) ? item.category.split('/')[0] : undefined, 'quantity': item.quantity, 'price': item.price, 'item_list_name': undefined, 'coupon': item.coupon }); } // Get Product List if ((ecommerce[eeAction].actionField) && (eeAction == 'add'||eeAction == 'click'||eeAction == 'detail')) { appWebEcom[0].item_list_name = ecommerce[eeAction].actionField.list || undefined; } return appWebEcom } } } } catch (error) {} } |
Promotion GA4 Ecommerce object
This variable creates the GA4 Ecommerce object for the following GA4 Events:
- select_promotion
- view_promotion
Name the variable ga4 – ecom – promotion – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // List Enhanced Ecommerce Actions // This could have been listed as an string, but an Array makes it easier to read var eeActionMapping = [ 'promoClick', // Promotion Clicks 'promoView' // Promotion Impressions ] // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (eeActionMapping.toString().indexOf(e) > -1) return e; }); if (eeAction) { var appWebEcom = []; for(var a =0; a < eeAction.length; a++){ if (ecommerce[eeAction].promotions.length>0) { var eeEcom = ecommerce[eeAction].promotions; for(var i =0; i < eeEcom.length; i++){ var item = eeEcom[i]; appWebEcom.push({ 'promotion_id': item.id, 'promotion_name': item.name, 'creative_name': item.creative, 'creative_slot': item.position }); } return appWebEcom } } } } catch (error) {} } |
View Item List GA4 Ecommerce object
This variable creates the GA4 Ecommerce object for the following GA4 Event:
- view_item_list
Name the variable ga4 – ecom – view_item_list – CJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
function(){ try { // Get ecommerce object var ecommerce = {{ecom - ecommerce - DLV}}; // List Enhanced Ecommerce Actions // This could have been listed as an string, but an Array makes it easier to read var eeActionMapping = [ 'impressions' // Product Impressions ] // Object.keys polyfill var getKeys = function(obj){ var keys = []; for(var key in obj){ keys.push(key); } return keys; } // Grab Current EEC Actions var eeAction = getKeys(ecommerce).filter(function (e) { if (eeActionMapping.toString().indexOf(e) > -1) return e; }); if (eeAction) { var appWebEcom = []; for(var a =0; a < eeAction.length; a++){ if (ecommerce[eeAction].length>0) { var eeEcom = ecommerce[eeAction]; for(var i =0; i < eeEcom.length; i++){ var item = eeEcom[i]; appWebEcom.push({ 'item_id': item.id, 'item_name': item.name, 'item_variant': item.variant, 'item_brand': item.brand, 'item_category': (item.category && item.category.split('/')[0]) ? item.category.split('/')[0] : undefined, 'item_category2': (item.category && item.category.split('/')[1]) ? item.category.split('/')[0] : undefined, 'item_category3': (item.category && item.category.split('/')[2]) ? item.category.split('/')[0] : undefined, 'item_category4': (item.category && item.category.split('/')[3]) ? item.category.split('/')[0] : undefined, 'item_category5': (item.category && item.category.split('/')[4]) ? item.category.split('/')[0] : undefined, 'quantity': item.quantity, 'price': item.price, 'item_list_name': item.list, 'coupon': item.coupon }); } return appWebEcom } } } } catch (error) {} } |
GTM Triggers for GA4 Ecommerce
You can maybe reuse many of your existing Enhanced Ecommerce Triggers in GTM (depending of your implementation), but if you in your Enhanced Ecommerce implementation have very generic Triggers (like sending Ecommerce data with PageView), I recommend that you create new Triggers for GA4 Ecommerce, that only Triggers for specific App+Web Events.
In my case I have a Enhanced Ecommerce Custom Event Trigger for impressions. This Trigger is in this Enhanced Ecommerce implementation used for sending impression data both for product lists (impressions) and promotions (promoView). Since I don’t want my App+Web Promotion Impression Tag to be Triggered if the Enhanced Ecommerce object doesn’t contain promotions, I have created a custom Trigger for GA4 promotion impressions as shown below. You should be able to use the same concept for your GA4 Ecommerce custom Triggers.
GTM Tags for GA4 Ecommerce
You will have to create up to 6 different GA4 Ecommerce Tags in GTM depending on your implementation.
If you wonder why currency isn’t a Event Parameter in the setup, that is because currency is set in the Google Analytics: GA4 – Configuration Tag in my setup.
The Tags are listed in the Table below. Use the image of the Ga4 Purchase Tag as a reference to the content in the table.
Tag Name | Event Name | Parameter Name | Parameter Value | App+Web Events |
---|---|---|---|---|
GA - App+Web - Ecom - Purchase | {{app+web - ecom - event - product - CJS}} | transactions_id value tax shipping coupon items | {{{ecom - purchase - Id - DLV}} {{ecom - purchase - revenue - DLV}} {{ecom - purchase - tax - DLV}} {{ecom - purchase - shipping - DLV}} {{ecom - purchase - coupon - DLV}} {{app+web - ecom - product - CJS}} | purchase |
GA - App+Web - Ecom - Checkout | {{app+web - ecom - event - checkout - CJS}} | checkout_step checkout_option items | {{ecom - checkout - step - DLV}} {{ecom - checkout - option - DLV}} {{app+web - ecom - product - CJS}} | checkout |
GA - App+Web - Ecom - Checkout Option | {{app+web - ecom - event - checkout - CJS}} | checkout_step checkout_option | {{ecom - checkout - step - DLV}} {{ecom - checkout - option - DLV}} | set_checkout_option |
GA - App+Web - Ecom - Product | {{app+web - ecom - event - product - CJS}} | items | {{app+web - ecom - product - CJS}} | add_to_cart remove_from_cart select_item view_item |
GA - App+Web - Ecom - View Item List | {{app+web - ecom - event - view_item_list - CJS}} | items | {{app+web - ecom - view_item_list - CJS}} | view_item_list |
GA - App+Web - Ecom - Promotion | {{app+web - ecom - event - promotion - CJS}} | items | {{app+web - ecom - promotion - CJS}} | view_promotion select_promotion |
Some final words
In my existing Enhanced Ecommerce implementation, I have lot’s of Product Scoped Custom Dimensions and Metrics that I would like to send to GA4 as well, but that isn’t supported (yet).
App+Web is in beta, which we can also see in the documentation. There are conflicting/mismatching information between the GTM Implementation documentation, and the Enhanced Ecommerce migration documentation. You will find some differences in App+Web Event namings between those 2 documents. And yes, also other differences exist. In some documentation I have found an discount Item parameter, but I haven’t found discount as an Item parameter in BigQuery.
With other words, I don’t guarantee that I have everything 100% correct.
And finally, some of the concept/code in this setup is inspired by David Vallejo and his APP+WEB Enhanced Ecommerce Traspiler for GTM.
Happy analysing GA4 Ecommerce data based on your existing Enhanced Ecommerce implementation.
Thank you so much! I get the event “undefined” when having a product view.
I used a simple Trigger url contains “product”.
Would you know why?
Thank you for sharing this method. Really helpful!
I’m running into an issue with the events, maybe I missed something.
What happens if multiple events happen on the same pageload? For instance, two view_item_list events are shown, or a detailview and add to cart happen on the same pageload. This returns empty/undefined values for me for the product objects. The datalayer has multiple product objects and ends up returning none of them. You seem to exclude this in your trigger, by setting it to not fire on undefined, but doesn’t this still mean you miss the second event?
In case you read my previous comment, I found a simple solution to include in the js variables. It will only grab the last element in the array when you have multiple pushes. during one pageload. I’m sure you can find a more elegant solution to this though:
if (eeAction.length > 1){
var eeAction = eeAction[eeAction.length – 1];
}