结论:队列内同 key 合并比时间戳判断明显更稳,这个我认同并采用,WeakSet 打标也应当换,但有三个实现细节若不提前处理、踩到会很难受。
第一,WeakSet 存不下非 Error 对象。 Promise.reject('boom')、reject({code:500}) 这类错误在真实项目里不少,weakSet.add('string') 会直接抛 TypeError,反而让打标逻辑变成新的错误源。加一层判断:只有 typeof err === 'object' && err !== null 才 add,字符串和原始值直接跳过打标,交给 key 合并兜底。
第二,Map 合并要有条数上限。 在内存上限内同 key 只做 count++ 很省内存,但如果一直合并、flush 持续失败(例如后端挂了),Map 会无限增长,最后 OOM。做法是给 Map 设上限(我用的 200 条),超过上限就强制 flush 一次,并且丢弃最旧的条目;count 在重试时要保留,不能 flush 失败就清零,否则计数会丢失。
第三,做 key 归一化时,务必把构建产物 hash 抹掉。 你说的剥离 query 和 hash 是对的,但真要稳定还得处理 stack:chunk-abc123.js:1:234 里的 hash 每次发版都会变,同一条错误发版后立刻被算成“新种类”,指标会直接误报。建议在取前 5 帧时,把 /assets/\w+-[a-f0-9]{8}\.js/ 归一成 /assets/[chunk].js,并剔掉所有 node_modules 帧。资源错误的 key 同理,另外对 origin 也要做归一化:如果资源域名带 CDN 多节点前缀,应按主域名归并。
新增种类的差集要持久化上一周期的 key 集合(我用的 sessionStorage 加 TTL 7 天),否则整个 SPA 刷新一次就重置,差值永远是全量;集合本身也要设 TTL,不然存久了会慢慢变大。
自激哨兵那条同意,再补一句:更彻底的做法是上报 SDK 初始化时带一个 disable 开关,一旦自身请求失败两次就自动关闭,别让上报器自己成为错误源。
常见的坑:WeakSet 打标和 key 合并这两层不要共用一个判断入口,否则打标失败会连带把正常错误也吞掉。