关于 analytics-tracking-testing
用于通过浏览器网络信标和类型化追踪计划验证分析事件的测试技能,帮助 QA 与开发团队在 CI 中发现事件丢失、参数错误、同意状态问题和重复转化。
主要功能
- 01拦截分析网络信标
- 02按类型化计划校验事件
- 03检查 dataLayer.push 数据
- 04测试基于同意状态的事件门控
- 05验证像素与 CAPI 去重
- 06在 CI 中阻止追踪回归
适用场景
- 01发现重构后丢失的 GA4 事件
- 02检查电商事件金额与商品数据
- 03测试同意或拒绝后的行为
- 04避免购买转化重复计数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill analytics-tracking-testing安装后在 Claude Code 中输入/analytics-tracking-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/capture-fixture.mdci-gating.mdconsent-mode.mddatalayer-capture.mdga4-interception.mdnews-media.mdpixels-and-dedup.mdtracking-plan.md详细介绍
快速导航
| 情况 | 前往 |
|---|---|
| 断言某个 GA4 事件已触发且参数正确 | 拦截 GA4 信标 |
| 将跟踪计划作为经过验证的契约 | 跟踪计划就是契约 |
| 验证 dataLayer.push 的结构(而非信标) | 断言 dataLayer.push |
| Pixel 与服务端 CAPI 去重(event_id) | Pixel 与服务端去重 |
| 按同意状态判断事件必须/不得触发 | Consent Mode v2 门控 |
| 在一个可复用的 fixture 中捕获所有 Pixel | references/capture-fixture.md |
| 新闻媒体:article-view、scroll-depth、paywall | 新闻媒体事件 |
| 跟踪回归时让构建失败 | 在 CI 中设置回归门禁 |
| 数百个事件、多个域名、小型团队 | 购买还是自建 |
调研问题
首先:检查项目根目录中的 .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 才触发信标。你可以在 push 层(输入契约)和信标层(输出契约)分别断言;这是两种不同的测试。 - 是否有跟踪计划? 如果没有,先建立一个——它是其他所有验证的契约。没有计划,就没有客观的通过/失败标准。
- 同意管理平台和默认同意状态是什么? Consent Mode v2 会改变同意前允许触发的信标。你需要使用 CMP 的接受/拒绝选择器来驱动测试。
- 是否同时使用客户端和服务端(CAPI)去重? 如果购买事件同时触发客户端 Pixel 和服务端 Conversions API,
event_id必须一致,否则就会重复计算。这才是正确性要求,而不是“fbq 是否运行”。 - 是否有新闻媒体页面? 文章页面会增加 article-view、滚动深度阈值(25/50/75/100)和付费墙计量事件;通用电商跟踪计划可能会遗漏这些事件。
核心原则
-
拦截信标,切勿只信任 DOM 或
dataLayer。 按钮点击后更新了 DOM,或者 GTM 悄无声息地丢弃了dataLayer.push,都不会产生 GA4 命中。事件确实已发送的唯一证据,是发往 collect 端点的出站请求。应断言网络信标的 URL 参数;通过page.evaluate读取window.dataLayer,只能证明推送发生了,无法证明数据已离开浏览器。 -
跟踪计划是类型化契约,而不是注释。 每个预期事件都应定义在外部架构(JSON/YAML/TS 接口)中,并注明必需参数及其类型。测试会根据架构校验捕获的事件,并报告缺失参数和类型违规。将某个预期值硬编码在行内,无法断言其他二十个参数,而且跟踪计划一变就会过时。
-
去重才是购买事件的真正正确性要求。 检查
fbevents.js是否已加载,或统计fbq('track','Purchase')的调用次数,都无法发现重复计数。真正重要的是:浏览器 Pixel 和服务端 CAPI 发送相同的event_id/eventID,使 Meta 将它们合并为一次转化。 -
同意状态是一个输入维度。 按照已批准的区域政策,在基本模式或高级模式下进行测试。观察应用实际的四个信号默认值及其更新;不要在测试中提供默认值。基本模式会在用户同意前阻止标签,而高级模式可能会在不使用 Cookie 的情况下发送允许的测量数据。应分别断言计划中的同意后事件和相关存储。
-
驱动真实用户操作并等待请求,而不是等待时间。 滚动深度事件由实际滚动触发(
mouse.wheel、scrollIntoView、evaluate(scrollTo)),应使用waitForRequest等待信标,而不是使用waitForTimeout。固定延时容易导致测试不稳定,也会掩盖本该捕获的时序问题。 -
无法让构建失败的跟踪测试只是做样子。 “在 GA4 DebugView 中检查”和“监控生产环境”都无法阻止回归。契约必须在 CI 中运行,并在事件被丢弃或缺少必需参数时以非零状态退出。
拦截 GA4 信标
GA4 网页标签使用 /g/collect 请求,有时 POST 正文中会包含多个事件,通用字段则位于查询参数中。以下解析器处理的是一种观测到的网页标签格式,而不是公开的 JSON Measurement Protocol。请保留来自已部署标签版本的捕获数据;第一方代理和服务端收集需要显式适配器。请求只能证明浏览器已发出数据,不能证明下游已成功接收。
你要断言的 GA4 /g/collect URL 语法:
| 参数 | 含义 | 示例 |
|---|---|---|
v=2 | 观测到的网页标签传输中的版本字段 | v=2 |
tid=G-XXXXXXX | 测量 ID | tid=G-ABC123 |
en= | 事件名称 | en=add_to_cart |
ep.<name>= | 自定义事件参数,类型为字符串 | ep.campaign_label=spring |
epn.<name>= | 事件参数,类型为数字 | epn.value=49.99 |
gcs= / gcd= | 同意状态(参见 Consent Mode v2 部分) | gcs=G111 |
保留传输中的类型信息。自定义参数可以使用 ep. 或 epn.,但预留的电商字段和商品有各自的网页标签编码。不要假设标准货币字段是 ep.currency,或商品 ID 是扁平的 ep.item_id。Google 的逻辑电商契约使用事件级 currency 和嵌套的 items[];在检查该契约之前,请通过经过捕获测试的适配器解码观测到的传输数据。
使用 page.waitForRequest(单个预期事件)或 page.on('request', ...)
(收集多个事件)进行拦截。在执行操作前添加监听器。匹配准确的 /g/collect 路径和获准的主机名;使用 references/ga4-interception.md 中的 parseGA4,将每个 POST 事件的参数合并到 URL 查询参数之上;仅匹配 URL 会漏掉批量事件。请使用测试媒体资源或本地收集器,以免测试污染生产分析数据。
以下模式结合了下方的解析器、应用经过验证的电商解码器和逻辑跟踪计划。decodeEcommerceWebEvent 是项目必须提供的适配器,并非此技能提供的包;不支持或缺失的传输字段必须导致失败。请参阅 references/ga4-interception.md 和 references/tracking-plan.md。
test('add_to_cart fires with correct params', async ({ page }) => {
await page.goto('/product/42');
const [request] = await Promise.all([
page.waitForRequest(r => parseGA4(r).some(event => event.name === 'add_to_cart')),
page.getByRole('button', { name: 'Add to cart' }).click(),
]);
const events = parseGA4(request).filter(event => event.name === 'add_to_cart');
expect(events).toHaveLength(1);
const params = events[0].params;
expect(params.tid).toBe('G-TEST'); // use the test property's exact measurement ID
// Application adapter, checked against redacted captures from the deployed tag.
const payload = decodeEcommerceWebEvent(params);
expect(validateEvent(plan, 'add_to_cart', payload)).toEqual([]);
expect(payload).toMatchObject({
currency: 'USD', value: 49.99,
items: [{ item_id: 'SKU-42', price: 49.99, quantity: 1 }],
});
});
切勿仅用 page.evaluate(() => window.dataLayer) 作为断言,也不要对按钮使用 toBeVisible(),或调用 waitForTimeout() 来“等待信标发送”。批量事件解析(GA4 可以将多个事件打包到一个 POST 正文中)和区域端点处理,请参阅 references/ga4-interception.md。
跟踪计划是契约
跟踪计划是事实来源:为每个事件规定其名称、必需参数及每个参数的类型。将它保存在带版本控制的文件中(tracking-plan.json / .yaml,或 TS interface / Zod schema),供应用团队和测试共同导入。测试读取捕获的事件,并根据计划进行验证——检查 schema 和已知场景的值。
验证规范化后的逻辑载荷,并返回每个缺失字段和类型违规项。references/tracking-plan.md 中的辅助函数会检查有限数字、非空字符串和非空的嵌套商品数组。它会拒绝不支持或为空的 schema。零是有效数字;缺失字段、空字符串、null、Infinity 或数字字符串都不等于具有数字类型的零。完成 schema 检查后,再添加特定场景的预期值。
const payload = decodeEcommerceWebEvent(capturedGA4Event.params); // capture-tested project adapter
expect(validateEvent(plan, 'add_to_cart', payload)).toEqual([]);
expect(payload.currency).toBe('USD');
expect(payload.value).toBe(49.99);
expect(payload.items).toEqual([{ item_id: 'SKU-42', price: 49.99, quantity: 1 }]);
schema 匹配并不能证明价格正确。请将载荷与已知的测试购物车进行比较,然后在应用的跟踪完成边界断言事件数量。保留原始输入事件和原始发出记录,确保解码器无法掩盖字段缺失、标签转换或重复购买。
断言 dataLayer.push
当问题明确针对 GTM 输入——“页面加载时,是否将正确的对象推送到了 dataLayer?”——应断言推送,而不是 GA4 信标。这与拦截信标的做法正好相反:此处的目标就是 dataLayer 推送。
导航前,先安装 references/datalayer-capture.md 中的被动观察器。
若 data layer 不存在,就让它保持不存在,观察应用首次赋值,保留
push 行为,并标记后续替换或观察丢失。若测试目的是测量应用的设置,
请勿初始化或修复应用队列。
检查捕获的对象前,先等待应用发出的完成信号。
对于 GTM 电商对象事件,应断言 ecommerce.currency、ecommerce.value 和
ecommerce.items[],并检查每个商品的预期 ID、价格和数量。根据 Google 的电商规范,币种
属于事件级别。除了检查结构,也要检查数量;只选取第一个匹配的事件会掩盖重复事件。直接的 gtag 命令数组需要使用相应的命令适配器,而不是对象事件解析器。
参考文件包含观察器和完整示例。如果问题涉及完整的跟踪链路,请同时使用观察到的输入和网络输出,并记录 每项观察能证明什么。
像素与服务端去重
营销像素会发送自己的信标。需要拦截的端点:
| 目标 | 端点 | 事件参数 | 去重键 |
|---|---|---|---|
| Meta Pixel | facebook.com/tr(也包括 /tr?) | ev=PageView、ev=Purchase | eid / event_id |
| TikTok | analytics.tiktok.com | 请求正文/参数中的事件 | event_id |
px.ads.linkedin.com | 转化 ID | — |
对于 Meta,断言 connect.facebook.net/en_US/fbevents.js 已加载,或 fbq('track','Purchase') 已运行,只是在检查加载或次数——这不能证明正确性。对于同时通过客户端(Pixel)和服务端(Conversions API / CAPI)触发的 Purchase,正确性要求是去重:两者必须携带相同的 event_id,这样 Meta 才会将它们合并为一次转化,而不是重复计数。
测试方法:捕获浏览器发往 facebook.com/tr、且 ev=Purchase 的信标,读取其 event_id,并断言它等于服务器发送给 CAPI 的 event_id(从模拟/捕获的服务器调用或已知的固定值中获取)。框架如下:
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
解析 TikTok/LinkedIn 负载并捕获服务器 CAPI 调用,请参阅 references/pixels-and-dedup.md。
Consent Mode v2 门控
编写门控前,先选择获批的地区默认设置以及基础或高级实施方式。参考示例采用的是全部拒绝的默认策略。 请参阅 Google 的 consent-mode 概述。
根据该策略观察以下四种信号:
| 信号 | 控制内容 |
|---|---|
ad_storage | 广告 Cookie |
analytics_storage | 分析 Cookie |
ad_user_data | 向 Google 发送用于广告的用户数据 |
ad_personalization | 个性化广告 / 再营销 |
每项测试中,都要在导航前安装被动观察器。它不得创建 gtag、发送 consent 命令,或修复被替换的数据层。缺少默认设置、过早的测量命令或不正确的中间状态,都必须导致测试失败。
**同意前:**基础模式会阻止已配置的标签请求。高级模式可以发送事件计划中处于拒绝状态时的 ping。观察真实的应用完成信号以及任何声明的观察时间段;采集器为空或达到 networkidle,都不能证明延迟跟踪正确。基础模式可以在用户接受后初始化默认设置。
**接受后:**记录现有命令和事件,在点击 CMP 按钮后,要求出现新的匹配更新以及新出现的预期信标。分别对拒绝、部分同意、撤回和持久化行为重复测试。
在捕获到的网页标签传输中,gcs=G100 和 gcs=G111 可区分这两种存储信号。gcd 是否存在只能作为诊断信息,不能证明四种信号或载荷标识符都正确。保留带版本号的传输捕获,并使用观察到的四种状态以及获批的载荷 / 存储清单。不要依赖未记录的 gcd 前缀。可执行辅助函数和测试配方见 references/consent-mode.md。
某个同意状态下的信标是否受法律允许,属于
compliance-testing的范畴。本技能断言的是:当信标触发时,其数据和 consent 参数必须正确。
新闻媒体事件
新闻和出版商网站具有通用电商计划会遗漏的跟踪面。以下三项都要覆盖:
- article_view(或
article-view)在文章加载时触发——断言信标只触发一次,并包含文章元数据(id、section、author)。 - scroll-depth 在 25 / 50 / 75 / 100 百分比阈值触发——每个区间触发一个事件,并由真实滚动驱动。
- 付费墙 / 阅读量限制——免费文章阅读量用尽时触发
paywall_hit(或 meter)事件。
使用实际操作来滚动——mouse.wheel、element.scrollIntoView 或 page.evaluate(() => window.scrollTo(...)) / scrollBy——并等待信标(waitForRequest 或你捕获的事件列表),不要使用 waitForTimeout。通过固定的休眠时间来滚动既容易导致测试偶发失败,也会掩盖阈值时序问题。
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。
多像素捕获 Fixture
不要在每项测试中重复实现信标捕获。构建一个可复用的 Playwright fixture(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。完整 fixture 见 references/capture-fixture.md。
CI 中的回归门禁
门禁会将捕获的事件与跟踪计划基线进行差异比对;发布后,如果之前触发的事件不再触发,或必需参数缺失,就会使构建失败(退出码非零)。将其构建为一个 Playwright 项目,运行用户旅程,通过 fixture 捕获每个信标,并根据计划逐一验证;一旦出现任何 missing/被丢弃的事件,测试 expect 就会失败,Playwright 以非零状态退出,GitHub Actions(或任何 CI)任务也会标记为失败。
不要把门禁做成“手动检查 GA4 DebugView”、“只针对生产流量运行”或“发出警告但仍然通过”——这些做法都无法阻止回归。针对基线进行差异比对的运行应放在合并前的 PR CI 中。有关工作流 YAML、基线差异比对脚本,以及如何在任务摘要中显示缺少参数导致的失败,请参阅 references/ci-gating.md。
购买还是自建
对于 CI 中受门禁控制、涉及少量关键用户旅程的一组有限事件,自行使用 Playwright 拦截是合适的选择。但规模扩大后,这种方式就不再划算:事件有数百个、涉及多个域名、团队规模又小,而且需要持续监控生产环境/偏移(例如发现营销人员凌晨 2 点在 GTM 中弄坏的标签;合并前的 CI 门禁永远看不到这类问题)。
| 维度 | 自建(Playwright) | 购买 |
|---|---|---|
| 少量事件、关键用户旅程、合并前门禁 | 最合适 | 杀鸡用牛刀 |
| 数百个事件、多个域名 | 维护工作会压垮团队 | 购买 |
| 持续 24/7 监控生产环境中的偏移 | 不在 CI 范围内 | 购买 |
| 小团队、高覆盖率需求 | 自建成本过高 | 购买 |
目前值得一提的付费产品:
- Trackingplan — 持续监控 Web、移动端和服务端的实时流量;如果你需要实时偏移检测,而非定期检查,它尤其适合。
- ObservePoint — 按计划扫描/审计用户旅程,并与跟踪计划进行比对;在定期治理方面已相当成熟。
不要建议只用 Segment Protocols 做验证(它治理的是流经 Segment 的数据,而非任意客户端信标),也不要把 Google Tag Assistant 称为 CI 门禁——它是交互式调试工具,不是自动化的通过/失败检查。决策应结合规模、域名数量、维护负担和持续监控需求——这些正是自建方案不再划算的方面。
反模式
1. 断言 window.dataLayer(或 DOM),而不是信标
page.evaluate(() => window.dataLayer) 只能证明发生过推送,不能证明 GA4 发送了任何内容;toBeVisible() 与跟踪无关。应拦截 /g/collect 请求,并断言其中的 en= 和 ep./epn. 参数。(唯一的例外:当推送本身就是约定内容时——参见 dataLayer.push;但这时绝不要转而检查 GA4 信标。)
2. 用 waitForTimeout“等待事件触发”
固定时长的休眠容易导致测试不稳定,还会掩盖时序错误。应等待请求:page.waitForRequest(r => r.url().includes('/g/collect'))。
3. 只断言事件名称
只检查名称的断言会在币种、金额和 item_id 错误时仍然通过。应根据跟踪计划验证每个必需参数及其类型。
4. 将预期值硬编码在代码中,不使用计划文件
没有单一事实来源,因此无法发现整个测试套件中参数重命名的问题。应将计划保存在外部文件中,并据此进行验证。
5. 检查像素是否加载/调用次数(已加载 fbevents.js、运行过 fbq)
这两项都不能证明转化正确或已去重。对于 Purchase,应断言客户端和服务端的 event_id 相同。
6. 测试由测试本身提供的默认值
预先设置拒绝同意状态,可能会掩盖应用没有提供自身默认值的问题。应根据所选策略观察全部四个真实信号,并区分基本模式和高级模式。
7. 在捕获辅助函数中使用 page.route(...).abort()
中止请求会阻止你要观察的信标。应使用 page.on('request')(或 route.continue())。
8. 无法让构建失败的“门禁”
GA4 DebugView、仅用于生产环境的监控,或只警告但仍通过的检查,都无法阻止回归。让 CI 在事件丢失或缺少必需参数时以非零状态码退出。
验证
先做最小检查——证明测试套件确实能捕获回归,而不是只相信绿色的运行结果:
- 一个事件,一个信标:
npx playwright test -g add_to_cart能通过,并且其跟踪记录(--trace on)显示捕获到的/g/collect请求中包含en=add_to_cart。如果测试通过,但跟踪记录中没有匹配请求,说明你断言的对象不对。 - 门禁确实能失败: 在一个分支中重命名必需参数(例如,在应用中将
value改为amount),然后重新运行 CI 项目——构建必须以非零状态码退出,并显示missing/违规信息。如果仍然通过,这道门禁就是摆设。 - 同意门禁能检测错误的应用状态: 实际默认值与所选策略相符;缺失或已授权的默认值会导致全拒绝策略 fixture 失败。新增操作需要新增更新和计划事件。健康与故意破坏的基础和高级 fixture 都应产生预期结果。
- 去重有效: 客户端
eid(来自facebook.com/tr?ev=Purchase)应等于同一订单的服务端 CAPIevent_id。值不同时,Meta 会重复计数。
完成标准
- 每个被跟踪事件都有一项测试,用于拦截真实的收集端点信标(
/g/collect、facebook.com/tr等),并从searchParams/postData中解析en=/ev=及参数——不能只断言 DOM 或 dataLayer。 - 存在一个带版本号的跟踪计划文件(JSON/YAML/TS),其中包含必需参数及其类型;测试会根据该计划验证捕获的事件,并报告
missing/类型违规,而不只是验证事件名称。 - 购买事件(或任何客户端与服务端都发送的事件)都有去重断言:客户端信标的
event_id等于服务端 CAPI 的event_id。 - 同意测试会观察全部四种实际信号,要求配置的事件,并根据获批的区域/基础/高级策略检查存储状态。它们绝不会预先写入自己要验证的同意状态。传输诊断会明确说明其限制。
- 存在可复用的捕获 fixture(
test.extend),能够收集 GA4、Meta、TikTok 和 LinkedIn 信标;没有测试重新实现捕获逻辑,且捕获辅助工具不会调用route.abort()。 - 新闻媒体页面(如有)会覆盖 article_view、通过真实滚动操作触发的 25/50/75/100 滚动深度,以及付费墙/计量事件——全部使用
waitForRequest,不使用waitForTimeout。 - CI 任务会运行测试套件,将捕获的事件与跟踪计划基线进行差异比较,并在事件丢失或缺少必需参数时以非零状态码退出——通过故意破坏标签并确认构建变红来验证。
相关技能
- compliance-testing — 判断依据 GDPR/CMP 同意法规、Cookie 同意界面和数据主体权利,信标是否允许触发。本技能假定信标已获准触发,并检查其数据是否正确;如需判断是否合法,请参阅该技能。
- playwright-automation — 页面对象模型、fixture、配置,以及本技能赖以实现拦截的一般端到端测试模式。
- api-testing — 直接验证服务端 Conversions API / Measurement Protocol 调用(请求正文、身份验证、响应);当你需要断言去重的服务端部分时,请参阅该技能。
- qa-project-context — 本技能的前期探索问题会先读取技术栈、标签管理器、同意平台和目标环境等信息。
参考文件(位于 references/)
- ga4-interception.md — 完整的 GA4
/g/collect拦截辅助工具、批量事件 POST 请求正文解析、区域端点处理。 - tracking-plan.md — JSON 电商契约、可复用的
validateEvent辅助工具,以及针对架构和场景值的独立断言。 - datalayer-capture.md —
addInitScriptdataLayer.push 包装器和电商items[]形状断言。 - pixels-and-dedup.md — Meta/TikTok/LinkedIn 载荷解析,以及客户端与服务器 CAPI
event_id去重捕获。 - consent-mode.md — 被动观察四种信号、基础/高级方案、操作检查点、完成信号,以及有限范围的
gcs=/gcd=诊断。 - news-media.md — article_view、去重后的 25/50/75/100 滚动分段,以及付费墙计量事件测试。
- capture-fixture.md — 可复用的多像素
test.extend捕获夹具。 - ci-gating.md — GitHub Actions 工作流、基线差异脚本,以及缺失参数故障的呈现方式。