Universal Ecommerce Tracking for GHL with a Custom Data Layer
tags: ghl, gtm, ecommerce, tracking, ga4, google tag manager, data layer
GoHighLevel (GHL) is a fantastic platform for marketers, but it has a notable gap: it doesn’t provide a native ecommerce data layer. This makes it challenging to implement accurate conversion tracking with tools like Google Analytics 4 (GA4), Facebook Pixel, or other advertising platforms. Without a standardized data layer, you’re often left with manual, brittle solutions that are hard to scale and maintain.
This article provides a robust, copy-paste solution: a custom JavaScript snippet that acts as a universal ecommerce data formatter. It reads the purchase data GHL saves in the browser’s local storage and transforms it into a standard GA4-compatible ecommerce object. We’ll then use Google Tag Manager (GTM) to deploy this script and create a custom trigger, enabling you to fire your tracking tags reliably on every purchase.
The Problem: No Standard Ecommerce Data Layer in GHL
When a customer completes a purchase in a GHL funnel, the order details are saved locally in their browser. However, this information isn’t exposed in a way that analytics tools can easily consume. The consequences are:
- Inaccurate Reporting: You can’t easily track revenue, transaction IDs, or product details.
- Complex Tagging: Setting up conversion tags in GTM becomes a messy process of DOM scraping or custom JavaScript for every small piece of data.
- Poor Scalability: As you add more tracking pixels or analytics tools, the complexity multiplies.
The ideal solution is to create a standardized dataLayer object that all your tags can reference.
The Solution: A Universal Data Layer Formatter
The script below is designed to be placed in a Custom HTML tag within GTM. It inspects GHL’s local storage for order information, formats it into a GA4-compliant structure, and pushes it to the dataLayer.
Here is the full snippet:
<script>// Initialize dataLayer if it doesn't existwindow.dataLayer = window.dataLayer || [];
function getDataLayerPayload(data) { var event = data.event; var items = data.items; var currency = data.currency; var transaction = typeof data.transaction !== 'undefined' ? data.transaction : {}; var totalValue = items.reduce(function(sum, item) { return sum + (item.price * item.quantity); }, 0);
var ecommerce = { currency: currency, items: items };
if (event === 'purchase') { Object.assign(ecommerce, { transaction_id: transaction.id, affiliation: transaction.affiliation, value: transaction.total, tax: transaction.tax, shipping: transaction.shipping, coupon: transaction.coupon }); } else { // For view_item, begin_checkout, add_to_cart ecommerce.value = totalValue; }
return { event: event, ecommerce: ecommerce };}
// --- EVENT FUNCTIONS ---
function pushPurchaseFromLocalStorage(funnelId) { if (!funnelId) { console.error('Funnel ID is required to fetch order details from local storage.'); return; }
var lsKey = '_pl_' + funnelId; var orderDetailsString = localStorage.getItem(lsKey);
if (!orderDetailsString) { console.warn('No order details found in local storage with key: ' + lsKey); return; }
var orderDetails = JSON.parse(orderDetailsString);
// 1. Transform GHL products into GA4 items var orderItems = orderDetails.products.map(function(product) { return { item_id: product.product._id, item_name: product.name, price: product.amount, quantity: product.qty }; });
// 2. Extract currency (assuming all items have the same currency) var orderCurrency = 'USD'; // Default currency if (orderDetails.products && orderDetails.products.length > 0 && orderDetails.products[0] && orderDetails.products[0].price && orderDetails.products[0].price.currency) { orderCurrency = orderDetails.products[0].price.currency; }
// 3. Create the GA4 transaction object var transaction = { id: orderDetails.orderIds[0], total: orderDetails.total, tax: orderDetails.tax.reduce(function(sum, taxItem) { return sum + (taxItem.amount || 0); }, 0), shipping: 0, // GHL object doesn't seem to provide shipping. Set to 0 or a default. coupon: '' // GHL object doesn't seem to provide coupon. Set to empty. };
// 4. Build and push the payload var payload = getDataLayerPayload({ event: 'purchase', items: orderItems, currency: orderCurrency, transaction: transaction });
window.dataLayer = window.dataLayer || []; window.dataLayer.push(payload); console.log('DataLayer Push from Local Storage: purchase', window.dataLayer);}
// Push the order details to data layer, assuming `window.funnelId` is availablepushPurchaseFromLocalStorage(window.funnelId);</script>How It Works
getDataLayerPayload: This is a helper function that constructs the final object to be pushed to thedataLayer. It’s designed to be flexible for different ecommerce events (purchase,view_item, etc.), but here we focus onpurchase.pushPurchaseFromLocalStorage: This is the core function.- It takes a
funnelIdas an argument. GHL stores order data in a local storage key that includes the funnel ID (e.g.,_pl_xxxx). - It retrieves and parses the order details JSON string from local storage.
- It maps the GHL
productsarray to the GA4itemsarray format. - It extracts key transaction details like
transaction_id,total, andtax. Note that shipping and coupon data are not readily available in the GHL object, so they are set to default values. - Finally, it pushes a
purchaseevent to thedataLayerwith the fully formedecommerceobject.
- It takes a
- Execution: The script calls
pushPurchaseFromLocalStorage(window.funnelId)to run the logic as soon as the GTM tag is fired. You must ensurewindow.funnelIdis available on the page where this runs (typically the order confirmation page).
Implementation with Google Tag Manager
Now, let’s deploy this script using GTM.
Step 1: Create a Custom HTML Tag
- In your GTM container, go to Tags > New.
- Give your tag a descriptive name, like
Custom HTML - GHL Ecommerce Formatter. - Click on Tag Configuration and choose Custom HTML.
- Copy and paste the entire JavaScript snippet (including the
<script>tags) into the HTML field. - Save the tag.
Step 2: Create a Custom Event Trigger
We don’t want this script to run on every page. It should only run when a purchase actually happens. To achieve this, we’ll create a trigger that listens for a custom event that we will fire from the GHL order confirmation page.- While still in the tag editor, click on Triggering.
- Click the
+icon in the top right to create a new trigger. - Name the trigger, for example,
Custom Event - ghl_purchase. - For Trigger Configuration, choose Custom Event.
- In the Event name field, enter
ghl_purchase. This name must match exactly what you’ll use on your website. - Leave “All Custom Events” selected.
- Save the trigger. Your Custom HTML tag should now be linked to this new trigger.
Triggering the Purchase Event from GHL
The final step is to tell GTM when to fire our tag. On your GHL order confirmation or “thank you” page, you need to add a small piece of code. This code pushes the ghl_purchase event to the dataLayer, which our GTM trigger is listening for.
You can add this code in the <body> or <footer> section of your GHL funnel page settings:
<script> window.dataLayer = window.dataLayer || []; window.dataLayer.push({ 'event': 'ghl_purchase' });</script>With this setup, the flow is as follows:
- A user lands on the order confirmation page.
- The small snippet fires, pushing
ghl_purchaseto thedataLayer. - GTM detects this event and fires our “Custom HTML - GHL Ecommerce Formatter” tag.
- The formatter script runs, reads the local storage, builds the ecommerce object, and pushes the
purchaseevent to thedataLayer. - Now, all your other tags (GA4, Facebook, etc.) can be triggered by the
purchaseevent and can use the rich data in thedataLayer.
Bonus: User Identification for Enhanced Tracking
To get a complete picture of your customer journey, you often need to identify users with a stable identifier like an email address. GHL also stores user information in local storage. You can create a GTM variable to safely extract this information.
Create a Custom JavaScript Variable in GTM
- In GTM, go to Variables > User-Defined Variables > New.
- Name the variable
cjs - Local Storage User Email. - Choose Custom JavaScript as the variable type.
- Paste the following code into the field:
function () { var LSraw = localStorage.getItem('_ud'); if (!LSraw) { return ""; }
try { var LSparsed = JSON.parse(LSraw); var LSemail = LSparsed.email; if (!LSemail) { return ""; } return LSemail; } catch (e) { return ""; }}This function safely reads the _ud key from local storage, parses it, and returns the user’s email if it exists. If anything fails, it returns an empty string. You can now use this GTM variable ({{cjs - Local Storage User Email}}) in any of your tags to send user-specific data to your analytics platforms.
Conclusion
By implementing this custom data layer formatter, you bridge a critical gap in GHL’s marketing capabilities. This GTM-based solution is:
- Centralized: Manage your ecommerce tracking logic in one place.
- Scalable: Easily add new tracking pixels that can all use the same standardized data.
- Reliable: It’s based on GHL’s own local storage data, which is more robust than scraping HTML elements.
This approach empowers you to build a professional-grade analytics setup on GHL, giving you the data you need to make smarter marketing decisions.