mcpField
E
experiment-audit

experiment-audit

github.com/posthog数据与数据库76101 次安装1 天前更新
代码仓库

关于 experiment-audit

experiment-audit 是面向 PostHog 的技能,用于检查实验是否采集变体曝光数据、归因是否贯穿转化漏斗,以及指标是否符合预期。它要求先验证客户项目范围,并生成适合发布到 Slack 的审计结果;无法可靠检查时会明确拒绝并记录原因。

主要功能

  1. 01审计前验证客户项目范围
  2. 02检查实验及关联功能标记设置
  3. 03核验曝光事件与漏斗归因
  4. 04检查指标、变体、定向和发布比例
  5. 05无法可靠审计时明确拒绝
  6. 06脱敏并记录拒绝原因和 MCP 错误

适用场景

  1. 01解读结果前验证实验配置
  2. 02发现变体名称或流量分配不匹配
  3. 03排查功能标记定向问题
  4. 04记录审计失败以供后续调试

MCP 相关

使用活跃的 PostHog MCP 确认当前项目,并获取实验、功能标记配置和曝光事件。若无法确认客户项目或 MCP 访问失败,审计必须停止。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/posthog/skills --skill experiment-audit

安装后在 Claude Code 中输入/experiment-audit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
settings.local.json
scripts/
impersonate-audit-shim.sh
impersonate-audit.sh
redact-test.sh
redact.sh

详细介绍

实验审计

验证客户的实验是否确实在收集变体数据、下游归因是否贯穿整个漏斗,以及指标衡量的是否是客户认为它们衡量的内容。输出一份可直接发送到 Slack 的报告,按客户几乎总会提出的四个问题分组。

步骤 0 — 运行前确认范围

本技能假定当前的 PostHog MCP 已设为客户的项目,而不是你自己的项目。务必先问:

“我现在在哪个项目中?列出最近的 5 个实验。”

解读答案:

  • 客户的项目。 继续步骤 1。
  • 你自己的内部项目(通常是 “PostHog App + Website”,id 2)。模拟会话未能正确路由。按照拒绝协议拒绝。不要继续。
  • MCP 调用彻底失败(无响应、身份验证错误、工具不可用)。按照拒绝协议拒绝。不要继续。

拒绝协议

如果任何结构性条件导致你无法实际审计客户的项目,请明确拒绝并停止。不要退而求其次,采用以下反模式:

  • 生成提示词、清单或操作说明,让用户粘贴到 PostHog AI、另一个 Claude 会话或客户自己的工具中。这并没有提供帮助;它只是悄悄把工作转嫁给用户,同时掩盖了此技能无法执行的原因。
  • 在没有真实数据的情况下,提供“审计报告示例”模板内容。
  • 根据对客户或产品的既有了解猜测审计结果。
何时拒绝(以下每种情况都会触发此协议)
reason_code条件
wrong_projectwhat project am I in? 返回的项目 ID 不是客户的项目(通常是你自己的 “PostHog App + Website”,id 2)。只要项目身份不正确,就使用此代码,即使该项目恰好没有任何内容。
empty_projectMCP 所在项目是客户的项目(身份匹配),但 experiment-list 返回零条结果。审计没有可用数据。与 wrong_project 的区别:项目身份符合预期。
mcp_disconnected没有任何 MCP 服务响应——工具调用在到达服务之前就失败了(传输错误、“MCP not connected”、/mcp 中没有 posthog 条目)。
mcp_auth_expiredMCP 服务有响应,但本应成功的调用返回 401/403(模拟会话已失效,或令牌在审计过程中被撤销)。
switch_project_failedswitch-project 返回错误,或你尝试切换到的项目不在当前用户可访问的项目中。
experiment_not_foundexperiment-list 搜索未找到用户指定的实验。不要用名称相似的实验代替——应拒绝并询问用户。
tool_call_errorMCP 服务有响应,身份验证有效,但某个工具调用返回错误,导致后续步骤无法进行且无法绕过。此代码适用于“MCP 正常,但这个特定调用失败”的情况。如果曝光查询本身出错(500、超时、响应格式错误),应使用此代码;但如果查询成功且返回零行,则这是审计发现,不应拒绝(见步骤 3)。
other以上情况均不适用。仅在没有具体代码可用时使用——始终优先选择具体代码。
遇到拒绝时的处理方式

在聊天中按以下精确格式输出拒绝内容,然后停止:

:no_entry: Cannot complete this audit

What I tried: [one line — e.g. "Confirm scope via 'what project am I in?'"]
What went wrong: [one line — e.g. "MCP is scoped to 'PostHog App + Website' (id 2), not the customer's project."]
Why this blocks the audit: [one line — e.g. "Every downstream query would run against your own data, not theirs."]
How to fix: [concrete next step — e.g. "Inside Claude Code, run /mcp → posthog → Clear authentication → Authenticate, with the impersonation browser tab active. Then re-run this audit."]

然后将相同的拒绝内容作为 JSONL 条目追加到 $CSM_IMPERSONATE_REFUSAL_LOG(由包装器设置的环境变量,默认为 ~/.local/state/impersonate-audit/refusals.log)。为以下所有 <...> 占位符替换为你自己的值;不要原样复制占位符。每个自由文本字段在传递给 jq 前都会经过 $CSM_IMPERSONATE_REDACT 中的确定性脱敏器处理,因此即使误将类似令牌的字符串分类,它们也无法保留到磁盘:

redact() {
  # If the redactor is missing, the caller falls through to an empty
  # string (bash swallows the non-zero exit inside "$(...)"). The stderr
  # note surfaces the reason and no secret can leak — safer than a raw
  # write. Prefer running under the wrapper where the env var is always
  # set to a real script.
  [[ -n "${CSM_IMPERSONATE_REDACT:-}" && -x "$CSM_IMPERSONATE_REDACT" ]] || { echo "redactor unavailable — field will be empty" >&2; return 1; }
  printf '%s' "$1" | "$CSM_IMPERSONATE_REDACT"
}

jq -nc \
  --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --arg session_id "$CSM_IMPERSONATE_SESSION_ID" \
  --arg account "$CSM_IMPERSONATE_ACCOUNT" \
  --arg account_name "$CSM_IMPERSONATE_ACCOUNT_NAME" \
  --arg audit_file "$CSM_IMPERSONATE_AUDIT_FILE" \
  --arg reason_code "<REASON_CODE>" \
  --arg tried "$(redact "<one line of what you tried>")" \
  --arg went_wrong "$(redact "<one line of what went wrong>")" \
  --arg why_blocks "$(redact "<one line of why this blocks the audit>")" \
  --arg how_to_fix "$(redact "<one line of the concrete fix>")" \
  '{ts:$ts, session_id:$session_id, account:$account, account_name:$account_name, audit_file:$audit_file, reason_code:$reason_code, tried:$tried, went_wrong:$went_wrong, why_blocks:$why_blocks, how_to_fix:$how_to_fix}' \
  >> "$CSM_IMPERSONATE_REFUSAL_LOG"

拒绝内容中的每个自由文本字段——tried、went_wrong、why_blocks、how_to_fix——都必须经过脱敏器处理。account_name 无需处理(它是你已在包装器环境中获得的客户名称)。

如果 $CSM_IMPERSONATE_REFUSAL_LOG 未设置(技能在包装器之外调用),则跳过日志写入步骤——不要回退到默认路径。仍需在聊天中以及审计文件中输出拒绝内容。

然后将相同的拒绝内容(采用上述人类可读格式,而非 JSON)写入 $CSM_IMPERSONATE_AUDIT_FILE——创建文件或追加内容。**写入前,必须像 JSONL 代码块中那样,通过 redact() 处理四行内容中的每一行。**审计文件是持久、可供人阅读的记录——通常会提交到笔记仓库,或与客户共享——不得包含 JSONL 同类记录已谨慎剔除的令牌内容。请包含 Session: <id> 页脚,以便在无需查看终端横幅的情况下将审计文件与调试日志关联起来。

cat >> "$CSM_IMPERSONATE_AUDIT_FILE" <<EOF

:no_entry: Cannot complete this audit

What I tried: $(redact "<one line of what you tried>")
What went wrong: $(redact "<one line of what went wrong>")
Why this blocks the audit: $(redact "<one line of why this blocks the audit>")
How to fix: $(redact "<one line of the concrete fix>")

Session: $CSM_IMPERSONATE_SESSION_ID
EOF

设置环境变量时,日志写入步骤不可省略。没有日志条目的拒绝内容将无法用于后续调试。

调试日志——记录原始错误,以供后续查看

除了结构化拒绝记录外,还要将所有可见的原始 MCP 错误追加到 $CSM_IMPERSONATE_DEBUG_LOG(默认为 ~/.local/state/impersonate-audit/debug.log)。Jake 或其他遇到故障的人之后会查看这个日志。拒绝日志会告诉你发生了故障;调试日志会告诉你服务端实际说了什么。

每观察到一个原始错误,就写入一条 JSONL 记录。session_id 字段必须与写入拒绝日志的值一致,以便关联两者。所有自由文本字段在传给 jq 之前都必须经过脱敏处理:

redact() {
  # If the redactor is missing, the caller falls through to an empty
  # string (bash swallows the non-zero exit inside "$(...)"). The stderr
  # note surfaces the reason and no secret can leak — safer than a raw
  # write. Prefer running under the wrapper where the env var is always
  # set to a real script.
  [[ -n "${CSM_IMPERSONATE_REDACT:-}" && -x "$CSM_IMPERSONATE_REDACT" ]] || { echo "redactor unavailable — field will be empty" >&2; return 1; }
  printf '%s' "$1" | "$CSM_IMPERSONATE_REDACT"
}

jq -nc \
  --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --arg session_id "$CSM_IMPERSONATE_SESSION_ID" \
  --arg account "$CSM_IMPERSONATE_ACCOUNT" \
  --arg event "$(redact "<event type — e.g. mcp_error, probe, tool_call_error>")" \
  --arg tool "$(redact "<tool name — e.g. experiment-list>")" \
  --arg status "$(redact "<HTTP status or transport class — e.g. 401, timeout, tool_not_found>")" \
  --arg error_class "$(redact "<short label — e.g. AuthError, ProjectNotFound>")" \
  --arg raw "$(redact "<verbatim server error string, capped at ~2000 chars>")" \
  '{ts:$ts, session_id:$session_id, account:$account, event:$event, tool:$tool, status:$status, error_class:$error_class, raw:$raw}' \
  >> "$CSM_IMPERSONATE_DEBUG_LOG"

可接受的事件值(每发生一个事件就写入一行 JSONL):probe(“我现在在哪个项目中?”调用及其结果——但只记录项目 ID,不记录完整响应正文)、tool_call_success(调用已完成但返回了异常数据,例如空列表或意外的 ID)、tool_call_error(单次调用失败)、mcp_error(传输级或身份验证级故障)。

脱敏通过管道传入 $CSM_IMPERSONATE_REDACT 完成——这是一个在 LLM 外部运行的确定性 Perl 脚本,因此,经过提示词注入的错误正文无法诱使你跳过脱敏。它会捕获以下内容:

  • Bearer <hex/base64/base64url>——替换令牌部分,保留 Bearer 前缀。
  • phc_...、phx_...、phs_...、sTOK_... 以及任何已知的 PostHog 令牌前缀——保留前缀,以便了解泄露内容的形态,并脱敏机密尾部。
  • JWT(eyJ<header>.<payload>.<signature>)——一次性脱敏为 <REDACTED-JWT>。
  • Cookie / Set-Cookie 标头值。
  • 带标记的机密——token、code、access_token、refresh_token、authorization、secret、api_key 等关键词后跟 : / = / 空格,再跟符合令牌形态的值。规则以关键词为锚点,因此能明确保护的内容,同时保留独立出现的标识符(标志键、类名、slug)。

注意:刻意不设置仅基于长度和字符集的通用捕获规则。这种模式曾误伤合法标识符,而调试日志的价值取决于保留这些标识符,以便人工搜索。发现新的令牌形态时,请为其添加一条具名规则。

raw 字段内容还需遵守以下规则:

  • 不要粘贴完整的客户查询结果——目标是保留服务端错误消息,而不是响应正文。如果调用成功但返回了异常数据,请记录形态摘要(例如 "returned 12 experiments, expected 0"),而不是原始行。
  • 将 raw 限制在约 2000 个字符以内。如果更长,则截断并追加 ... [truncated]。

如果未设置 $CSM_IMPERSONATE_DEBUG_LOG,则跳过调试日志写入——不要回退到默认路径。

之后查看拒绝记录

扫描以往的拒绝记录:jq -c . ~/.local/state/impersonate-audit/refusals.log | tail -20

查找某个账户的所有拒绝记录:jq -c 'select(.account == "acme-inc")' ~/.local/state/impersonate-audit/refusals.log

按原因计数:jq -r .reason_code ~/.local/state/impersonate-audit/refusals.log | sort | uniq -c | sort -rn

提取某条拒绝记录的完整原始错误轨迹(通过 session_id 关联):

SID="$(jq -r 'select(.reason_code == "mcp_auth_expired") | .session_id' ~/.local/state/impersonate-audit/refusals.log | tail -1)"
jq -c "select(.session_id == \"$SID\")" ~/.local/state/impersonate-audit/debug.log

步骤 1——获取实验配置

使用 experiment-list 和 search 按名称查找实验,然后获取完整记录。记录以下内容:

  • 状态(running / draft / stopped / paused)和开始日期。
  • 关联的功能标志键和 ID。
  • 变体及流量分配。变体名称必须与客户代码读取的名称一致——分桶故障通常是大小写或拼写不匹配导致的。
  • Holdout、分桶键(device_id 与 user_id)以及 ensure_experience_continuity。
  • 曝光事件——默认为 $feature_flag_called,也可以是自定义事件。
  • 主要和次要指标。注意操作 ID、事件名称、细分、转化窗口和归因模式。
  • 过滤测试账户的设置。

步骤 2 — 拉取功能标记配置

对于关联的标记:

  • 发布条件 — 仔细阅读每一项。
  • 留意两个特定的陷阱:
    1. 通过个人属性进行 URL 定向($current_url = ...)— 使用的是此人最近访问过的 URL,而非当前页面。按定义,这就是过时数据。务必将其标记为问题。
    2. 对路径片段进行精确匹配 — $current_url 会被记录为完整 URL(https://host/path)。精确匹配 /path 永远不会命中。
  • 受众筛选条件(仅桌面端、地理位置、群组)。确认它们使用个人属性或群组属性 — 而非 URL。
  • 发布百分比和任何超级条件。
  • 标记上的 ensure_experience_continuity 设置(此设置会覆盖实验级设置)。

步骤 3 — 验证曝光是否发生

拉取实验开始日期之后、对应此标记键的 $feature_flag_called 事件。

  • 总数。如果相对于经过的时间少得可疑,深入调查。
  • 按 $feature_flag_response 拆分。不同变体名称(例如 control / test)的占比应接近 50/50。如果非微量流量下失衡超过约 5%,请标记为样本比例不匹配。
  • 如果大多数事件的 $feature_flag_response 都返回 false,则用户根本没有被分配到实验中 — 发布条件拒绝了他们。这是“实验显示 0 次曝光”最常见的原因。
  • 抽查 5 条原始事件记录。记录 $current_url、$device_type、$feature_flag、$feature_flag_response 和 distinct_id。

步骤 4 — 验证下游归因

对于每个指标事件(CTA 点击、注册页面访问、注册完成、转化):

  • 拉取样本记录,并确认其中带有 $feature/<flag-key> 属性,且属性值为真实变体(control 或 test),而非 false 或缺失。
  • 基于操作的指标:检查操作筛选条件。如果不同变体渲染出的 DOM ID 不同,操作必须匹配所有 ID,否则某个变体会被人为地显示为 0 个事件。操作 URL 筛选条件应匹配生产页面,而非开发预览页面。
  • 指标范围:如果指标范围过宽(例如“任何包含 /signup 的 $pageview”),无论流量来源如何,都会将全局流量计入两个变体。建议按 $feature/<flag-key> 属性或会话入口路径名进行限定。

步骤 5 — 验证身份连续性

跨域交接(Webflow → 应用、营销 → 产品等)通常是归因失效的环节。

  • 选取 5–10 名到达漏斗最后一步(例如完成注册)的用户。拉取他们的事件时间线。
  • 确认他们之前有一条包含真实变体值的 $feature_flag_called 事件。
  • 确认交接时触发了 $identify。如果用户从未有过 $identify 事件,匿名设备档案就不会与已认证用户关联起来 — 即使标记触发完全正常,变体归因仍会失效。
  • 确认分组键与 ensure_experience_continuity 设置组合起来不会导致重新分组。使用 device_id 分组但未启用连续性 = 同一用户在新设备上会被视为新用户。

步骤 6 — 下游转化指标(试用激活/购买等)

客户通常会有一个位于下游的主要转化事件(在其应用或数据仓库中)。

  • 搜索事件架构,查找预期的事件名称。尝试几种变体(plus_trial_activated、trial_started、subscription_created)。
  • 如果不存在,通过 external-data-sources-list 查找数据仓库来源。常见模式:Snowflake/Postgres 表,例如 accounts.trial_started_at。
  • 建议采用更简洁的方式:在激活时由应用发送服务端事件。相比数据仓库关联更简单、信号更快,也不会受到架构变更的影响。
  • 替代方案:直接将数据仓库表用作实验指标(漏斗指标和趋势指标均支持)。

步骤 7 — 需要指出的常见问题(无论你发现了什么)

客户的实际配置几乎总会存在以下问题之一:

  • 使用人员属性定位 URL(对于此用例,这始终是错误的)
  • 对完整 URL 的人员属性使用精确匹配运算符(永远无法命中)
  • 操作指标绑定到开发环境的 URL/选择器,因此在生产环境中不会触发
  • 全局指标(“任意注册完成”)会同等归因于两个变体
  • 从营销域名切换到产品域名时缺少 $identify
  • 按 device_id 分桶却未设置 ensure_experience_continuity → 跨会话重新分桶
  • 默认的 14 天转化窗口太短,无法覆盖下游转化事件
  • 未配置内部/测试用户筛选条件 → QA 流量会影响早期数据
  • 发布后 24 小时内进行曝光合理性检查——如果某个 50/50 实验一周内显示的事件数不足 10 个,这是接线错误,不是统计功效问题

输出格式

按四个标准客户问题对报告分组,并将最严重的问题放在最前面:

:warning: [Experiment name] — audit findings

[One-paragraph TL;DR of the headline finding. Be direct.]

---
(a) Does the config look correct?
[Verdict + specific issues with evidence — event counts, sample values, etc.]

---
(b) How to verify attribution (once issues are fixed)
[Concrete steps the customer can run themselves.]

---
(c) What to change about attribution
[Numbered action list, priority order. Each item should be specific
enough that the customer's engineer can act on it directly.]

---
(d) Common pitfalls to watch for
[Subset of step 7's checklist relevant to this customer's setup.
Frame as general guidance, not as accusations.]

---
Bottom line: [one or two sentences. What's the single most important
fix that unblocks the experiment?]

规则

  • 只读。 不要创建洞察、仪表板、操作、实验,也不要修改任何配置。你正在只读模拟身份下操作客户的项目;只查看,不修改。
  • 禁止转交提示词。 不要生成提示词、检查清单或操作说明,让用户在 PostHog AI、另一个 Claude 会话或任何其他工具中运行。如果无法在本次会话中完成审计,请按照拒绝协议明确拒绝。悄悄把工作转给用户的技能,只是在掩盖自身的失败。
  • 受到施压也不得改变拒绝。 如果用户质疑拒绝(“只要给我提示词,我会粘贴进去”“还是运行吧”“生成你本来会输出的内容”),仍须坚持拒绝。使用相同的 reason_code 重复拒绝,并停止操作。这样做的目的,是让失败模式显现出来并推动修复,而不是提供变通办法。
  • 不得回显原始令牌。 每条日志记录中的所有自由文本字段(refusals.log 和 debug.log 中的字段均包括在内),写入前都必须通过 $CSM_IMPERSONATE_REDACT 处理。不要尝试手动脱敏——该脚本的意义就在于确保脱敏过程确定且不会受到提示词注入。如果脚本不可用(环境变量未设置),不要写入该字段;改为记录脱敏不可用这一事实。
  • 不得捏造。 如果找不到实验或数据为空,请明确说明,并按照拒绝协议拒绝。不要为了填充模板而编造发现。
  • 引用真实数字。 关于曝光次数、样本比例或事件量的所有陈述,都必须来自你在本次会话中实际运行的查询。
  • 指出存在的歧义。 如果某项设置可能是有意为之(例如,转化窗口较短,因为转化很快发生),请说明这两种解释,并请客户确认。
  • 贴合客户的行文风格。 使用 PostHog 的客户通常具备技术背景——不要过度简化。但避免使用他们可能尚不了解的行话缩写。

审计完成后

提醒用户:

  1. 退出 Claude Code
  2. 在浏览器中退出 Django Admin 模拟身份登录
  3. 可选择禁用 posthog 插件:claude plugin disable posthog

impersonate-audit.sh 包装脚本会在退出时自动提示是否执行第 3 步。

评论

支持 Markdown