analytics-tracking-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<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>
<objective> 在重构后,GA4 DebugView中看似正常的追踪仍可能静默丢失事件、发送错误货币类型或重复统计购买行为。读取`window.dataLayer`或断言按钮`toBeVisible()`无法证明任何问题——信标可能从未离开浏览器。本技能可让你拦截真实的网络信标(`google-analytics.com/g/collect`、`facebook.com/tr`、`analytics.tiktok.com`、`px.ads.linkedin.com`),从请求中解析事件名称与参数,并将其作为类型化契约与追踪计划进行比对,随后在CI中启用该契约管控,确保事件丢失会导致构建失败。 </objective>

Quick Route

快速导航

SituationGo to
Assert one GA4 event fired with correct paramsIntercepting GA4 beacons
Treat the tracking plan as a validated contractThe Tracking Plan Is the Contract
Verify dataLayer.push shape (not the beacon)Asserting dataLayer.push
Pixel + server CAPI deduplication (event_id)Pixels and Server-Side Deduplication
Events must/mustn't fire by consent stateConsent Mode v2 Gating
Capture every pixel in one reusable fixture
references/capture-fixture.md
News-media: article-view, scroll-depth, paywallNews-Media Events
Fail the build on a tracking regressionRegression-Gating in CI
Hundreds of events, many domains, small teamBuy vs Build
场景跳转至
断言单个GA4事件触发并携带正确参数拦截GA4信标
将追踪计划作为已验证契约使用追踪计划即契约
验证dataLayer.push的结构(非信标)断言dataLayer.push
像素与服务器CAPI去重(event_id)像素与服务器端去重
根据同意状态控制事件是否触发Consent Mode v2管控
在可复用夹具中捕获所有像素
references/capture-fixture.md
新闻媒体场景:article-view、scroll-depth、paywall新闻媒体事件
追踪回归时触发构建失败CI中的回归管控
数百个事件、多域名、小团队场景【自研vs采购】](#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.

首先:检查项目根目录下的
.agents/qa-project-context.md
,跳过已涵盖的内容(技术栈、标签管理器、同意平台、目标环境)。
  • 当前启用了哪些追踪目标? GA4(
    /g/collect
    )、Meta Pixel(
    facebook.com/tr
    )、TikTok(
    analytics.tiktok.com
    )、LinkedIn(
    px.ads.linkedin.com
    )、广告技术标签——每个目标的端点语法不同,因此捕获工具需适配所有类型。
  • 使用GTM还是硬编码gtag? 使用GTM意味着数据先通过
    window.dataLayer.push
    流转,再由GTM触发信标。你可以在推送层(输入契约)和信标层(输出契约)分别进行断言,这是两种不同的测试。
  • 是否存在追踪计划? 如果没有,需先制定——它是所有验证工作的依据。没有计划就没有客观的通过/失败标准。
  • 使用的同意平台与默认同意状态? Consent Mode v2会改变同意前允许触发的信标类型。你需要获取CMP的同意/拒绝选择器来驱动测试。
  • 是否启用了客户端+服务器(CAPI)去重?** 如果购买行为同时触发客户端像素与服务器转化API,则
    event_id
    必须匹配,否则会重复统计。这是正确性验证的核心,而非仅验证
    fbq
    是否运行。
  • 是否为新闻媒体场景? 文章页面会新增article-view、滚动深度阈值(25/50/75/100%)以及付费墙计量事件,这些是通用电商计划未覆盖的内容。

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.

  1. 拦截信标,绝不单独信任DOM或
    dataLayer
    。更新DOM的按钮点击,或被GTM静默丢弃的
    dataLayer.push
    ,都不会产生GA4命中。事件已发送的唯一证明是发往收集端点的出站请求。需断言网络信标的URL参数;通过
    page.evaluate
    读取
    window.dataLayer
    仅能证明推送行为发生,无法证明数据已离开浏览器。
  2. 追踪计划是类型化契约,而非注释。每个预期事件都存储在外部 schema(JSON/YAML/TS接口)中,包含必填参数及其类型。测试会将捕获的事件与计划进行比对,并报告
    缺失
    的参数和类型
    违规
    情况。硬编码单个预期值无法验证其他二十个参数,且会在计划首次变更后失效。
  3. 去重是购买行为正确性的核心验证点。检查
    fbevents.js
    是否加载或统计
    fbq('track','Purchase')
    的运行次数,无法发现重复统计问题。真正关键的验证点是:浏览器像素与服务器CAPI发送相同
    event_id
    /
    eventID
    ,确保Meta将其合并为一次转化。
  4. 同意状态是输入维度,而非附属说明。同一页面在同意前后会产生不同的信标。需测试两种状态:同意前→无信标(或仅发送无Cookie的同意请求);同意后→完整信标。且Consent Mode v2的四个信号必须默认设置为
    denied
    ——默认
    granted
    既是合规漏洞,也会使管控测试失去意义。
  5. 模拟真实用户操作并等待请求,绝不依赖固定延迟。滚动深度事件需由真实滚动操作触发(
    mouse.wheel
    scrollIntoView
    evaluate(scrollTo)
    ),并通过
    waitForRequest
    等待信标,而非
    waitForTimeout
    。固定延迟不仅不稳定,还会掩盖本应捕获的时序问题。
  6. 无法触发构建失败的追踪测试毫无意义。「在GA4 DebugView中检查」和「监控生产环境」无法阻止回归。契约必须在CI中运行,当事件丢失或必填参数缺失时返回非零退出码。

Intercepting GA4 Beacons

拦截GA4信标

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:
ParamMeaningExample
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
):
ts
test('add_to_cart fires with correct params', async ({ page }) => {
  await page.goto('/product/42');
  const [request] = await Promise.all([
    page.waitForRequest(r => r.url().includes('/g/collect') && r.url().includes('en=add_to_cart')),
    page.getByRole('button', { name: 'Add to cart' }).click(),
  ]);
  const params = new URL(request.url()).searchParams;
  expect(params.get('v')).toBe('2');
  expect(params.get('tid')).toContain('G-');
  expect(params.get('en')).toBe('add_to_cart');
  expect(params.get('ep.currency')).toBe('USD');
  expect(Number(params.get('epn.value'))).toBe(49.99);
});
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.

GA4(gtag.js / GTM)通过HTTP请求将每个事件发送至
https://www.google-analytics.com/g/collect
(也存在
region1.google-analytics.com/g/collect
等区域变体)。事件标识存储在URL查询字符串中——无需关注响应内容。
需断言的GA4
/g/collect
URL语法:
参数含义示例
v=2
测量协议版本(GA4始终为2)
v=2
tid=G-XXXXXXX
测量ID
tid=G-ABC123
en=
事件名称
en=add_to_cart
ep.<name>=
事件参数,字符串类型
ep.currency=USD
epn.<name>=
事件参数,数字类型
epn.value=49.99
gcs=
/
gcd=
同意状态(见Consent Mode v2章节)
gcs=G111
字符串/数字的区分至关重要:GA4会自动为参数分配类型,因此
price
epn.value
(数字)形式传递,
currency
ep.currency
(字符串)形式传递。若实际为
epn.value
却断言
ep.value
,会导致静默失败。
可通过
page.waitForRequest
(单个预期事件)、
page.on('request', ...)
(捕获多个事件)或
page.route
(检查后继续——切勿
abort
)进行拦截。通过
new URL(request.url()).searchParams
解析参数,随后使用
expect(...).toBe(...)
/
toEqual
/
toContain
对解析后的值进行断言。
最简实现模式(完整带工具类版本见
references/ga4-interception.md
):
ts
test('add_to_cart fires with correct params', async ({ page }) => {
  await page.goto('/product/42');
  const [request] = await Promise.all([
    page.waitForRequest(r => r.url().includes('/g/collect') && r.url().includes('en=add_to_cart')),
    page.getByRole('button', { name: 'Add to cart' }).click(),
  ]);
  const params = new URL(request.url()).searchParams;
  expect(params.get('v')).toBe('2');
  expect(params.get('tid')).toContain('G-');
  expect(params.get('en')).toBe('add_to_cart');
  expect(params.get('ep.currency')).toBe('USD');
  expect(Number(params.get('epn.value'))).toBe(49.99);
});
切勿仅用
page.evaluate(() => window.dataLayer)
作为断言依据,或仅断言按钮
toBeVisible()
,或使用
waitForTimeout()
来「等待信标发送」。如需批量事件解析(GA4可将多个事件打包进一个POST请求体)和区域端点处理,请参考
references/ga4-interception.md

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:
ts
// tracking-plan.json
{ "add_to_cart": { "required": { "currency": "string", "value": "number", "item_id": "string" } } }

function validateEvent(plan, eventName, params) {
  const spec = plan[eventName];
  const violations = [];
  for (const [name, type] of Object.entries(spec.required)) {
    const raw = params.get(`ep.${name}`) ?? params.get(`epn.${name}`);
    if (raw == null) { violations.push({ param: name, problem: 'missing' }); continue; }
    if (type === 'number' && Number.isNaN(Number(raw))) violations.push({ param: name, problem: 'type', expected: 'number' });
  }
  return violations; // empty array = event satisfies the contract
}
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.

追踪计划是唯一可信来源:每个事件的名称、必填参数及各参数类型均需定义。将其存储为版本化文件(
tracking-plan.json
/
.yaml
,或TS
interface
/
Zod schema
),供应用团队与测试用例共同导入。测试用例读取捕获的事件并与计划进行验证——切勿在代码中硬编码预期值。
验证会生成结构化结果,而非简单的通过/失败布尔值:列出所有
缺失
的必填参数和所有类型
不匹配
/
违规
情况。计划条目与验证器示例:
ts
// tracking-plan.json
{ "add_to_cart": { "required": { "currency": "string", "value": "number", "item_id": "string" } } }

function validateEvent(plan, eventName, params) {
  const spec = plan[eventName];
  const violations = [];
  for (const [name, type] of Object.entries(spec.required)) {
    const raw = params.get(`ep.${name}`) ?? params.get(`epn.${name}`);
    if (raw == null) { violations.push({ param: name, problem: 'missing' }); continue; }
    if (type === 'number' && Number.isNaN(Number(raw))) violations.push({ param: name, problem: 'type', expected: 'number' });
  }
  return violations; // 空数组表示事件符合契约
}
随后使用
expect(validateEvent(plan, 'add_to_cart', params)).toEqual([])
进行断言。仅断言事件名称而忽略参数,或无计划文件硬编码预期值,是本技能旨在替代的低效做法。如需YAML格式、Zod类型化计划及可复用的
assertAgainstPlan
匹配器,请参考
references/tracking-plan.md

Asserting dataLayer.push

断言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
:
ts
await page.addInitScript(() => {
  window.dataLayer = window.dataLayer || [];
  const orig = window.dataLayer.push.bind(window.dataLayer);
  window.__pushes = [];
  window.dataLayer.push = (...args) => { window.__pushes.push(...args); return orig(...args); };
});
await page.goto('/product/42');
const pushes = await page.evaluate(() => window.__pushes);
const viewItem = pushes.find(p => p.event === 'view_item');
expect(viewItem.ecommerce.items[0]).toMatchObject({ item_id: 'SKU-42', price: 49.99, currency: 'USD' });
Use
find
/
filter
/
some
to locate the event in the recorded pushes. Full helper in
references/datalayer-capture.md
.

当测试目标为GTM的输入——「页面加载时是否向
dataLayer
推送了正确的对象?」——需断言推送行为,而非GA4信标。这与信标拦截相反:此处dataLayer推送本身就是测试目标。
在导航通过
addInitScript
包装
window.dataLayer.push
,记录页面加载后的所有推送行为,随后通过
page.evaluate
读取记录的数组。请勿使用
page.route
模拟dataLayer(这会替换被测对象),也请勿读取GA4网络信标(这是输出,属于不同契约)。
对于电商场景,需断言嵌套结构,而非仅事件名称——
ecommerce.items
数组及每个商品的
item_id
price
currency
ts
await page.addInitScript(() => {
  window.dataLayer = window.dataLayer || [];
  const orig = window.dataLayer.push.bind(window.dataLayer);
  window.__pushes = [];
  window.dataLayer.push = (...args) => { window.__pushes.push(...args); return orig(...args); };
});
await page.goto('/product/42');
const pushes = await page.evaluate(() => window.__pushes);
const viewItem = pushes.find(p => p.event === 'view_item');
expect(viewItem.ecommerce.items[0]).toMatchObject({ item_id: 'SKU-42', price: 49.99, currency: 'USD' });
使用
find
/
filter
/
some
在记录的推送中定位事件。完整工具类见
references/datalayer-capture.md

Pixels and Server-Side Deduplication

像素与服务器端去重

Marketing pixels send their own beacons. Endpoints to intercept:
DestinationEndpointEvent paramDedup 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:
ts
const [pixel] = await Promise.all([
  page.waitForRequest(r => r.url().includes('facebook.com/tr') && r.url().includes('ev=Purchase')),
  completeCheckout(page),
]);
const clientEventId = new URL(pixel.url()).searchParams.get('eid'); // event_id on the wire
expect(clientEventId).toBe(serverCapiEventId); // deduplicates against the server CAPI event
See
references/pixels-and-dedup.md
for parsing TikTok/LinkedIn payloads and capturing the server CAPI call.

营销像素会发送独立信标。需拦截的端点:
目标平台端点事件参数去重标识
Meta Pixel
facebook.com/tr
(也包含
/tr?
ev=PageView
,
ev=Purchase
eid
/
event_id
TikTok
analytics.tiktok.com
事件位于请求体/参数中
event_id
LinkedIn
px.ads.linkedin.com
转化ID
对于Meta,断言
connect.facebook.net/en_US/fbevents.js
已加载或
fbq('track','Purchase')
已运行,仅属于加载/计数检查——无法证明转化的正确性。同时触发客户端(像素)与服务器端(转化API/CAPI)的Purchase事件,其正确性验证点为去重:两者必须携带相同
event_id
,确保Meta将其合并为一次转化而非重复统计。
测试方法:捕获浏览器端
facebook.com/tr
信标中的
ev=Purchase
事件,读取其
event_id
,并断言该值与服务器发送至CAPI的
event_id
(来自模拟/捕获的服务器调用或已知固定值)一致。示例框架:
ts
const [pixel] = await Promise.all([
  page.waitForRequest(r => r.url().includes('facebook.com/tr') && r.url().includes('ev=Purchase')),
  completeCheckout(page),
]);
const clientEventId = new URL(pixel.url()).searchParams.get('eid'); // 传输中的event_id
expect(clientEventId).toBe(serverCapiEventId); // 与服务器CAPI事件进行去重匹配
如需解析TikTok/LinkedIn payload及捕获服务器CAPI调用,请参考
references/pixels-and-dedup.md

Consent Mode v2 Gating

Consent Mode v2管控

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
:
SignalGoverns
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.

Consent Mode v2是标准(强制四信号模型)。截至2026年6月15日的更新,Google仅依据CMP发送的同意信号执行操作,因此任何双信号方案均已过时。需测试两种状态。
四个信号——全部必须默认设置为
denied
信号管控范围
ad_storage
广告Cookie
analytics_storage
分析Cookie
ad_user_data
向Google发送用户数据用于广告
ad_personalization
个性化广告/再营销
默认
granted
属于漏洞;遗漏
ad_user_data
ad_personalization
(v2新增信号)属于过时的双信号模型,不符合要求。
同意前:不应触发完整信标——或仅发送无Cookie的同意请求。使用
addInitScript
在页面脚本运行前设置
gtag('consent', 'default', {...})
为denied状态,并断言无
/g/collect
请求触发(或触发的请求携带denied同意状态)。
同意后:点击CMP同意按钮;此时应触发完整信标。
同意状态通过信标URL传递:
  • gcs=
    —— 仅编码
    ad_storage
    +
    analytics_storage
    G100
    =两者均denied,
    G111
    =两者均granted,
    G110
    /
    G101
    =部分同意。同意前预期值为
    gcs=G100
  • gcd=
    —— 编码全部四个信号(以
    11...
    开头的字符串);所有发送至Google服务的请求均携带该参数。
断言同意前的denied默认状态及信标中的
gcs=
/
gcd=
值,随后断言同意后的granted状态。完整测试(包含
addInitScript
同意状态预置及CMP点击流程)见
references/consent-mode.md
特定同意状态下信标是否符合法规要求,属于
compliance-testing
范畴。本技能仅断言信标触发时,其数据与同意参数是否正确。

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(...))
/
scrollBy
— and wait on the beacon (
waitForRequest
or your captured-events list), never
waitForTimeout
. Scrolling in fixed sleeps both flakes and masks threshold-timing bugs.
ts
const buckets = [25, 50, 75, 100];
for (const pct of buckets) {
  const [req] = await Promise.all([
    page.waitForRequest(r => r.url().includes('/g/collect') && r.url().includes('en=scroll')),
    page.evaluate(p => window.scrollTo(0, document.body.scrollHeight * (p / 100)), pct),
  ]);
  expect(new URL(req.url()).searchParams.get('epn.percent_scrolled')).toBe(String(pct));
}
Full suite (article_view metadata, the four scroll buckets de-duplicated, and the paywall-meter event) in
references/news-media.md
.

新闻与发布商站点存在通用电商计划未覆盖的追踪场景。需覆盖以下三类:
  • article_view(或
    article-view
    ):文章加载时触发——断言信标触发一次并携带文章元数据(ID、栏目、作者)。
  • scroll-depth:在25/50/75/100%滚动阈值处触发——每个阈值对应一个事件,由真实滚动操作驱动。
  • paywall / meter:免费文章计量耗尽时触发
    paywall_hit
    (或计量事件)。
通过真实操作驱动滚动——
mouse.wheel
element.scrollIntoView
page.evaluate(() => window.scrollTo(...))
/
scrollBy
——并等待信标(
waitForRequest
或捕获的事件列表),绝不使用
waitForTimeout
。固定延迟滚动不仅不稳定,还会掩盖阈值时序问题。
ts
const buckets = [25, 50, 75, 100];
for (const pct of buckets) {
  const [req] = await Promise.all([
    page.waitForRequest(r => r.url().includes('/g/collect') && r.url().includes('en=scroll')),
    page.evaluate(p => window.scrollTo(0, document.body.scrollHeight * (p / 100)), pct),
  ]);
  expect(new URL(req.url()).searchParams.get('epn.percent_scrolled')).toBe(String(pct));
}
完整测试套件(包含article_view元数据、四个滚动桶去重、付费墙计量事件)见
references/news-media.md

Multi-Pixel Capture Fixture

多像素捕获夹具

Don't re-implement beacon capture in every test. Build one reusable Playwright fixture (
test.extend
) that listens with
page.on('request', ...)
, matches all destination endpoints (GA4
/g/collect
,
facebook.com/tr
,
analytics.tiktok.com
,
px.ads.linkedin.com
), parses each request's
new URL(...).searchParams
and
postData()
, and pushes a normalized
{ destination, eventName, params }
onto a
collected
array tests assert against.
Critical trap: a capture helper must observe, so use
page.on('request')
(or
page.route
followed by
route.continue()
), never
page.route(...).abort()
— aborting blocks the very beacons you are trying to see. And it must capture pixels too, not GA4 only. Full fixture in
references/capture-fixture.md
.

无需在每个测试中重复实现信标捕获逻辑。构建一个可复用的Playwright夹具(
test.extend
),通过
page.on('request', ...)
监听所有目标端点(GA4
/g/collect
facebook.com/tr
analytics.tiktok.com
px.ads.linkedin.com
),解析每个请求的
new URL(...).searchParams
postData()
,并将标准化的
{ destination, eventName, params }
对象推入
collected
数组供测试断言使用。
关键注意事项:捕获工具仅需观察,因此使用
page.on('request')
(或
page.route
后调用
route.continue()
),切勿使用
page.route(...).abort()
——终止请求会阻止你要观察的信标。且工具需同时捕获所有像素,而非仅GA4。完整夹具见
references/capture-fixture.md

Regression-Gating in CI

CI中的回归管控

The gate diffs captured events against the tracking-plan baseline and fails the build (non-zero exit) when a previously-firing event stops firing or a required param goes missing after a release. Structure it as a Playwright project that runs the journeys, captures every beacon via the fixture, and validates each against the plan; on any
missing
/dropped event the test
expect
fails, Playwright exits non-zero, and the GitHub Actions (or any CI) job goes red.
Do not make the gate "check GA4 DebugView manually," "only run against production traffic," or "warn but pass anyway" — none of those block a regression. The diff-against-baseline run belongs in PR CI, before merge. See
references/ci-gating.md
for the workflow YAML, the baseline-diff script, and how to surface missing-param failures in the job summary.

管控逻辑会将捕获的事件与追踪计划基线进行比对,当已触发的事件在版本发布后停止触发或必填参数缺失时,触发构建失败(非零退出码)。将其构建为Playwright项目,运行测试流程,通过夹具捕获所有信标并与计划进行验证;当出现
缺失
/事件丢失时,测试
expect
断言失败,Playwright返回非零退出码,GitHub Actions(或任意CI)任务标记为失败。
切勿将管控设置为「手动检查GA4 DebugView」「仅针对生产流量运行」或「仅警告但允许通过」——这些方式均无法阻止回归。基线比对需在PR CI中运行,即在合并之前。如需工作流YAML、基线比对脚本及在任务摘要中展示缺失参数失败信息,请参考
references/ci-gating.md

Buy vs Build

自研vs采购

DIY Playwright interception is the right call for a bounded set of events on a few critical journeys gated in CI. It stops paying off at scale: hundreds of events across many domains, with a small team, and a need for continuous production / drift monitoring (catching a tag a marketer breaks in GTM at 2am, which a pre-merge CI gate never sees).
DimensionBuild (Playwright)Buy
Few events, key journeys, pre-merge gateBest fitOverkill
Hundreds of events, many domainsMaintenance crushes youBuy
Continuous 24/7 production drift monitoringOut of scope for CIBuy
Small team, high coverage demandBuild cost too highBuy
Current live paid options worth naming:
  • Trackingplan — always-on/continuous monitoring of live traffic across web, mobile, and server-side; strongest when you need real-time drift detection rather than scheduled checks.
  • ObservePoint — scheduled scans/audits of journeys against a tracking plan; mature for periodic governance.
Don't recommend Segment Protocols as the only validation (it governs data flowing through Segment, not arbitrary client beacons), and never call Google Tag Assistant a CI gate — it is an interactive debug tool, not an automated pass/fail. Tie the decision to scale, many domains, maintenance burden, and continuous monitoring — the axes where DIY stops paying off.

当仅需在少量关键流程中验证有限事件并在CI中管控时,自研Playwright拦截方案是最佳选择。但在规模化场景下不再适用:数百个事件跨多域名、小团队、需要持续生产/漂移监控(例如营销人员在凌晨2点修改GTM标签导致问题,而预合并CI管控无法发现)。
维度自研(Playwright)采购
少量事件、关键流程、预合并管控最佳适配过度冗余
数百个事件、多域名维护成本过高建议采购
7×24小时持续生产漂移监控CI无法覆盖建议采购
小团队、高覆盖率需求自研成本过高建议采购
当前值得推荐的付费方案:
  • Trackingplan —— 持续监控Web、移动端及服务器端的实时流量;在需要实时漂移检测而非定时检查时表现最佳。
  • ObservePoint —— 定时扫描/审计测试流程并与追踪计划比对;在周期性治理场景下成熟可靠。
不建议仅使用Segment Protocols作为验证工具(仅管控流经Segment的数据,无法验证任意客户端信标),且切勿将Google Tag Assistant作为CI管控工具——它是交互式调试工具,而非自动化通过/失败工具。决策需基于规模、多域名、维护负担及持续监控需求——这些都是自研方案不再划算的场景。

Anti-Patterns

反模式

1. Asserting
window.dataLayer
(or the DOM) instead of the beacon

1. 仅断言
window.dataLayer
(或DOM)而非信标

page.evaluate(() => window.dataLayer)
proves a push happened, not that GA4 sent anything;
toBeVisible()
proves nothing about tracking. Intercept the
/g/collect
request and assert its
en=
and
ep.
/
epn.
params. (The one exception: when the push itself is the contract — see dataLayer.push — but then never reach for the GA4 beacon instead.)
page.evaluate(() => window.dataLayer)
仅能证明推送行为发生,无法证明GA4已发送数据;
toBeVisible()
无法证明追踪有效。需拦截
/g/collect
请求并断言其
en=
ep.
/
epn.
参数。(唯一例外:当推送行为本身就是契约时——见dataLayer.push章节,但此时切勿转而断言GA4信标。)

2.
waitForTimeout
to "wait for the event to fire"

2. 使用
waitForTimeout
「等待事件触发」

A fixed sleep flakes and hides timing bugs. Wait on the request:
page.waitForRequest(r => r.url().includes('/g/collect'))
.
固定延迟不仅不稳定,还会掩盖时序问题。应等待请求:
page.waitForRequest(r => r.url().includes('/g/collect'))

3. Asserting only the event name

3. 仅断言事件名称

Name-only assertions pass while currency, value, and item_id are wrong. Validate every required param and its type against the tracking plan.
仅断言名称会导致货币类型、数值、商品ID错误时仍通过测试。需根据追踪计划验证所有必填参数及其类型。

4. Hardcoding expected values inline with no plan file

4. 无计划文件,硬编码预期值

There is no source of truth, so nothing catches a renamed param across the suite. Keep the plan external and validate against it.
无可信来源,无法在套件中捕获参数重命名问题。需将计划存储在外部并基于其进行验证。

5. Load/count checks for pixels (
fbevents.js
loaded,
fbq
ran)

5. 像素加载/计数检查(
fbevents.js
已加载、
fbq
已运行)

Neither proves the conversion is correct or de-duplicated. For Purchase, assert the client and server
event_id
match.
这些检查无法证明转化正确或已去重。对于Purchase事件,需断言客户端与服务器的
event_id
匹配。

6. Two-signal Consent Mode, or a
granted
default

6. 双信号Consent Mode或默认
granted

Omitting
ad_user_data
and
ad_personalization
is the pre-2026 model. Default all four to
denied
and assert
gcs=
/
gcd=
.
遗漏
ad_user_data
ad_personalization
属于2026年前的旧模型。需将四个信号全部默认设置为
denied
并断言
gcs=
/
gcd=

7.
page.route(...).abort()
inside a capture helper

7. 在捕获工具中使用
page.route(...).abort()

Aborting blocks the beacons you meant to observe. Use
page.on('request')
(or
route.continue()
).
终止请求会阻止你要观察的信标。应使用
page.on('request')
(或
route.continue()
)。

8. A "gate" that can't fail the build

8. 无法触发构建失败的「管控」

GA4 DebugView, production-only monitoring, or warn-but-pass do not stop a regression. Make CI exit non-zero on a dropped event or missing required param.

GA4 DebugView、仅生产环境监控或仅警告不阻止构建,均无法阻止回归。需设置CI在事件丢失或必填参数缺失时返回非零退出码。

Verification

验证

Smallest check first — prove the suite actually catches a regression, don't just trust a green run:
  • One event, one beacon:
    npx playwright test -g add_to_cart
    passes, and its trace (
    --trace on
    ) shows the captured
    /g/collect
    request with
    en=add_to_cart
    . A green test with no matching request in the trace means you asserted on the wrong thing.
  • The gate can fail: rename a required param in a branch (e.g.
    value
    amount
    in the app) and re-run the CI project — the build must exit non-zero with a
    missing
    /violation message. If it still passes, the gate is theater.
  • Consent default is denied: with the denied seed and no accept click,
    gcs=G100
    (or no full beacon) on every captured hit; after accept,
    gcs=G111
    . A pre-consent
    G111
    means the default is wrongly
    granted
    .
  • Dedup holds: the client
    eid
    from
    facebook.com/tr?ev=Purchase
    equals the server CAPI
    event_id
    for the same order. Different values mean Meta will double-count.
首先进行最小化检查——证明套件确实能捕获回归,不要仅信任绿色运行结果:
  • 单个事件、单个信标
    npx playwright test -g add_to_cart
    通过,且其追踪记录(
    --trace on
    )显示捕获到带有
    en=add_to_cart
    /g/collect
    请求。若测试通过但追踪记录中无匹配请求,说明断言对象错误。
  • 管控可触发失败:在分支中重命名必填参数(例如将应用中的
    value
    改为
    amount
    )并重新运行CI项目——构建必须返回非零退出码并显示
    缺失
    /违规信息。若仍通过,说明管控无效。
  • 同意默认状态为denied:设置denied预置且未点击同意时,所有捕获的请求均携带
    gcs=G100
    (或无完整信标);点击同意后携带
    gcs=G111
    。若同意前为
    G111
    ,说明默认状态错误设置为
    granted
  • 去重有效
    facebook.com/tr?ev=Purchase
    中的客户端
    eid
    与同一订单的服务器CAPI
    event_id
    一致。若值不同,Meta会重复统计。

Done When

完成标准

  • Each tracked event has a test that intercepts the real collect-endpoint beacon (
    /g/collect
    ,
    facebook.com/tr
    , etc.) and parses
    en=
    /
    ev=
    plus params from
    searchParams
    /
    postData
    — no DOM-only or dataLayer-only assertions.
  • A versioned tracking plan file exists (JSON/YAML/TS) with required params and types; tests validate captured events against it and report
    missing
    /type violations, not just the event name.
  • Purchase (or any client+server event) has a deduplication assertion: client beacon
    event_id
    equals the server CAPI
    event_id
    .
  • A Consent Mode v2 test asserts all four signals default to
    denied
    , no full beacon fires before consent (or only a cookieless ping), the full beacon fires after accept, and
    gcs=
    /
    gcd=
    carry the expected consent state.
  • A reusable capture fixture (
    test.extend
    ) collects GA4 + Meta + TikTok + LinkedIn beacons; no test re-implements capture and no capture helper calls
    route.abort()
    .
  • News-media surfaces (if present) cover article_view, scroll-depth at 25/50/75/100 driven by real scroll actions, and a paywall/meter event — all using
    waitForRequest
    , no
    waitForTimeout
    .
  • A CI job runs the suite, diffs captured events against the tracking-plan baseline, and exits non-zero on a dropped event or missing required param — verified by a deliberately-broken tag turning the build red.
  • 每个追踪事件均有对应的测试,拦截真实收集端点信标(
    /g/collect
    facebook.com/tr
    等)并从
    searchParams
    /
    postData
    中解析
    en=
    /
    ev=
    及参数——无仅依赖DOM或dataLayer的断言。
  • 存在版本化追踪计划文件(JSON/YAML/TS),包含必填参数及类型;测试将捕获的事件与计划进行验证并报告
    缺失
    /类型违规,而非仅断言事件名称。
  • Purchase(或任意客户端+服务器事件)存在去重断言:客户端信标
    event_id
    与服务器CAPI
    event_id
    一致。
  • Consent Mode v2测试断言四个信号默认均为
    denied
    ,同意前无完整信标触发(或仅发送无Cookie请求),同意后触发完整信标,且
    gcs=
    /
    gcd=
    携带预期同意状态。
  • 存在可复用捕获夹具(
    test.extend
    ),可收集GA4 + Meta + TikTok + LinkedIn信标;无测试重复实现捕获逻辑,且捕获工具未调用
    route.abort()
  • 新闻媒体场景(若存在)覆盖article_view、由真实滚动操作驱动的25/50/75/100%滚动深度、付费墙/计量事件——全部使用
    waitForRequest
    ,无
    waitForTimeout
  • CI任务运行套件,将捕获的事件与追踪计划基线进行比对,当事件丢失或必填参数缺失时返回非零退出码——已通过故意破坏标签触发构建失败进行验证。

Related Skills

相关技能

  • compliance-testing — Whether a beacon is allowed to fire under GDPR/CMP consent law, cookie-consent UI, and data-subject rights. This skill assumes the beacon is permitted and checks its data is correct; go there for the legality question.
  • playwright-automation — Page Object Model, fixtures, config, and the general E2E patterns this skill builds its interception on.
  • api-testing — Validating the server-side Conversions API / Measurement Protocol calls directly (request body, auth, response) when you need to assert the server half of deduplication.
  • qa-project-context — Stack, tag manager, consent platform, and target environments that this skill's discovery questions read first.
  • compliance-testing —— 验证信标是否符合GDPR/CMP consent法规要求、Cookie同意UI及数据主体权利。本技能假设信标已获允许,仅检查数据正确性;合法性问题请参考该技能。
  • playwright-automation —— 页面对象模型、夹具、配置及本技能拦截逻辑所依赖的通用E2E模式。
  • api-testing —— 直接验证服务器端转化API/测量协议调用(请求体、认证、响应),用于断言去重的服务器端部分。
  • qa-project-context —— 本技能调研问题需先读取的技术栈、标签管理器、同意平台及目标环境信息。

Reference Files (in
references/
)

参考文件(位于
references/

  • ga4-interception.md — Full GA4
    /g/collect
    interception helper, batched-event POST-body parsing, region-endpoint handling.
  • tracking-plan.md — JSON and YAML plan forms, a Zod-typed contract, and the reusable
    assertAgainstPlan
    matcher.
  • datalayer-capture.md
    addInitScript
    dataLayer.push wrapper and ecommerce
    items[]
    shape assertions.
  • pixels-and-dedup.md — Meta/TikTok/LinkedIn payload parsing and client-vs-server CAPI
    event_id
    dedup capture.
  • consent-mode.md — Consent Mode v2 default-denied seeding, CMP accept-click flow, and
    gcs=
    /
    gcd=
    assertions.
  • news-media.md — article_view, de-duplicated 25/50/75/100 scroll buckets, and paywall-meter event tests.
  • capture-fixture.md — The reusable multi-pixel
    test.extend
    capture fixture.
  • ci-gating.md — GitHub Actions workflow, baseline-diff script, and surfacing missing-param failures.
  • ga4-interception.md —— 完整GA4
    /g/collect
    拦截工具类、批量事件POST请求体解析、区域端点处理。
  • tracking-plan.md —— JSON与YAML格式计划、Zod类型化契约及可复用
    assertAgainstPlan
    匹配器。
  • datalayer-capture.md ——
    addInitScript
    dataLayer.push包装器及电商
    items[]
    结构断言。
  • pixels-and-dedup.md —— Meta/TikTok/LinkedIn payload解析及客户端与服务器CAPI
    event_id
    去重捕获。
  • consent-mode.md —— Consent Mode v2默认denied预置、CMP同意点击流程及
    gcs=
    /
    gcd=
    断言。
  • news-media.md —— article_view、去重后的25/50/75/100%滚动桶及付费墙计量事件测试。
  • capture-fixture.md —— 可复用多像素
    test.extend
    捕获夹具。
  • ci-gating.md —— GitHub Actions工作流、基线比对脚本及缺失参数失败信息展示。