Sentry + PostHog 闭环实战:用“统一事件封装 + 关联键 + 导出链路三件套”把线上问题变成可定位、可验证、可回归
Sentry + PostHog 闭环实战:用“统一事件封装 + 关联键 + 导出链路三件套”把线上问题变成可定位、可验证、可回归
很多人都听过“可观测的正确打开方式”是反馈闭环:信号→定位→修复→验证→护栏。 但如果没有一个能落地的案例,这套方法很容易停留在概念层,最后变成“接了工具,但没人用”。
这篇文章用一个典型的前端重链路:导出文件(PPTX/PDF/PNG/...),演示如何把 错误信号(Sentry)与行为信号(PostHog)串起来, 让你能回答四个关键问题:
- 发生了什么?(错误类型/失败点/堆栈/上下文)
- 影响谁、影响多大?(失败率、受影响会话数、按格式/语言/版本分桶)
- 为什么会发生?(关联到路由/版本/语言/文档属性/功能开关)
- 修复是否真的变好?(对照指标 + 回归门禁)
TL;DR(30 秒讲清楚)
- 核心动作:把“导出链路”打成三件套:
export_clicked→export_completed/export_failed(可选加export_started)。 - 统一封装:一个
track()入口做事件 schema、脱敏与采样;一个captureException()入口补齐 tags/extra。 - 关联键:
release、route、locale、session_id是基础;导出类链路再加trace_id、doc_id、export_format、slide_count_bucket。 - 隐私与噪声:默认关闭 autocapture;用白名单限制采集页面;杜绝 PII/secret;对成功事件采样、对失败事件高采样。
- 验收指标:导出失败率、导出耗时 p95、Crash-free sessions、MTTD/MTTR;上线后与上一版本对照。
问题定义(Problem Statement)
对“导出文件”这条关键链路建设可观测闭环:当导出失败或变慢时,能快速定位到影响面与根因;修复后能用指标对照验证;并把高频问题沉淀为回归样本与发布门禁,避免反复退步。
真实链路(导出案例:从点击到下载)
先把链路讲清楚,你才能知道信号该打在哪里。一个常见的浏览器导出链路会涉及“新窗口/跳转/关闭窗口”等副作用:
- 用户点击导出按钮:选择 format(pdf/pptx/png/...)。
- 前置校验:先发一个“justCheck”请求,确认是否允许导出(权限/额度/状态)。
- 打开导出页:用新窗口进入导出页(避免阻塞主页面),必要时处理弹窗拦截的降级。
- 导出页调用导出接口:拿到
exportUrl后跳转到真实下载地址。 - 成功后自动关闭:部分 format 可以在下载触发后延迟关闭窗口。
- 失败路径:网络超时/权限失败/导出渲染异常/跨域资源失败,都会导致“用户看到失败,但你后台看不懂”。
关键伪代码(读者可复现)
1) 统一事件封装(AnalyticsProvider):schema + 脱敏 + 采样
埋点的质量取决于“统一入口”。否则你会得到一堆不兼容的事件:同一个字段叫 format / export_format / type,而且到处漏 release、漏 locale。
伪代码:track 统一入口(概念版)
function buildEnvelope(ctx) {
return {
release: ctx.release,
route: ctx.route,
locale: ctx.locale,
session_id: ctx.sessionId,
user_id: ctx.userId || null,
trace_id: ctx.traceId || null,
doc_id: ctx.docId || null,
feature_flags: ctx.flags || {},
};
}
function redact(props) {
// 永远不要直接上报:email、token、原始文本内容、完整 query、原始错误栈
return stripPIIAndSecrets(props);
}
function shouldSample(eventName, props) {
// 成功事件可采样;失败事件尽量全量(或更高采样)
if (String(eventName).endsWith('_failed')) return true;
return Math.random() < 0.2;
}
function track(eventName, props, ctx) {
const payload = { ...buildEnvelope(ctx), ...redact(props) };
if (!shouldSample(eventName, payload)) return;
analytics.capture(eventName, payload);
}
2) 错误上报(Sentry):tags 用来分桶,extra 用来排查
错误平台最强的是“聚类与追踪回归”。但前提是你要把可分桶的维度放进 tags,把可排查的上下文放进 extra。
伪代码:captureException(概念版)
function captureException(err, ctx, extra = {}) {
errorTracking.captureException(err, {
tags: {
release: ctx.release,
route: ctx.route,
locale: ctx.locale,
export_format: ctx.exportFormat || 'unknown',
},
extra: {
trace_id: ctx.traceId,
doc_id: ctx.docId,
...extra,
},
});
}
3) 导出链路三件套:clicked → completed / failed(可选 started)
导出类链路有两个常见坑:
- 只打 clicked 不打 failed:你只能看到“点击很多,但完成很少”,却不知道为什么。
- 把 completed 打在“请求发起”时:会把失败当成功,导致转化与失败率都不可信。
伪代码:导出闭环(概念版)
async function exportFile({ format, docId, slideCount, locale }) {
const traceId = crypto.randomUUID();
const ctx = {
traceId,
docId,
exportFormat: format,
locale,
route: getRoute(),
release: getRelease(),
sessionId: getSessionId(),
userId: getUserIdOrNull(),
};
track('export_clicked', {
export_format: format,
slide_count_bucket: bucketize(slideCount),
}, ctx);
const start = performance.now();
try {
track('export_started', { export_format: format }, ctx); // 可选
const exportUrl = await requestExportUrl({ format, docId }); // 调用导出接口
await triggerBrowserDownload(exportUrl); // 跳转/下载
const durationMs = Math.round(performance.now() - start);
track('export_completed', { export_format: format, duration_ms: durationMs }, ctx);
} catch (e) {
const durationMs = Math.round(performance.now() - start);
captureException(e, ctx, { duration_ms: durationMs });
track('export_failed', { export_format: format, duration_ms: durationMs, error_code: normalize(e) }, ctx);
throw e;
}
}
4) 身份与会话治理:identify 一次、logout/reset 一次
多用户/登录态系统里,如果你不做 reset,很容易出现“用户 A 的事件挂在用户 B 身上”的事故。 工程上建议:
- 登录后 identify(一次):避免重复 identify;必要时写 person properties。
- 退出/401 时 reset:清理 analytics 身份与缓存,避免跨用户污染。
- 匿名 session_id 永远存在:即使不登录,也能串联“点击→失败→重试”。
指标与验证(Metrics & Validation)
建议把导出链路的闭环验收,固化为一张仪表盘(按 release/format/locale 分桶):
- 导出失败率:
export_failed / (export_completed + export_failed) - 导出耗时 p95:按 format 分桶(pdf/pptx/png)与 slide_count_bucket 分桶
- Crash-free sessions:导出相关页面的无崩溃会话占比
- MTTD / MTTR:从异常出现到被发现、到修复上线的时间
通过标准(建议):
- 可关联:事件/异常都带统一 envelope(release/route/locale/session/trace/doc 等),能按维度分桶定位。
- 可验收:失败率与耗时 p95 有基线对照(新版本不回退),Top issues 可回溯到具体链路。
- 可控:采样与脱敏到位(不泄露 PII),并把高频问题沉淀成回归护栏。
验证方式建议用“对照”而不是“感觉”:
- 上线前:跑一套导出样本库回归(含 RTL/混排/长文/列表/复杂图片)。
- 上线后:对照上一版本的失败率与耗时 p95,至少观察 24 小时。
- 复盘时:把高频失败样本沉淀成回归用例(并写清触发条件)。
最小可复现验证(示例):
# 1) 触发一次成功导出(产生 export_clicked + export_completed)
# 2) 触发一次失败导出(产生 export_clicked + export_failed,并在 Sentry 里出现对应异常)
# 3) 在 PostHog 里按 release/format/locale 分桶看漏斗:clicked→completed/failed 是否闭环
# 4) 在 Sentry 里打开同一 release 的 issue:tags/extra 是否包含 trace_id/doc_id/export_format
# 5) 抽样复制 trace_id:在两边都能搜到,并能还原“谁在什么条件下失败”
预期与判定(建议):
- 闭环成立:每次 clicked 最终能落到 completed/failed 之一(别只打 clicked)。
- 可关联:PostHog 事件与 Sentry 异常共享关联键(至少 release/route/locale/session/trace/doc)。
- 可对照:按 release 对照失败率与耗时 p95,新版本不回退;出现回退能定位到具体 issue/样本。
不通过先查:事件是否因跳转/关窗丢失(必要时用 sendBeacon);identify/reset 是否导致跨用户污染; autocapture 是否带来噪声淹没关键事件;以及脱敏是否把“可定位所需字段”一并抹掉了。
常见坑与规避(Pitfalls)
- 事件字段不统一:没有 envelope 就没有关联;没有关联就无法分桶定位。
- 只采集成功不采集失败:会导致你永远在“转化掉了”里盲修。
- PII 泄露:把脱敏写进统一封装,禁止业务方直接上报原始文本/email/token。
- autocapture 过度:默认关;只对白名单页面开;只采集能驱动决策的事件。
- 新窗口/跳转导致丢事件:关键事件要尽量在“跳转前”发送;必要时用
sendBeacon类机制兜底。
FAQ(常见问题)
Q1:为什么要同时用 Sentry 和 PostHog?不能只用一个吗?
两者数据模型不同:错误平台擅长聚类、堆栈与回归;行为平台擅长漏斗、分群与影响面。 关键是让它们共享关联键,才能把“错误”映射到“业务影响”。
Q2:如何定义“关键链路”?
选择标准:失败会直接损失收入/留存/信任,或耗时会显著破坏体验(例如导出、支付、生成、保存)。 关键链路建议都打三件套:clicked→completed/failed。
Q3:如何控制成本与噪声?
对“成功事件”做采样,对“失败事件”提高采样;禁用全站 autocapture; 对导出等长链路只采集关键步骤与关键维度(format/locale/release/slide_count_bucket)。
Q4:如何在不泄露隐私的前提下做定位?
不上报原始内容;用 hash/长度/桶化字段替代(例如 slide_count_bucket); 用业务实体 ID(doc_id)串联,不要上报文档全文。
Q5:如何保证“修复后不会退步”?
把高频失败样本加入回归库;把失败率、耗时 p95 设为发布门禁;必要时灰度并对照上一版本。 可观测闭环的终点不是“修一次”,而是“修一次就加一条护栏”。
Q6:为什么 clicked/completed/failed 这三件套这么重要?能不能只打一个事件?
只打一个事件你永远无法回答“是用户没点,还是点了但失败了”。三件套的价值是把链路闭合:你能算失败率、能算耗时、能按维度分桶。 对长链路(导出/生成/支付)来说,这是最小可观测单元。
Q7:新窗口/跳转导致事件丢失怎么办?
关键事件尽量在“跳转前”发送;必要时用 sendBeacon 或“先写队列后异步 flush”兜底; 同时把 export_failed 这种关键失败事件设为高采样甚至全量,避免你只看到“漏斗掉了”却看不到失败原因。
延伸阅读: 可观测性方法论(反馈闭环) / 导出链路一致性方法论