analytics-tracking-testing

Validate that analytics and marketing tracking fire CORRECTLY: GA4/GTM dataLayer events, Meta/TikTok/LinkedIn pixels, and ad-tech tags. Covers building a tracking plan as the contract, intercepting collect-endpoint beacons and dataLayer.push in Playwright, asserting event name + params + values + ti

By petrkindlmann · 581 installs

npx skills add petrkindlmann/qa-skills --skill analytics-tracking-testing

Source repository · Upstream listing

<objective Tracking that "looks fine" in GA4 DebugView still drops events silently after a refactor, sends the wrong currency, or double counts a purchase. Reading window.dataLayer or asserting the button is toBeVisible() proves nothing — the beacon may never leave the browser. This skill makes you intercept the real network beacon ( google analytics.com/g/collect , facebook.com/tr , analytics.tiktok.com , px.ads.linkedin.com ), parse the event name and parameters out of the request, and assert them against a tracking plan that is a typed contract — then gate that contract in CI so a dropped event fails the build. </objective Quick Route Situation Go to Assert one GA4 event fired with correct params [Intercepting GA4 beacons]( intercepting ga4 beacons) Treat the tracking plan as a validated contract [The Tracking Plan Is the Contract]( the tracking plan is the contract) Verify dataLayer.push shape (not the beacon) [Asserting dataLayer.push]( asserting datalayerpush) Pixel + server CAPI deduplication (event id) [Pixels and Server Side Deduplication]( pixels and server side deduplication) Events must/mustn't fire by consent state [Consent Mode v2 Gating]( consent mode v2 gating) Capture every pixel in one reusable fixture references/capture fixture.md News media: article view, scroll depth, paywall [News Media Events]( news media events) Fail the build on a tracking regression [Regression Gating in CI]( regression gating in ci) Hundreds of events, many domains, small team [Buy vs Build]( buy vs build) Discovery Questions First: check .agents/qa project context.md in the project root and skip anything it already answers (stack, tag manager, consent platform, target environments). Which tracking destinations are live? GA4 ( /g/collect ), Meta Pixel ( facebook.com/tr ), TikTok ( analytics.tiktok.com ), LinkedIn ( px.ads.linkedin.com ), ad tech tags — each has a different endpoint grammar, so the capture helper must know them all. GTM or hardcoded gtag? GTM means the truth flows through window.dataLayer.push first, then GTM fires the beacon. You may assert at the push layer (input contract) AND the beacon (output contract); they are different tests. Is there a tracking plan? If not, build one first — it is the contract everything else validates against. No plan means no objective pass/fail. Consent platform and default consent state? Consent Mode v2 changes which beacons are even allowed to fire before consent. You need the CMP's accept/reject selectors to drive the test. Client + server (CAPI) dedup in play? If purchases fire both client Pixel and server Conversions API, the event id must match or you double count. This is the correctness property, not "did fbq run." News media surface? Article pages add article view, scroll depth thresholds (25/50/75/100), and paywall meter events that generic e commerce plans miss. Core Principles 1. Intercept the beacon, never trust the DOM or dataLayer alone. A button click that updates the DOM, or a dataLayer.push that GTM silently drops, leaves no GA4 hit. The only proof an event was sent is the outbound request to the collect endpoint. Assert on the network beacon's URL params; reading window.dataLayer via page.evaluate only proves the push happened, not that anything left the browser. 2. The tracking plan is a typed contract, not a comment. Every expected event lives in an external schema (JSON/YAML/TS interface) with required params and their types. Tests validate captured events against it and report missing params and type violations . Hardcoding one expected value inline asserts nothing about the other twenty params and rots on the first plan change. 3. De duplication is the real correctness property for purchases. Checking that fbevents.js loaded or counting that fbq('track','Purchase') ran misses double counting entirely. The property that matters: the browser Pixel and the server CAPI send the same event id / eventID so Meta collapses them into one conversion. 4. Consent state is an input dimension, not a footnote. The same page produces different beacons before vs after consent. Test both: before consent → no beacon (or only a cookieless consent ping); after accept → full beacon. And the default must be denied for all four Consent Mode v2 signals — a granted default is a compliance bug AND makes the gating test meaningless. 5. Drive real user actions and wait on requests, never the clock. Scroll depth fires from actual scrolling ( mouse.wheel , scrollIntoView , evaluate(scrollTo) ), and you wait for the beacon with waitForRequest , not waitForTimeout . A fixed sleep is flaky and hides the very timing bug you should catch. 6. A tracking test that can't fail the build is theater. "Check it in GA4 DebugView" and "monitor production" never block a regression. The contract must run in CI and exit non zero when an event drops or a required param goes missing. Intercepting GA4 Beacons GA4 (gtag.js / GTM) sends every event as an HTTP request to https://www.google analytics.com/g/collect (region variants like region1.google analytics.com/g/collect also occur). The event identity lives in the URL query string — you do not need the response. GA4 /g/collect URL grammar you assert on: Param Meaning Example v=2 Measurement Protocol version (always 2 for GA4) v=2 tid=G XXXXXXX Measurement ID tid=G ABC123 en= Event name en=add to cart ep.<name = Event parameter, string type ep.currency=USD epn.<name = Event parameter, number type epn.value=49.99 gcs= / gcd= Consent state (see Consent Mode v2 section) gcs=G111 The string/number split is load bearing: GA4 types params automatically, so price arrives as epn.value (number) and currency as ep.currency (string). Asserting ep.value when it is really epn.value silently fails. Intercept with page.waitForRequest (single expected event), page.on('request', ...) (collect many), or page.route (inspect then continue — never abort ). Parse params from new URL(request.url()).searchParams . Then expect(...).toBe(...) / toEqual / toContain on the parsed values. Minimal pattern (full version with helper in references/ga4 interception.md ): Never substitute page.evaluate(() = window.dataLayer) as the only assertion, toBeVisible() on the button, or waitForTimeout() to "let the beacon send." See references/ga4 interception.md for batched event parsing (GA4 can pack multiple events into one POST body) and region endpoint handling. The Tracking Plan Is the Contract A tracking plan is the source of truth: for every event, its name, required params, and each param's type. Keep it as a versioned file ( tracking plan.json / .yaml , or a TS interface / Zod schema) that both the app team and the tests import. Tests read the captured event and validate it against the plan — they do not hardcode expected values inline. Validation produces a structured result, not a pass/fail boolean: list every missing required param and every type mismatch / violation . Example plan entry and validator: Then expect(validateEvent(plan, 'add to cart', params)).toEqual([]) . Asserting only the event name and ignoring params, or hardcoding expected values with no plan file, is the bare agent shortcut this skill exists to replace. See references/tracking plan.md for the YAML form, a Zod typed plan, and a reusable assertAgainstPlan matcher. Asserting dataLayer.push When the question is specifically the GTM input — "is the right object pushed to dataLayer when the page loads?" — assert the push, not the GA4 beacon. This is the inverse of beacon interception: here the dataLayer push IS the target. Capture pushes by wrapping window.dataLayer.push in addInitScript before navigation so you record every push from page load, then read the recorded array via page.evaluate . Do not page.route to mock the dataLayer (you would replace the thing under test), and do not read the GA4 network beacon instead (that is the output, a different contract). For ecommerce, assert the nested shape, not just the event name — the ecommerce.items array and each item's item id , price , currency : Use find / filter / some to locate the event in the recorded pushes. Full helper in references/datalayer capture.md . Pixels and Server Side Deduplication Marketing pixels send their own beacons. Endpoints to intercept: Destination Endpoint Event param Dedup key Meta Pixel facebook.com/tr (also /tr? ) ev=PageView , ev=Purchase eid / event id TikTok analytics.tiktok.com event in body/params event id LinkedIn px.ads.linkedin.com conversion id — For Meta, asserting that connect.facebook.net/en US/fbevents.js loaded, or that fbq('track','Purchase') ran, is a load/count check — it does not prove correctness. The correctness property for a Purchase that fires both client side (Pixel) and server side (Conversions API / CAPI) is deduplication : both must carry the same event id so Meta merges them into one conversion instead of double counting. Test it: capture the browser facebook.com/tr beacon for ev=Purchase , read its event id , and assert it equals the event id your server sent to CAPI (from a mocked/captured server call or a known fixture value). Skeleton: See references/pixels and dedup.md for parsing TikTok/LinkedIn payloads and capturing the server CAPI call. Consent Mode v2 Gating Consent Mode v2 is the standard (mandatory four signal model). As of the June 15 2026 change, Google acts only on the CMP sent consent signal, so any two signal answer is outdated. Test two states. The four signals — all must default to denied : Signal Governs ad storage Advertising cookies analytics storage Analytics cookies ad user data Sending user data to Google for ads ad personalization Personalized ads / remarketing A granted default is a bug; omitting ad user data and ad personalization (the v2 additions) is the outdated two signal model and is wrong. Before consent: no full beacon should fire — or only a cookieless consent ping . Use addInitScript to seed the gtag('consent', 'default', {...}) denied state before page scripts run, and assert no /g/collect request fires (or that the one that does carries a denied consent state). After accept: click the CMP accept button; the full beacon now fires. The consent state rides on the beacon URL: gcs= — encodes ad storage + analytics storage only. G100 = both denied, G111 = both granted, G110 / G101 = partial. Before consent you expect gcs=G100 . gcd= — encodes all four signals (string starting 11... ); present on every hit to Google services. Assert the denied default and the gcs= / gcd= value on the pre consent beacon, then the granted state post accept. Full test with addInitScript consent seeding and CMP click in references/consent mode.md . Whether the law permits a beacon under a given consent state is compliance testing . This skill asserts that when a beacon fires, its data and consent params are correct. News Media Events News and publisher sites have a tracking surface generic e commerce plans miss. Cover all three: article view (or article view ) on article load — assert the beacon fires once with article metadata (id, section, author). scroll depth at the 25 / 50 / 75 / 100 percent thresholds — one event per bucket, driven by real scrolling . paywall / meter — a paywall hit (or meter) event when the free article meter is exhausted. Drive scroll with actual actions — mouse.wheel , element.scrollIntoView , or page.evaluate(() = window.scrollTo(...))