skills/gtm-testing/SKILL.md
Comprehensive GTM tracking testing and validation including automated Playwright headless testing, browser console testing, GTM Preview mode validation, and GA4 DebugView verification. Use when users need to "test GTM tracking", "validate dataLayer events", "debug GTM", "check if tracking works", "automated tracking tests", "run tracking tests without opening browser", or troubleshoot tracking issues. Prioritises automated testing over manual when possible.
npx skillsauth add aimonk2025/google-tag-manager-automation gtm-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide users through comprehensive testing of GTM tracking implementation. Prefers automated headless testing over manual steps wherever possible.
Four-tier validation approach, ordered by automation level:
Always start with Tier 0 if the user asks "can you do it yourself" or wants automated testing. Fall back to manual tiers only for GTM container and GA4 validation, which require a browser session.
Check for gtm-context.md in the project root:
This file is created automatically by gtm-analytics-audit at the end of its first run.
Run this tier first. It requires no browser interaction from the user and can be run entirely by Claude.
# Check if Playwright is installed
npx playwright --version
# If not installed as a project dependency:
npm install --save-dev playwright
npx playwright install chromium
# Check if dev server is running
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000
# If not running, start it:
# npm run dev
This helper intercepts all dataLayer.push() calls during an action and returns the captured events. Use it in every test.
async function captureDataLayerEvents(page, action) {
await page.evaluate(() => {
window.__testEvents = [];
const original = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function (...args) {
window.__testEvents.push(args[0]);
return original(...args);
};
});
await action();
await page.waitForTimeout(300);
return await page.evaluate(() => window.__testEvents || []);
}
Next.js Link components (navigation, course buttons):
Do NOT use page.click() — it triggers navigation before the onClick handler fires. Use dispatchEvent instead:
await page.evaluate(() => {
const btn = document.querySelector('a.js-module_nav');
if (btn) btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
});
await page.waitForTimeout(400);
Outbound links (href to external domains): Block the navigation so the page stays loaded while the event fires:
await page.route('**github.com**', route => route.abort());
// Then use dispatchEvent as above
Regular buttons and internal actions: Standard click works fine:
await page.locator('#element-id').click({ force: true });
Create scripts/test-tracking.js in the project root:
const { chromium } = require('playwright');
const BASE_URL = 'http://localhost:3000';
const results = { passed: [], failed: [], warnings: [] };
function pass(test, detail = '') {
results.passed.push({ test, detail });
console.log(` PASS ${test}${detail ? ' - ' + detail : ''}`);
}
function fail(test, detail = '') {
results.failed.push({ test, detail });
console.log(` FAIL ${test}${detail ? ' - ' + detail : ''}`);
}
function warn(test, detail = '') {
results.warnings.push({ test, detail });
console.log(` WARN ${test}${detail ? ' - ' + detail : ''}`);
}
async function captureDataLayerEvents(page, action) {
await page.evaluate(() => {
window.__testEvents = [];
const original = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function (...args) {
window.__testEvents.push(args[0]);
return original(...args);
};
});
await action();
await page.waitForTimeout(300);
return await page.evaluate(() => window.__testEvents || []);
}
async function runTests() {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
console.log('\n=== GTM dataLayer Event Tests ===\n');
// TEST: dataLayer initialisation
console.log('Test: dataLayer initialisation');
{
const page = await context.newPage();
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
const hasDataLayer = await page.evaluate(() => Array.isArray(window.dataLayer));
hasDataLayer ? pass('dataLayer initialised') : fail('dataLayer not found');
const gtmEvent = await page.evaluate(() => window.dataLayer.find(e => e['gtm.start']));
gtmEvent ? pass('GTM bootstrap event present') : fail('GTM bootstrap event missing - container may not be installed');
await page.close();
}
// TEST: cta_click event
// Adjust selector to match your actual element ID
console.log('\nTest: cta_click event');
{
const page = await context.newPage();
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
const events = await captureDataLayerEvents(page, async () => {
const el = await page.$('#your-cta-id');
if (el) await page.evaluate(el => el.dispatchEvent(new MouseEvent('click', { bubbles: true })), el);
});
const e = events.find(e => e.event === 'cta_click');
e ? pass('cta_click fired', `location=${e.cta_location}`) : fail('cta_click did not fire');
await page.close();
}
// Add more tests following the same pattern...
// RESULTS
await browser.close();
console.log('\n=== RESULTS ===');
console.log(` Passed: ${results.passed.length}`);
console.log(` Failed: ${results.failed.length}`);
console.log(` Warnings: ${results.warnings.length}`);
if (results.failed.length > 0) {
console.log('\nFailed:');
results.failed.forEach(f => console.log(` - ${f.test}: ${f.detail}`));
}
process.exit(results.failed.length > 0 ? 1 : 0);
}
runTests().catch(err => { console.error('Fatal:', err.message); process.exit(1); });
Before writing tests, always inspect what's actually rendered. Use this discovery script:
// Add to test script or run as a separate debug script
const page = await context.newPage();
await page.goto(`${BASE_URL}/your-page`, { waitUntil: 'networkidle' });
const tracked = await page.evaluate(() => {
return Array.from(document.querySelectorAll('[id], .js-track, [data-track]')).map(el => ({
id: el.id,
tag: el.tagName,
class: el.className,
text: el.textContent.trim().substring(0, 40),
href: el.getAttribute('href'),
}));
});
console.log(JSON.stringify(tracked, null, 2));
Before running browser tests, verify the GTM container itself is correctly configured. This catches orphaned trigger references and missing measurement IDs.
// scripts/audit-gtm-coverage.js
// Uses googleapis + gtm-credentials.json + gtm-token.json + gtm-config.json
// Critical checks:
// 1. Base GA4 config tag (googtag) - must fire on Page View trigger
// If firingTriggerId references a non-existent trigger, NO pages are tracked
// 2. GA4 event tags (gaawe) - must have measurementIdOverride set
// Parameter key is "measurementIdOverride" NOT "measurementId"
// 3. All triggers must be used by at least one tag
// 4. All tags must have at least one firing trigger
Critical GTM API bug to watch for: The gaawe (GA4 Event) tag type uses measurementIdOverride and eventSettingsTable (not measurementId or eventParameters). The correct parameter structure is:
// CORRECT structure for gaawe tags via API
{
type: 'gaawe',
parameter: [
{ type: 'boolean', key: 'sendEcommerceData', value: 'false' },
{ type: 'template', key: 'eventName', value: 'your_event_name' },
{ type: 'template', key: 'measurementIdOverride', value: 'G-XXXXXXXXXX' },
{
type: 'list',
key: 'eventSettingsTable',
list: [
{
type: 'map',
map: [
{ type: 'template', key: 'parameter', value: 'param_name' },
{ type: 'template', key: 'parameterValue', value: '{{DL - Variable Name}}' },
],
},
],
},
],
}
node scripts/test-tracking.js
Exit code 0 = all tests passed. Exit code 1 = failures exist.
Result types:
PASS - Event fired with correct parametersFAIL - Event did not fire, or required element not foundWARN - Component has tracking code but is not rendered on any page yet (not a bug)Common failure causes and fixes:
| Failure | Cause | Fix |
|---------|-------|-----|
| dataLayer not found | GTM snippet missing from <head> | Add GTM container snippet to layout |
| Event didn't fire | Element uses wrong selector in test | Use discovery script to find real ID/class |
| Event fired but page unloaded | Clicking a Next.js <Link> | Use dispatchEvent not page.click() |
| Outbound click not captured | Page navigated away before capture | Use page.route() to block external navigation |
| No events on page load | GTM base tag has orphaned trigger reference | Fix via GTM API or UI - re-assign to Page View trigger |
Use when you want to manually verify events while interacting with the site.
window.dataLayer
// Expected: [...] array
// If undefined: GTM not installed
const _push = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function(...args) {
console.log('%c dataLayer.push', 'background:#222;color:#0f0;padding:2px 6px', args[0]);
return _push(...args);
};
// Now every push is logged in green
Click elements one at a time and verify the console output matches expected:
Expected for cta_click:
{
event: "cta_click",
cta_location: "hero",
cta_type: "primary",
cta_text: "Start Course",
cta_destination: "/claude-code"
}
window.dataLayer is an arraygtm.start) presentevent nameConfirms the GTM container receives events and fires tags. Requires GTM UI access.
For each event, the Preview panel should show:
Event fired: "cta_click"
Triggers: CE - CTA Click [FIRED]
Tags: GA4 - CTA Click [FIRED]
Click a fired tag to verify:
measurementIdOverride has the correct GA4 ID{{DL - ...}} strings)Confirms events reach GA4 with correct parameters. Requires GA4 property access.
Option A - URL parameter: append ?debug_mode=true to the page URL
Option B - Chrome extension: install Google Analytics Debugger, enable it
GA4 > Admin > Property > DebugView
Events appear in real time as you interact with the site.
cta_click)page_view event fires on every page navigationpage_view events firing on all pages| Issue | Tier Found | Cause | Fix |
|-------|-----------|-------|-----|
| dataLayer undefined | 1 | GTM snippet missing | Add GTM snippet to <head> |
| Event fires, trigger doesn't | 2 | Event name mismatch (case-sensitive) | Confirm trigger uses {{_event}} equals exact event name |
| Trigger fires, tag doesn't | 2 | Orphaned trigger or misconfigured tag | Check tag has valid firing trigger assigned |
| Page View tag never fires | 2 | Orphaned trigger reference on base tag | Re-assign base tag to a valid Page View trigger |
| Parameters show as {{DL - X}} | 2 | Data Layer Variable not created | Create DLV with correct dataLayer key name |
| Events in Preview, not DebugView | 3 | Wrong GA4 property or debug mode off | Verify Measurement ID matches, enable debug mode |
| Parameters missing in GA4 | 3 | Not mapped in tag's eventSettingsTable | Add parameter mapping in GTM tag config |
1. Run Tier 0 (automated)
PASS → proceed to publish GTM
FAIL → fix code or GTM config, re-run
2. Run Tier 1 (console) — optional manual spot-check
PASS → proceed
FAIL → fix before Tier 2
3. Run Tier 2 (GTM Preview)
PASS → proceed
FAIL → fix GTM tags/triggers/variables
4. Run Tier 3 (GA4 DebugView)
PASS → publish GTM container
FAIL → fix GA4 config or parameter mappings
5. Publish GTM → GTM UI > Submit > Publish version
6. Disable debug mode in production
7. Monitor GA4 Reports > Engagement > Events
references/debugging-guide.md - Extended issue diagnosisreferences/test-checklist.md - Printable checklist templateexamples/sample.md - Example test run output showing PASS/FAIL/WARN format and common failure fixesgtm-implementation, gtm-analytics-audit, gtm-reportingdevelopment
Strategic GTM tracking planning with product manager expertise. Use when users need to plan tracking strategy, define what metrics to measure, understand business impact of tracking, create tracking specifications, or need guidance on "what should I track?" questions. Asks discovery questions about business goals, maps objectives to events, defines event taxonomy, and creates structured tracking plans. Trigger on - "plan GTM tracking", "what should I track", "create tracking plan", "define measurement strategy", "GTM strategy".
development
Pipeline status check for GTM projects. Use when returning to a project mid-implementation, when unsure what step comes next, or to get a quick overview of what has been completed. Reads all GTM output files and shows which skills have run, current implementation coverage, and the recommended next step. No API calls, instant. Trigger on - "what step am I on", "gtm status", "where did I leave off", "what's been done", "check progress", "pipeline status".
development
Automates Google Tag Manager API setup including googleapis installation, OAuth credential creation, token management, and prerequisites validation. Use when users need to "set up GTM API", "configure GTM API access", "get GTM OAuth credentials", "install googleapis", or encounter authentication errors. Handles complete technical setup from dependency installation through API connection verification.
development
Generates GTM implementation documentation, reporting impact analysis, GA4 report configurations, and stakeholder summaries. Use when users need to "document GTM implementation", "what reports can I build", "create event schema docs", "generate stakeholder summary", "analyze reporting impact", or want to understand business value of tracking data. Creates technical documentation, suggests GA4 explorations, defines remarketing audiences, and translates technical events into business insights.