PostHog 运行时加载审计实战:用 Playwright 双轮扫描定位追踪器丢失与低计数根因
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
本文是 PostHog 开源仓库中 Web Analytics 支持工单排查技能(triaging-web-analytics-support)的核心参考文档 loading-audit.md 的深度展开:当客户反馈"数据比某竞品低""GTM 加载了但没数据""装了广告拦截器就没了"这类工单时,静态代码审查无法覆盖 GTM、同意管理平台(CMP)与广告拦截器的真实运行效果,只有对线上站点做运行时加载审计才能还原真相。读完本文,你将掌握两轮 Playwright 审计的完整流程、关键判读指标(加载方式、首请求时延、同意地域门控)、四大注意事项,以及仓库中可复用的现成工具与源码级依据。
一、为什么静态审计不够,必须做运行时审计
PostHog 安装向导(wizard)的审计模式本质上是对客户仓库的静态代码审查——它检查代码里 snippet 是否存在、posthog.init配置是否正确,但看不到浏览器里实际发生的事。正如 loading-audit.md 开篇所指出的:
这是 wizard 审计模式不做的事——线上站点审计是唯一能看到 GTM、同意管理和广告拦截器影响的途径。
原因很直白:追踪器的真实命运取决于三条运行时链路,而这三条链路全部无法从静态代码推断:
- 投递链(delivery chain):SDK 是直接写在
<head>里的 snippet,还是经由 Google Tag Manager 注入,还是打进前端 bundle? - 同意链(consent chain):CMP 是否在 GDPR 区域外根本不加载?它解析完成前用户是否已离开?
- 拦截链(blocker chain):广告拦截器(如 EasyPrivacy 规则)是否在请求发出前就 abort 了脚本或接口?
在 diagnostic-playbooks.md 的工单分类中,这类问题对应"Tracker not loading / undercounts competitor"形态,其首要动作就是"针对客户线上页面运行运行时加载审计"。
二、审计流程:两轮 Playwright(Chromium)扫描
审计的标准做法是对 2~4 个代表性 URL 跑两轮访问。URL 的选择建议覆盖三类页面:落地页(landing page)、一个深层页面(deep page)、一个带 UTM 参数的页面——分别对应入口流量、站内深链和营销渠道流量的加载特征。
第一轮:Normal pass(正常画像)
以真实用户 UA、美区 locale/时区访问页面,完成两件事:
- 记录所有指向追踪器域名的请求,时间戳以导航开始(navigation start)为基准(即"页面开始加载后第几毫秒发出")。
- 加载完成并等待约 10 秒后,在页面内(in-page)评估以下运行时状态:
window.posthog:__loaded是否置位、config.api_host、config.token前缀、person_profiles取值;- 标签管理器容器(GTM container)是否存在、
dataLayer长度; - 同意平台对象及其解析后的状态(是否已 resolve、是否授出 consent);
- 竞品追踪器的全局变量与 script 标签。
第二轮:Blocklist pass(拦截画像)
除"abort 掉命中 EasyPrivacy 风格域名列表的请求"(典型如googletagmanager.com、google-analytics.com、各类 analytics 供应商域名)外,其余完全相同。两轮结果对比,即可回答"哪些追踪器在广告拦截下存活"。
仓库中 tools/traffic-sim 正是围绕这套思路实现的工具化版本,其check-loading场景在每个 URL 上导航真实浏览器、等待 PostHog 初始化后,通过一段注入页面的检测脚本(cli.py 中的POSTHOG_DETECT_JS)采集:
window.posthog是否定义且__loaded;- 加载方式:
head_snippet/snippet/array_js_only/none; - snippet 位于文档
<head>还是<body>; - 初始化配置:
api_key、api_host、person_profiles; - 运行时状态:包括已分配的
distinct_id。
检测脚本的具体判定逻辑(从源码可见):先过滤所有 script 标签中 src/id/textContent 命中posthog、array.js、phc_、ph_init的候选,再用正则!function\s*\([a-z],[a-z]\)\s*\{[^}]*__SV识别官方 web snippet,用posthog.init(...)正则解析出 init 配置,最后按"__loaded+#posthog-init元素是否存在"推导加载方式。
三、结果判读:三个决定性指标
两轮扫描产出的数据量很大,判读时聚焦三个维度:
1. 加载方式(Load method)——投递链是最大弱点
区分三种加载形态:<head>原生 snippet、经标签管理器(GTM)投递、打进前端 bundle。
关键结论来自源码级推理与实战反复验证(见 loading-audit.md 与 diagnostic-playbooks.md):
即使
api_host做了反向代理(endpoint 无法被轻易拦截),只要 SDK 是通过 GTM 加载的,拦截googletagmanager.com照样会让整个追踪链死掉——投递链是最薄弱环节。
这也是check-posthog-loading技能里被列为重点红线的原因:load_method: array_js_only且无 init 配置,说明array.js加载了但posthog.init()从未被调用(常见于手工安装只做了一半);页面间load_method不一致则提示部分页面用了不同模板(详见 check-posthog-loading/SKILL.md)。
2. 首请求时延(First-request delta)——快速跳出的盲区窗口
比较追踪器的首个网络请求发生在导航后第几毫秒,并与竞品脚本的首请求时间做差。这个差值就是"快速跳出盲区":在标签管理器 + CMP 异步解析完成之前,用户若已离开页面,posthog一行数据都不会发出——快速跳出的访客在 consent 解析窗口内是完全不可见的。
3. 同意地域门控(Consent geo-gating)——别把 CMP 延迟算成横幅
同意平台常见的实现是仅在 GDPR 区域加载;因此非 GDPR 区域页面上出现延迟时,延迟来源是标签管理器的启动时间(tag-manager boot),而不是同意横幅本身。判读时必须先分清"这 500ms 到底花在了哪一环"。
四、审计的四大注意事项(Caveats)
原文给出了三条必须遵守的纪律,第四条来自仓库工具的补充设计:
- 两大主流 SDK 都会抑制自动化:posthog-js 与多数竞品会检测
navigator.webdriver,在无头浏览器中不发送任何事件。因此审计中看到"零 capture 请求"是预期行为,说明不了真实用户的表现——断言对象应是脚本/运行时存在性与时机,而不是事件 POST。值得对照的是,cli.py 的_new_context通过add_init_script把navigator.webdriver定义为undefined以模拟真实用户发事件;而加载审计按原文纪律只断言存在性与时机。 ?utm_...测试参数无害,但严禁注入虚构转化事件到客户项目。仓库工具也恪守此边界:traffic-sim README 明确列出"本工具不做的事"——它只观察客户站点自然发出的 PostHog 事件,不注入任何测试流量;add_tracking_params(cli.py)仅追加__posthog_debug、run_id、scenario三个无害参数。- 数字型主机名会被
new URL()解析为 IPv4:对合成输入(synthetic inputs)不要断言精确的解析后 host。仓库实现同样做了防护——cli.py 用urlparse(url).hostname且包了try/except ValueError,命中不了任何已知 PostHog 域名的请求直接排除,避免把"路径里恰好含 posthog.com 字符串"的请求误判为上报流量。 - (补充纪律)比较竞品时要口径对齐:竞品在访问定义、机器人过滤、无 cookie 计数上各不相同,应先把加载链差距量化,再去讨论口径差异(见 diagnostic-playbooks.md)。
五、把审计变成可复用的工具:仓库里的现成资产
如果你要落地这套审计,仓库已经提供了三层现成资产,无需从零写 Playwright 脚本:
1.traffic-simCLI(全功能实现)
cli.py 是原文"prior art"中提及的全功能 CLI 在开源仓库的落地形态(check-loading、new-user、returning-user三个场景)。安装与运行:
# 首次:在 monorepo 中同步 Python 环境并安装 Chromium uv sync uv run playwright install chromium # 检查 snippet 在你关心的每个 URL 上是否加载 uv run python tools/traffic-sim/cli.py check-loading \ --url https://example.com/ \ --url https://example.com/pricing \ --url https://example.com/blog常用选项(cli.py):
--posthog-host https://eu.i.posthog.com:EU 云;自建反向代理可传https://ph.example.com;--urls-file urls.json:从 JSON 批量加载 URL(支持扁平列表或{base_url, categories}结构,参考 urls.example.json);--headed:显示浏览器窗口,便于现场调试;--verbose:打印每条 PostHog 请求与 console 行;--cloud:切到 BrowserStack 云端运行(需额外安装 browserstack-sdk/pyyaml 并配置凭证)。
几个值得了解的源码细节:
- 域名匹配集合由 resolve_posthog_domains 生成:始终包含云默认域名(
posthog.com、i.posthog.com),即使--posthog-host指向自定义反向代理,云上报流量也不会漏抓; - 结果以 JSON 落盘到
tools/traffic-sim/results/,文件名含场景、run_id 与时间戳; - 单测覆盖了 console 事件提取、URL 参数追加、域名集合解析等关键逻辑(tests/test_cli.py)。
2.console-snippet.js(零依赖的 DevTools 手工检查)
不想装环境时,把 console-snippet.js 粘贴进浏览器 DevTools 控制台即可拿到同一套检测结果:script 标签清单、init 配置(含ui_host、session_recording)、运行时状态、加载方式,甚至识别 Next.js 服务端注入的self.__next_s场景。适合单页快速抽查,也是审计前的"手动冒烟"。
3. MCP 工具与 Claude Code skills(编排层)
同一套操作通过 mcp_server.py 暴露为 MCP 工具(check_posthog_loading、simulate_new_user、simulate_returning_user),并由 skills/ 下的 Claude Code skill 编排,例如 check-posthog-loading/SKILL.md 明确给出了结果判读红线:
loaded全空、not_loaded全满→ snippet 哪都没装上,重新执行安装或检查布局模板;loaded/not_loaded混合→ 部分页面(常因不同模板/布局渲染)丢了 snippet;- 页面间出现多个不同
api_key→ 有页面指向了错误的 PostHog 项目; - 页面间出现多个不同
api_host→ 有页面指向了错误的接入端点(EU 云 vs US 云、或 vs 自建反向代理),事件会落到错误的项目; load_method: array_js_only且无 init 配置→posthog.init()从未调用。
4. 输出建议:逐页对比表
原文强调,逐页对比表能让"部分迁移状态"一目了然。表头建议:URL、加载方式、snippet 位置、配置 key/host、与基线是否匹配(match vs baseline)。缺失追踪器的页面会在表中瞬间暴露——这正是把两轮审计结果落成可交付结论的标准形态。
六、总结:审计纪律速查
最后把整篇审计方法论压缩成一张可执行清单:
| 环节 | 要点 |
|---|---|
| 选 URL | 落地页 + 深层页 + 带 UTM 页,共 2~4 个 |
| 跑两轮 | Normal pass(真实 UA/US 时区)→ Blocklist pass(abort EasyPrivacy 域名) |
| 判读加载方式 | <head>snippet / GTM / bundle;GTM 投递在拦截下必死 |
| 量首请求时延 | 与竞品对比,差值即快速跳出盲区窗口 |
| 分清延迟来源 | 非 GDPR 区延迟 = 标签管理器启动,不是横幅 |
| 断言对象 | 脚本存在性与时机,不要断言事件 POST(SDK 会抑制自动化) |
| 纪律红线 | 不注入虚构转化事件;不断言合成输入的数字主机名 |
| 复用工具 | cli.py、console-snippet.js、check-posthog-loading skill |
| 交付形态 | 逐页对比表(加载方式 / snippet 位置 / config / 基线匹配) |
运行时加载审计的核心心法只有一句:投递链决定生死,首请求时延决定可见度,consent 门控决定地域差异——把这三件事量化出来,"低于竞品"的工单就有了可落地的证据链。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考