必须依靠双方约定的结构化字段和服务端主导的生成规则来确保异常指纹对齐。具体包括:服务端统一转化异常为含code、message、traceid、stepid、requestid的标准json;前端从响应体或预置数据中提取这些字段;指纹计算使用完全相同的字段组合与拼接顺序;禁用error.timestamp、error.stack、error.name等易变属性;通过typescript/json schema定义fingerprintinput契约并共用computefingerprint纯函数实现。

不能靠对象属性本身确保异常指纹对齐。对象属性(如 error.timestamp、error.stack、error.name)在前后端运行环境不同、序列化方式不同、甚至类定义完全不同时,无法直接复用或比对。真正起作用的是**双方约定的结构化字段 + 服务端主导的生成规则**。
用共享业务上下文替代运行时对象属性
客户端 JavaScript 的 Error 实例和服务器端 Java 的 RuntimeException 或 Go 的 error 接口,没有跨语言继承关系,属性名可能相同但语义和格式常不一致(例如 stack 字段在 JS 中含文件路径,在 Java 中含行号和类加载器信息)。应主动放弃“复用原生对象属性”的思路:
- 服务端统一将异常转化为标准 JSON 对象,只保留契约字段:
code、message、traceId、stepId、requestId - 前端不读取原始
error.stack,而是从响应体或window.__INITIAL_DATA__中提取上述字段 - 所有指纹计算(如
hash(code + traceId + stepId))两端使用完全相同的字段组合与拼接顺序
禁止依赖易变或不可控的对象属性
以下属性看似直观,但在跨端场景中极易导致指纹错配:
-
error.timestamp/Date.now():客户端和服务端系统时间存在毫秒级偏差,不能作为指纹核心 -
error.stack:堆栈格式随运行时、框架版本、source map 状态变化;JS 堆栈不含行号列号,Java 堆栈含类加载器细节,无法哈希对齐 -
error.name:JS 中是字符串(如"TypeError"),Java 中无对应字段;且自定义异常名可能大小写不一致或带包路径 -
error.cause或嵌套异常:JS 不支持异常链,Java 的getCause()在 JSON 序列化中常被忽略或截断
用 TypeScript/JSON Schema 定义指纹字段契约
把指纹所需字段变成可验证的接口,而非运行时对象的反射结果:
- 在 shared-types 包中定义:
type FingerprintInput = { code: string; traceId: string; stepId: string; env: 'prod' | 'staging' } - 服务端在捕获异常后,从 MDC、请求头、路由参数中提取这些值,填入响应体
- 前端在水合(hydration)阶段从预置数据中读取该结构,不做任何属性推断或 fallback
- 指纹哈希逻辑封装为纯函数:
computeFingerprint(input: FingerprintInput): string,前后端共用同一份实现(可通过 npm 包或代码复制保障一致性)
对齐的关键不在“对象”,而在“协议”
异常指纹不是为了还原错误现场,而是为了唯一标识一类问题以便聚合、告警和归因。与其让客户端尝试解析一个不存在的“服务器异常对象”,不如让两端都按同一份 JSON Schema 构造输入。只要 traceId 由服务端生成并透传、stepId 由前端在关键操作前主动上报、code 来自统一枚举——哪怕客户端连 Error 构造函数都没调用,也能生成和服务端 100% 匹配的指纹。











