Debug GTM without developers — log dataLayer.push in 30 seconds
The marketing analyst asks: "did this event fire?". The developer says: "probably, check GTM Preview". GTM Preview, however, shows different data than production. Four JS snippets that surface raw dataLayer.push straight into the console — without touching production code.
Why standard tooling falls short
Google Tag Manager Preview works great, but has three limits:
- Not all events show up — some triggers fire in late phases, GTM Preview catches only some.
- Requires login as a container editor — freelance analysts often don't have access.
- Doesn't show raw objects — you see the event name and parameters, but not the full JSON that landed in
dataLayer.
Your solution: inject JS that wraps dataLayer.push and logs everything. Without site code changes, no deploy, no review.
Method 1 — basic logger
Simplest version. Wraps dataLayer.push and dumps every call into the console:
// Wait for GTM init
window.dataLayer = window.dataLayer || [];
const origPush = window.dataLayer.push;
window.dataLayer.push = function(...args) {
console.log(
'%c[GTM]', 'color:#16a34a;font-weight:bold;font-size:13px',
args
);
return origPush.apply(window.dataLayer, args);
};
console.log('[GTM] dataLayer logger active');
You open DevTools, click anything on the page (an "Add to cart" button, login, scroll), and the console shows:
[GTM] [{event: 'add_to_cart', ecommerce: {items: [...]}}]
[GTM] [{event: 'gtm.click', gtm.element: button, ...}]
Every push in full form. Exactly what GTM sees.
Method 2 — filter only events you care about
The method 1 logger produces a lot of noise (GTM itself pushes gtm.dom, gtm.load, gtm.click etc). If you're debugging a specific flow — say e-commerce purchase — filter:
const TARGET_EVENTS = [
'purchase', 'add_to_cart', 'begin_checkout',
'view_item', 'select_item', 'add_payment_info'
];
window.dataLayer = window.dataLayer || [];
const origPush = window.dataLayer.push;
window.dataLayer.push = function(...args) {
const eventName = args[0]?.event;
if (TARGET_EVENTS.includes(eventName)) {
console.group(
`%c[GTM] ${eventName}`,
'color:#16a34a;font-weight:bold;font-size:14px'
);
console.log('Payload:', args[0]);
if (args[0].ecommerce) {
console.table(args[0].ecommerce.items);
}
console.groupEnd();
}
return origPush.apply(window.dataLayer, args);
};
console.group creates a collapsible section, console.table renders items as a table with columns — nicer than raw JSON. Perfect for e-commerce events with many products.
Method 3 — intercept gtag()
Some integrations (GA4 setup) use the gtag() function instead of direct dataLayer pushes. Under the hood it's the same, but you have to catch it differently:
// Wait for gtag to become available
function wrapGtag() {
if (typeof window.gtag !== 'function') {
setTimeout(wrapGtag, 100);
return;
}
const origGtag = window.gtag;
window.gtag = function(...args) {
console.log(
'%c[gtag]', 'color:#2563eb;font-weight:bold',
args[0] /* 'event' | 'config' | 'set' */,
args.slice(1)
);
return origGtag.apply(this, args);
};
console.log('[gtag] logger active');
}
wrapGtag();
Polls every 100ms until gtag is defined (GTM loads it async). Once found — wraps. Logs every gtag('event', '...', {...}) and gtag('config', '...').
Method 4 — visual toast feedback
The console is fine, but sometimes you want to see events without opening DevTools — say, demonstrating to a client that tracking works. Floating toast in the top-right corner:
// Toast container
const container = document.createElement('div');
container.style.cssText = `
position: fixed; top: 16px; right: 16px;
z-index: 999999; display: flex; flex-direction: column;
gap: 6px; max-width: 320px;
`;
document.body.appendChild(container);
function showToast(text) {
const t = document.createElement('div');
t.style.cssText = `
background: #16a34a; color: white;
padding: 8px 12px; border-radius: 6px;
font: 12px ui-monospace, monospace;
box-shadow: 0 4px 12px rgba(0,0,0,.2);
`;
t.textContent = text;
container.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
const origPush = window.dataLayer.push;
window.dataLayer.push = function(...args) {
const name = args[0]?.event || 'unknown';
showToast('GTM: ' + name);
return origPush.apply(window.dataLayer, args);
};
Each event → green toast top-right, disappears in 3 seconds. The client watches the flow, sees events "firing" — credibility without DevTools.
Pitfalls worth avoiding
- Timing vs GTM initialisation — if your wrapper loads after the first GTM events (e.g.
gtm.dom), those won't be logged. Inject as early as possible (JustZix runs before DOMContentLoaded — usually enough). - dataLayer.push can be replaced by another tag in GTM — if someone overrode the method later, your wrap breaks. Check
window.dataLayer.push.toString()in the console — it should contain your code. - Verbose console.log of large objects can slow the browser in SPA contexts with 1000+ events. In production, limit:
if (args[0].event === 'purchase') .... - Don't commit the toggle to production — JustZix is a dev tool, not an analytics extension. Disable the rule after debugging, don't leave it on permanently.
How to plug into JustZix
- Install JustZix.
- Create a "GTM debug" folder.
- Per-environment rule: URL pattern
https://mystore.com/*andhttps://staging.mystore.com/*, JavaScript = Method 1 or 2. - Floating button → toggle the rule when you need it. Disable after the debug session.
- Sync: paste the sync key on a second device — your marketing colleague also has the logger one click away.
What's next
The same pattern (wrap an existing global function + log) lets you log fetch, XMLHttpRequest, console.error, framework callbacks. See Examples → JavaScript and Use cases → analytics debugging.
Install JustZix and have developer tools loaded on every site, without changes to production code.
Rate this post
No ratings yet — be the first.