用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台

用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台

大家好,去年我写过一篇 Flutter 全链路监控 SDK 那一版可以理解成 v1。

v1 的目标很直接:Flutter App 里发生了什么问题,我要尽可能采到。

比如:

  • Flutter framework error;
  • Dart uncaught error;
  • 启动耗时;
  • 页面加载耗时;
  • Dio / http 请求耗时和状态码;
  • 页面 PV 和停留时长;
  • 用户关键点击;
  • Flutter frame timing 卡顿序列;
  • app、user、device、platform、timestamp 等上下文。

这些能力都很有价值。

但继续往下做,我发现只"采到"还不够。

因为真实排查问题的时候,开发者面对的往往不是一个干净的指标名,而是一句非常朴素、非常折磨人的反馈:

"刚才 App 卡了一下。"

或者:

"订单页好像很慢,但我也说不清是哪一步。"

又或者更真实一点:

"用户说支付那里转圈了很久,后来又好了。"

这时候,如果监控系统只告诉你:

  • 有一条卡顿事件;
  • 有一个接口 800ms;
  • 有一个页面加载 1200ms;
  • 有一个错误堆栈;

它当然有价值,但还不够。

真正能帮助定位的问题是:

  • 这个问题发生在哪个用户的哪次 session?
  • 当时在哪个页面、哪个模块、哪个 route stack?
  • 卡顿前用户点了什么?
  • 同一时间有没有接口请求?
  • 这个接口是不是影响了页面首帧或可交互?
  • 当时设备、网络、版本、灰度开关是什么?
  • 后面有没有跟着错误、重试、内存压力或生命周期切换?

所以这次 v2,我做的不是"再加几个指标",而是把 Flutter Monitor 从一个指标采集 SDK,升级成了一个 以链路为组织方式的端侧观测 workspace

一句话总结:

v1 解决"我能不能采到问题";v2 解决"我能不能还原问题发生前后到底经历了什么"。

为什么指标不等于现场

做监控很容易有一个错觉:指标越多,排查越容易。

启动慢,就加启动耗时。

页面慢,就加页面耗时。

接口慢,就加 HTTP 耗时。

卡顿,就加慢帧统计。

崩溃,就加错误堆栈。

这些都对,但它们只是"点"。

真实问题往往是"线"。

比如用户反馈一个详情页卡住,背后可能是这样的:

text 复制代码
进入首页
  -> 点击 banner
  -> 请求活动接口
  -> 跳转商品详情
  -> 商品详情接口慢
  -> 列表构建卡顿
  -> 触发一个空对象错误

如果这些事件散落在日志里,开发者要靠人肉把它们拼起来。

这件事非常累。

更麻烦的是,很多时候用户和 QA 说不清路径。

他不会告诉你 traceId,也不会告诉你 route stack。他只会说:

"刚才那个页面卡了一下。"

所以我希望监控 SDK 不只是采到一堆事件,而是能把这些事件组织成一次可回放的用户会话。

flowchart LR A[&#34;v1:指标采集 SDK&#34;] --> B[&#34;错误 / 页面 / 网络 / 卡顿等独立事件&#34;] B --> C[&#34;能知道有问题&#34;] C --> D[&#34;但难还原问题过程&#34;] E[&#34;v2:链路观测 workspace&#34;] --> F[&#34;session / trace / span / breadcrumb&#34;] F --> G[&#34;统一事件信封<br/>EventEnvelope&#34;] G --> H[&#34;Workbench 回放与诊断&#34;]

v2 的核心不是推翻 v1,而是把 v1 已经采到的信号重新组织起来。

v1 到 v2:从采集器升级成链路观测

v1 更像一个"多信号采集器"。

每个模块都很清楚:

  • ErrorMonitor 负责错误;
  • PerformanceMonitor 负责启动和页面;
  • 网络 interceptor 负责 HTTP;
  • JankMonitor 负责卡顿;
  • 行为模块负责点击和 PV;
  • Reporter 负责分发。

这套设计适合早期阶段。

因为早期最重要的是先有信号。

但当信号越来越多以后,我遇到了几个问题。

第一,事件之间的关系不稳定。

一条 HTTP 事件、一条 page load、一条 jank sequence、一条 error 可能发生在同一个页面附近,但它们之间不一定有统一的 session、trace、span 关系。

第二,上下文容易漂移。

事件发生时在 /checkout,但异步 flush 时用户已经跳到 /profile。如果到 flush 时才读当前 route,就会把事件挂到错误页面。

第三,展示层容易补第二套模型。

如果 Workbench 为了展示方便,自己重新解释字段、自己聚合状态、自己补 route,那它很快就会变成另一个事实源。

第四,服务端协议容易发散。

如果 SDK 上报一套结构,DevTools 导出一套结构,Workbench 查询又是一套结构,后续任何字段演进都会很痛苦。

所以 v2 的方向很明确:

text 复制代码
保留原有信号源
  -> 补 session / trace / span / breadcrumb / context
  -> 进入统一 EventEnvelope
  -> 统一输出到日志、Workbench、服务端和未来工具

先把几个概念说清楚。

session 是一次用户使用过程,或者一段可以分析的 App 活动窗口。

比如用户打开 App,从登录、进入首页、浏览详情、点击结算,到退出或切后台,这可以是一段 session。

trace 是一次可追踪流程。

比如冷启动、热启动、页面打开、一次关键业务操作、一次自定义流程,都可以是 trace。

span 是 trace 里的一个阶段。

比如 SDK 初始化、路由切换、页面首帧、HTTP 请求、图片解码、列表构建。

breadcrumb 是问题发生前后的关键足迹。

比如页面进入、点击、请求、弹窗、生命周期变化、卡顿、内存压力和错误。

context 是事件发生时的动态上下文。

比如用户、页面、模块、网络、生命周期、发布信息、灰度开关。

resource 是稳定资源。

比如 App 版本、设备、系统、SDK 版本、运行环境。

这些概念组合起来,目标是让一次用户会话可以被还原成类似这样的时间线:

text 复制代码
session_abc
  app.cold_start
  page.view /home
  ui.click home_banner
  http.client GET /campaign
  page.visit /product/detail
  page.load /product/detail
  ui.jank.sequence /product/detail
  error NoSuchMethodError
  page.stay /product/detail

这样看问题的方式就变了。

我不再只看"某个指标坏了",而是能看"这次用户使用过程发生了什么"。

先看 v2 成果:它已经不是单包 SDK

为了避免这篇文章变成纯概念,我先把 v2 现在的形态摆出来。

它已经不是一个单包 Flutter SDK,而是一个 workspace:

text 复制代码
flutter_monitor/
  docs/                         项目级模型、架构、采集、协议和计划
  packages/
    flutter_monitor_core/       字段、schema、状态、summary、隐私规则
    flutter_monitor_sdk/        Flutter runtime SDK、采集器、pipeline、outputs
    flutter_monitor_native/     可选 native 生命周期和内存增强
  platform/
    services/monitor-service/   NestJS:ingest、SQLite、查询、SSE、Swagger
    web/                        Workbench UI
    shared/                     TypeScript wire mirror 和共享 helper

核心职责可以这样理解:

模块 负责什么
flutter_monitor_core 唯一事件模型、schema、字段注册、隐私规则、summary、export
flutter_monitor_sdk Flutter runtime 采集、上下文、链路、pipeline、outputs、业务 API
flutter_monitor_native 可选 native memory、memory pressure、native lifecycle 等增强信号
Monitor Service 接收统一事件、存储、查询、SSE、Swagger
Workbench Web 以 session 为主线做诊断展示
flowchart TD Core[&#34;统一模型核心<br/>flutter_monitor_core&#34;] SDK[&#34;Flutter 运行时 SDK<br/>flutter_monitor_sdk&#34;] Native[&#34;可选原生增强<br/>flutter_monitor_native&#34;] Service[&#34;Monitor Service<br/>NestJS + SQLite&#34;] Web[&#34;Workbench Web<br/>Session 诊断工作台&#34;] SDK --> Core Native --> Core Native --> SDK SDK --> Service Service --> Web Web --> Core

这次拆包的关键不是为了"看起来架构很高级",而是为了避免协议分裂。

如果 SDK、native、Workbench、服务端各自定义一套事件结构,后面一定会失控。

所以 v2 里我把 flutter_monitor_core 放在最底层:所有入口都复用同一套模型,不允许再出现第二套事件协议。

Example App:用真实场景验证链路

v2 里 example 不再只是一个简单 demo。

它模拟了一个更接近真实业务的 Flutter App:

text 复制代码
Splash → Login(输入 userId 或随机生成)→ 首页 Tab App

登录成功后会写入 context.user.userId,这样 Workbench 可以按用户维度查 session。

页面结构包括:

  • 启动页;
  • 登录页;
  • 首页;
  • 我的事件 / 全部事件;
  • 订单结算;
  • 数据同步中心;
  • 视频频道;
  • 内容创作中心;
  • 事件详情页。

里面故意设计了一些监控场景:

  • Dio/http 成功请求;
  • 404;
  • timeout;
  • 本地慢请求;
  • 校验失败;
  • 重试成功;
  • 图表交互;
  • 故意卡顿;
  • retained memory;
  • 手动错误;
  • 业务 track;
  • 交互 measure。

这些不是为了把 example 做得花哨,而是为了验证 v2 的目标:

  • 页面事件能不能闭合;
  • HTTP 能不能关联页面;
  • track 能不能进入 breadcrumb;
  • measure 能不能观察交互性能;
  • jank 能不能关联当前 route;
  • error 能不能携带 recent breadcrumbs;
  • Workbench 能不能按 eventId、sessionId、traceId 查回完整事件。

这类示例对监控 SDK 很重要。

因为监控工具最怕"单点 demo 能跑,真实路径一接就乱"。

统一事件信封:所有信号都进 EventEnvelope

v2 里最关键的设计是 EventEnvelope

不用展开所有字段,可以把它理解成五块:

区域 作用
公共字段 事件 ID、时间、类型、名称、状态、优先级、session/trace/span
resource 稳定资源,比如 SDK、App、设备、运行环境
context 事件发生时的上下文,比如用户、页面、网络、生命周期、发布信息
attributes 可检索、可聚合的结构化字段,比如 HTTP 状态码、页面耗时、卡顿帧数
payload 事件详情,比如错误堆栈、breadcrumbs、HTTP 详情

这里最重要的是职责分层。

resource 放稳定信息。

比如 App 版本、设备型号、SDK 版本。

context 放事件发生时的动态环境。

比如当前用户、当前页面、当前网络状态、当前生命周期状态。

attributes 放可查询、可聚合、低基数的字段。

比如 http.status_codepage.load_msframe.slow_count

payload 放事件特有详情。

比如错误堆栈、HTTP body 详情、breadcrumbs、诊断上下文。

这样做的好处是,服务端和 Workbench 可以稳定查询 attributes,但不会把巨大 payload 当成索引字段;排查时又可以回到 payload 看细节。

flowchart TB E[&#34;统一事件信封<br/>EventEnvelope&#34;] Public[&#34;公共字段<br/>事件 ID / 时间 / 类型 / 状态 / 链路 ID&#34;] Resource[&#34;稳定资源<br/>SDK / App / 设备 / 运行时&#34;] Context[&#34;动态上下文<br/>用户 / 页面 / 网络 / 生命周期 / 发布信息&#34;] Attributes[&#34;可查询字段<br/>低基数 / 可聚合 / 可索引&#34;] Payload[&#34;事件详情<br/>堆栈 / breadcrumbs / HTTP 详情 / 诊断信息&#34;] E --> Public E --> Resource E --> Context E --> Attributes E --> Payload

这套结构的价值是:所有信号都能被同一套工具消费。

错误不是一套格式,HTTP 不是一套格式,卡顿不是一套格式,native 也不是另一套格式。

它们都变成统一 envelope。

后续 Workbench、Service、DevTools、导出文件、未来 CLI/MCP 都只需要理解这一种结构。

SDK 运行时管线:采集器只采事实

v1 里很容易出现一种倾向:哪个模块采到事件,哪个模块就顺手把它格式化、上报、打印。

短期很快,长期会乱。

v2 里我把采集器的职责收窄:

采集器只负责捕获事实,不直接构建最终协议,不直接上报,不做采样、重试、隐私过滤和服务端适配。

最终统一进入 pipeline。

flowchart LR A[&#34;采集器<br/>错误/页面/HTTP/卡顿/内存&#34;] --> B[&#34;原始信号<br/>RawSignal&#34;] B --> C[&#34;上下文快照<br/>页面/用户/设备/网络&#34;] C --> D[&#34;链路快照<br/>session/trace/span/breadcrumb&#34;] D --> E[&#34;统一事件信封<br/>EventEnvelope&#34;] E --> F[&#34;校验 / 脱敏 / 采样限流&#34;] F --> G[&#34;日志 / HTTP / Workbench / 服务端&#34;]

这个顺序很重要。

事件发生时先取上下文快照,避免异步 flush 时上下文漂移。

举个例子,用户在 /checkout 页面触发了一个失败请求,但等 SDK 真的 flush 时,用户可能已经跳到 /profile。如果那时才读取当前 route,这个失败请求就会被挂到错误页面。

所以 v2 要在事件捕获时固化 context。

然后补 session、trace、span 和相关 breadcrumbs。

再构建 EventEnvelope

然后做 schema 校验、隐私过滤、采样限流。

最后才分发到不同 output。

这样一来,不管信号来自页面、HTTP、错误、卡顿、内存还是 native,最终都会走同一条路。

业务接入面反而变少了

v2 还有一个变化:公开 API 没有变得更复杂,反而更收敛。

普通业务不需要理解 RawSignal、FieldPaths、EventEnvelope、trace/span 内部细节。

业务侧常用入口主要是:

text 复制代码
FlutterMonitorSDK.init(...)
FlutterMonitorSDK.setContext(...)
FlutterMonitorSDK.track(...)
FlutterMonitorSDK.measure(...)
FlutterMonitorSDK.recordError(...)
FlutterMonitorSDK.createDioInterceptor()
FlutterMonitorSDK.createHttpClient()
FlutterMonitorSDK.routeObserver
FlutterMonitorSDK.flush()

这些 API 背后的职责是:

API 作用
init 初始化 SDK、配置输出模式和基础上下文
setContext 设置用户、模块、发布、网络等通用排查上下文
track 记录稳定低基数的业务动作,并进入 breadcrumb
measure 观测一次关键交互性能窗口
recordError 记录业务 catch 住但仍需进入链路的错误
createDioInterceptor / createHttpClient 自动采集网络请求并关联当前链路
routeObserver 自动采集页面进入、停留、加载和 route context
flush 主动 flush output 缓存事件

这里的设计原则是:内部越复杂,外部越要克制。

业务侧只应该关心几件事:

  • 初始化 SDK;
  • 设置用户、模块、发布、网络等通用上下文;
  • 记录关键业务动作;
  • 观测一次关键交互性能;
  • 手动记录业务 catch 住的错误;
  • 接入路由和 HTTP;
  • 必要时 flush。

至于事件怎么关联 session、怎么挂 breadcrumbs、怎么构建 envelope、怎么脱敏、怎么进队列,不应该暴露给普通业务代码。

三种输出模式:开发、QA、生产分开

v2 里我把输出模式收敛成三种:

模式 适合场景 行为
consoleOnly 本地开发 只输出 compact log,不启用生产队列
localLive QA / 本地复现 小 batch 写入本地 Workbench service,便于近实时查看 session timeline
production 灰度或线上 启用离线队列、batch、重试、采样限流、优先级和 SDK 自监控

这个设计是为了避免业务方手动组合一堆 output、queue、retry、sampling 配置。

本地开发时,我只需要看到摘要日志,不想被完整 JSON 淹没。

QA 复现时,我希望事件尽快进 Workbench,方便刚复现完就看 session timeline。

生产环境里,我更关心可靠性和成本:

  • 网络断了怎么办?
  • 服务端暂时不可用怎么办?
  • 队列满了怎么办?
  • 某个事件太大怎么办?
  • 采样丢了多少怎么办?
  • flush 失败怎么办?

所以 production 模式需要离线队列、batch、重试、采样限流和 SDK 自监控。

也就是说,SDK 不只监控业务 App,也要监控自己有没有丢证据、有没有 flush 失败、有没有被限流。

真正上线后,脏活都在边界情况里

监控 SDK 在 demo 里很好写。

App 正常启动,网络正常,服务端正常,事件正常发送,Workbench 正常展示,看起来一切都很美。

但真实上线后,问题会变得很脏。

比如:

  • 用户在弱网环境下连续操作;
  • App 刚进入后台就被系统杀掉;
  • 服务端短时间不可用;
  • 某个页面短时间产生大量事件;
  • HTTP body 或错误堆栈过大;
  • 离线队列写满;
  • 低端设备上 SDK 自己造成性能压力;
  • 采样策略把排查关键证据采没了;
  • flush 阻塞了业务线程;
  • SDK 还没初始化完,业务错误已经发生;
  • 用户登录态切换后,上下文不能污染旧事件。

这些才是生产 SDK 最麻烦的地方。

所以 v2 的设计里,我不只是关心"事件能不能发出去",还关心"发不出去的时候怎么处理,以及处理过程不能伤害宿主 App"。

我给 production 模式定了三条底线:

  • 业务调用路径只做轻量采集,不等待网络;
  • 所有事件先进入统一管线,完成 schema 校验、上下文快照和隐私过滤;
  • 上报失败、队列拥塞、采样丢弃、flush 超时,都必须留下 SDK 自监控证据。

可以把生产链路理解成下面这张图:

flowchart LR A[&#34;业务调用路径<br/>track / error / route / http&#34;] --> B[&#34;轻量采集<br/>生成原始信号&#34;] B --> C[&#34;统一管线<br/>补上下文 / 校验 / 脱敏&#34;] C --> D[&#34;可靠投递层<br/>离线队列 / batch / retry&#34;] D --> E[&#34;输出目标<br/>Workbench / 监控服务&#34;] D --> F[&#34;SDK 自监控<br/>drop / retry / flush / queue health&#34;]

这张图里最关键的不是 HTTP 上报,而是中间的"可靠投递层"。

因为真实 App 里,上报通道一定会遇到阻塞、失败、重试、限流和丢弃。SDK 的职责不是假装这些问题不存在,而是把它们设计成有边界、可降级、可审计的系统行为。

1. 弱网和服务端不可用:先入队,再批量发送

真实 App 不应该默认一条事件一个请求。

一事件一请求在本地 demo 里很直观,但线上会放大网络成本,也更容易在弱网下失败。

所以 production 模式应该走:

text 复制代码
EventEnvelope
  -> 离线队列
  -> batch
  -> HTTP 上报
  -> 失败后重试
  -> 成功后确认移除

发送失败时,不应该立刻丢事件。

网络超时、5xx、429、临时服务不可用,这些都属于可重试场景。SDK 应该把事件保留在队列里,按指数退避或固定策略重试。

但也不能无限重试。

无限重试会拖垮设备、电量和网络,也会让队列永远清不掉。

所以需要有:

  • 最大重试次数;
  • 重试退避;
  • 单批最大事件数;
  • 单批最大字节数;
  • 单事件最大字节数;
  • 队列最大事件数;
  • 队列最大字节数;
  • 事件最大保留时间。

这些参数看起来不性感,但它们决定了 SDK 能不能真的上生产。

还有一个容易被忽略的细节:删除队列事件应该基于确认,而不是基于"请求已经发出"。

也就是说,事件进入 batch 之后,不代表它已经安全离开本地。只有服务端确认接收后,SDK 才能把这批事件从离线队列里移除。

如果是网络超时、5xx、429 这类临时失败,就进入下一轮重试。

如果是 schema 不兼容、鉴权失败、服务端明确拒绝这类不可重试错误,就不能无限重试,而是要进入 non_retryable_rejected 或类似原因的 SDK health 审计。

2. App 被杀:flush 是尽力语义,不能假装百分百可靠

移动端最现实的情况是:App 不一定有机会优雅退出。

用户可能直接划掉进程。

系统可能因为内存压力杀掉 App。

后台时间可能非常短。

所以 flush on background 只能是尽力语义。

进入后台或退出前,SDK 可以更积极地 flush,但不能把它说成"保证所有事件都发出去了"。

更合理的设计是:

  • 关键事件尽量提前入队;
  • 后台时触发一次快速 flush;
  • flush 有超时,不无限阻塞;
  • 来不及发送的事件保留在离线队列;
  • 下次启动后继续补发;
  • 如果因为物理限制丢了事件,要记录 SDK health。

这里我很在意一个表达:不要假装可靠性是绝对的。

监控系统最怕静默丢证据。

丢了不可怕,可怕的是使用方不知道丢了多少、为什么丢。

3. 上传通道阻塞:flush 可以慢,业务线程不能等

弱网和服务端不可用解决的是"发不出去怎么办"。

但还有一个更隐蔽的问题:上报通道阻塞时,不能把采集路径一起拖住。

比如:

  • 上一批还在 retry;
  • 服务端响应很慢;
  • 当前 flush 正在发送大 batch;
  • App 进入后台,只剩很短的执行窗口;
  • QA 复现时短时间产生大量事件;
  • 业务手动调用了 flush,同时页面还在继续产生事件。

如果采集、入队、flush、HTTP 发送共用一把大锁,就很容易出现一种很糟糕的结果:监控 SDK 没有把问题发出去,反而把用户当前操作卡住了。

所以这里我更倾向把链路拆开:

text 复制代码
业务调用
  -> 快速生成事件
  -> 异步进入队列
  -> flush 任务按策略读取队列
  -> batch 发送
  -> ack 后删除 / 失败后更新重试计划

这里有几个设计点:

  • 业务调用只负责把事实交给 SDK,不等待 HTTP;
  • 入队和发送解耦,发送慢不会反向阻塞新的采集;
  • 正在 flush 时,新事件可以继续进入队列,下一轮再发;
  • app exit flush 使用短超时,不能拖慢退出;
  • 同一时间避免多个 output 各自监听生命周期、重复 flush;
  • retry 应该有 nextAttemptAt 之类的调度概念,而不是失败后立刻死循环;
  • flush 成败、耗时、batch 大小、队列水位都要进入 SDK health。

这样设计之后,flush 的语义就更清楚了。

它不是"所有数据一定立刻上报成功",而是"尽量把当前可发送的数据推出去,并把失败原因留下来"。

4. 队列背压:不能让监控把 App 拖慢

监控 SDK 是寄生在业务 App 里的。

它再重要,也不能比业务本身更重要。

如果某个页面短时间产生大量事件,SDK 不能因为写队列、序列化 JSON、压缩 payload 或等待网络,把主线程卡住。

所以队列设计要有背压。

我希望它遵守几个原则:

  • 采集路径尽量轻;
  • 复杂处理放到 pipeline/output,避免阻塞业务调用;
  • 队列有上限,不能无限增长;
  • 队列满时按保留等级和优先级降级;
  • 低价值事件先被采样、限流或驱逐;
  • 高价值事件尽量保留;
  • 所有丢弃都进入 SDK health 统计。

这也是为什么 v2 里要区分 hardcompressiblesampleable

队列背压不是简单地"满了就丢最旧"。

更合理的是:先保住排查最关键的事实。

上报阻塞解决的是"发送慢时不要卡住 App"。

队列背压解决的是"真的堆不下时应该保谁、丢谁、怎么说明"。

如果没有背压,离线队列就可能无限膨胀,最后占用存储、拖慢查询、甚至影响业务 App 本身。

如果背压太粗暴,关键错误、卡顿、启动慢、内存压力这些排查证据又可能被普通日志挤掉。

所以 v2 的降级顺序大致是:

  1. 高频辅助事件先采样或限流;
  2. 可压缩事件先裁剪详情,只保留事实层;
  3. 队列压力继续升高时,按保留等级和优先级驱逐;
  4. 实在触达物理极限时,允许丢硬证据,但必须审计。

这里最重要的不是"永不丢",而是"丢得有规则,并且能被看见"。

5. 大事件:先裁剪详情,不要直接丢事实

真实事件可能很大。

比如:

  • HTTP response body 很大;
  • 错误堆栈很长;
  • payload 带了大量诊断信息;
  • breadcrumbs 太多;
  • 某个业务 properties 传得过大。

如果一个事件超过上限,不能第一反应就整条丢掉。

更合理的是分层处理。

attributes 里的事实层尽量保留,因为它服务查询和聚合。

payload 里的详情层可以裁剪,比如:

  • 截断 body;
  • 去掉 query/detail;
  • 裁剪 breadcrumbs;
  • 保留 hash、长度和截断标记;
  • 删除过大的非关键字段。

只有裁剪后仍然超过限制,才进入丢弃流程,并记录 payload_too_large 一类的 SDK health 证据。

这就是 EventEnvelope 分层的价值:事实和详情分开,才有空间做降级。

6. 隐私过滤:必须早于任何输出

监控 SDK 很容易碰到敏感数据。

URL query、request body、response body、token、cookie、手机号、身份证、地址、精确位置,都不能随便进日志或上报。

所以隐私过滤不能只放在 HTTP output 之前。

它必须早于任何 output:

text 复制代码
EventEnvelope
  -> schema 校验
  -> 隐私过滤
  -> log / HTTP / Workbench / file

否则就会出现一个很危险的情况:HTTP 上报脱敏了,但本地 log 或 Workbench 已经泄露了原始数据。

v2 的原则是:所有 output 只能消费脱敏后的统一 envelope。

7. SDK 自身性能:采集不能变成新的卡顿来源

监控 SDK 的悖论是:它要发现性能问题,但它自己不能制造性能问题。

所以我在设计时会尽量避免几个坑:

  • 不在业务调用路径做重型 JSON 处理;
  • 不在主线程等待网络;
  • 不默认输出完整 JSON 刷屏;
  • memory sample 默认低频;
  • breadcrumb store 有数量上限;
  • payload 有大小上限;
  • batch 发送,不做高频小请求;
  • compact log 只输出摘要;
  • Workbench live 模式和 production 模式分开。

这里没有银弹。

监控 SDK 一定会有成本,关键是成本要可控、可配置、可降级。

我会把每一次 SDK 调用都当成"在用户交互路径上抢时间片"。

所以 public API 的设计不能只看好不好用,还要看它在最坏情况下会不会拖慢业务。

比如 trackrecordError、HTTP interceptor、route observer 这类入口,应该尽量快速返回;真正重的工作放到 pipeline、队列和 output 后面去做。

8. 初始化前和上下文切换:缺失也要显式表达

还有一些边界非常容易被忽略。

比如 SDK 还没初始化完,Flutter error 就发生了。

或者用户刚登录,userId 才被设置。

又或者用户退出登录,后续事件不能继续带旧用户上下文。

这类问题不能靠"希望业务按顺序调用"解决。

v2 的做法是:

  • 事件捕获时取 context snapshot;
  • 旧事件不被新上下文改写;
  • 缺少上下文时显式标记 context.missing 和原因;
  • 用户登录、登出、切换账号时更新后续上下文;
  • route 不可用、native bridge 不可用、平台能力受限,都要有明确缺失原因。

这样排查时至少知道:这个字段为什么没有。

不知道原因的空值,才是最难排查的。

把这些边界收敛一下,大概是这张表:

生产边界 v2 的设计重点 不能做什么
弱网 / 服务端不可用 离线队列、batch、重试、ack 后删除 一条事件一个请求,失败就丢
App 被杀 / 后台时间短 关键事件提前入队,后台尽力 flush,下次启动补发 承诺退出前一定全部发送成功
上传阻塞 采集、入队、发送解耦,flush 有短超时 让业务线程等待 HTTP
队列写满 按保留等级、优先级、大小和 TTL 降级 无限增长,或无差别丢弃
事件过大 先裁剪详情层,保留事实层和截断标记 直接整条丢弃且不留原因
敏感数据 脱敏早于所有 output 只在 HTTP 上报前脱敏
SDK 性能成本 低频采样、大小上限、异步处理、compact log 让监控制造新的卡顿
上下文缺失 / 切换 捕获时快照,缺失原因显式表达 用新上下文回填旧事件

hard 证据:不是所有事件都能随便采样

生产环境一定要考虑采样、限流、队列上限。

但不是所有事件都能一样处理。

v2 里我把事件保留策略分成几类:

类型 含义
hard 硬证据,问题定位的关键事实,不能被普通采样静默丢掉
compressible 有价值但可压缩,可以剥离详情或聚合
sampleable 高频辅助事件,可以采样或限流

比如:

  • error;
  • http.client 事实层;
  • 关键业务 action;
  • interaction measure;
  • 启动 end;
  • jank sequence;
  • memory pressure;
  • SDK init / SDK health;

这些都是高价值证据。

它们不能像普通日志一样随便采样掉。

当然,这不代表 hard 事件"绝不丢失"。

如果离线队列达到物理极限,SDK 仍然可能不得不丢弃最旧事件。但关键是:不能静默丢。

必须进入 SDK health 审计,让使用方知道证据缺口的存在和规模。

这也是 v2 比 v1 更像生产系统的地方。

Workbench:不是 JSON Viewer,而是 Session 诊断工作台

v2 另一个很重要的成果是 Workbench。

Workbench 不是传统大盘,也不是普通 JSON Viewer。

它的定位是:

以 session 为主线的 Flutter 端侧链路诊断工作台。

它要帮助我回答:

  • 刚才复现的 session 是哪一个?
  • 这次 session 里发生了哪些页面、请求、点击、错误、卡顿?
  • 某个错误前面发生了什么?
  • 某个卡顿是否和 HTTP 请求重叠?
  • 某个页面慢在页面加载、请求、卡顿还是错误?
  • 我能不能从一个问题节点回到完整 envelope?
flowchart TD A[&#34;Example App 复现问题&#34;] --> B[&#34;SDK 采集统一事件&#34;] B --> C[&#34;Monitor Service 接收并存储&#34;] C --> D[&#34;Workbench 查看 Session&#34;] D --> E[&#34;Timeline 还原操作路径&#34;] E --> F[&#34;选中问题节点查看完整 Envelope&#34;]

目前 platform 里已经有:

  • NestJS Monitor Service;
  • SQLite 本地存储;
  • /api/monitor/v1/events ingest;
  • session / trace / event 查询;
  • SSE 实时通知;
  • Swagger API;
  • React/Vite Workbench;
  • Session Detail;
  • Event Inspector;
  • HTTP Detail;
  • Performance Overview。

这也是 v2 和 v1 体感差异最大的地方。

v1 更像"我打印了很多监控日志"。

v2 更像"我能打开一个工作台,看这次复现到底发生了什么"。

一次典型排查应该怎么走

假设 QA 反馈:

用户 102 在结算页点击提交后卡了一下,后来提示失败。

我希望 v2 的排查路径是:

text 复制代码
输入 userId + 时间范围
  -> 找到对应 session
  -> 进入 Session Detail
  -> timeline 中看到 checkout.submit
  -> 后面跟着 http.client timeout
  -> 同一时间段出现 ui.jank.sequence
  -> 最后出现 error 或 business failure
  -> 点击任意节点查看完整 EventEnvelope

这里有几个关键点。

第一,开发者不需要先知道 sessionId

真实反馈里很少有人能给出 sessionId。更自然的入口是 userId + 大概时间、App 版本、页面、错误、卡顿或慢请求。

第二,Session Detail 里先看 timeline。

不是先看 JSON,而是先看这次会话发生了什么。

第三,任何摘要都能回到原始 envelope。

UI 可以做时间线、状态标签、摘要卡片、HTTP Detail,但最终必须能回到完整事件。

第四,问题节点要能串上下文。

一个失败请求不只是状态码,它应该知道自己属于哪个页面、哪个 trace、前面有哪些 breadcrumbs、后面有没有错误或卡顿。

为什么 Workbench 不是传统监控大盘

传统监控大盘更关心聚合:

  • 错误率;
  • 慢请求率;
  • P95;
  • 影响用户数;
  • 趋势;
  • 告警。

这些当然重要。

但 Workbench 第一阶段更关心的是:

这一次发生了什么?

它服务的是本地调试、QA 复现和性能分析。

所以它的第一优先级不是做一个重型 BI 报表,而是让开发者从 session 出发,快速理解一次 App 使用过程。

长期趋势、告警、跨版本对比、影响用户数,可以放到后续服务端能力里。

Workbench 先做好 session timeline、节点诊断和 raw JSON 回查。

为什么我没有选择"无限加指标"

做监控 SDK 很容易陷入一个误区:不断加指标。

启动不够就加启动指标,页面不够就加页面指标,内存不够就加内存指标,native 不够就加 native 指标。

指标越来越多,但问题不一定更好查。

v2 里我更关心的是:这个指标能不能进入链路?

如果一个事件无法关联 session、页面、用户、版本、设备、网络、breadcrumbs,它就很难服务真实排查。

所以设计上我更偏向:

  • 先稳定事件模型;
  • 再接入信号;
  • 再补生产可靠性;
  • 最后做 Workbench 和服务端诊断。

这也是为什么文档里反复强调:

不要创建第二套事件模型。

和 v1 相比,v2 到底升级了什么

我用一张表简单总结:

维度 v1 v2
目标 采集错误、性能、网络、卡顿等指标 还原一次用户或 QA 会话链路
组织方式 独立事件 session / trace / span / breadcrumb
数据模型 偏模块化事件结构 统一 EventEnvelope
上下文 事件里补充部分上下文 捕获时 ContextSnapshot 固化
输出 日志 / 简单输出 consoleOnly / localLive / production
可靠性 基础输出能力 队列、batch、重试、采样限流、SDK health
展示 控制台和事件日志为主 Workbench Session Timeline
未来扩展 继续加模块 core / sdk / native / platform 共享协议

这不是一次"功能堆叠",而是一次组织方式升级。

当前边界

这套 v2 不是说所有事情都已经完成。

当前主线更接近:

  • Flutter-only SDK 主链路已经打通;
  • core schema、pipeline、输出模式、Workbench 本地回查已经形成闭环;
  • native 保留为可选增强方向;
  • DevTools extension、CLI、MCP 等工具入口可以后置;
  • 真实 App 灰度和长期服务端能力还需要继续验证。

这点我觉得要说清楚。

监控 SDK 这种东西,不能只看 demo 能不能跑。

它真正的挑战在灰度、弱网、离线、隐私、服务端兼容、长期字段演进和真实排查效率。

这次重构给我的几个启发

第一,监控不是采集越多越好,而是关联越清楚越好。

孤立指标适合看 dashboard,可关联的 session 和 trace 才适合调查真实问题。

第二,事件模型要先于功能扩展稳定。

如果字段和协议还没统一,就继续加采集器,后面一定会还债。

第三,业务 API 要克制。

监控 SDK 内部可以复杂,但普通业务接入不应该被迫理解 trace/span/envelope。

第四,Workbench 不应该成为第二套事实源。

UI 可以做摘要、筛选和图表,但事实仍然应该来自统一 envelope。

第五,生产可靠性不能后补。

离线、重试、采样、限流、队列上限、SDK 自监控,这些不是"上线后再说"的细节,而是监控系统本身可信的前提。

总结

Flutter Monitor v2 对我来说,不是一次"功能增加",而是一次组织方式变化。

从:

text 复制代码
采集一堆指标

变成:

text 复制代码
围绕 session 组织所有信号

从:

text 复制代码
我知道 App 某处卡了

变成:

text 复制代码
我能还原这次卡顿前后发生了什么

从:

text 复制代码
控制台里刷一堆 JSON

变成:

text 复制代码
Workbench 里看 session timeline 和问题节点

我越来越觉得,监控 SDK 的价值不是采到更多点,而是帮助开发者更快还原现场。

指标是基础,链路才是答案。

相关推荐
天渺工作室10 小时前
实现一个adblock/adblock plus等浏览器广告拦截器检测插件
前端·javascript
阳光是sunny10 小时前
Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
前端·面试·架构
ZhengEnCi10 小时前
Q04-Vite禁用CSS代码分割-解决生产环境样式加载顺序混乱问题
前端·vue.js·vite
九酒11 小时前
AI Agent 开发踩坑记:口播功能非得用 APP 原生实现吗?
前端·人工智能·agent
Jackson__11 小时前
做了一段时间的AI coding后,我终于搞清了 CLI 和 MCP 的区别
前端·agent·ai编程
IT_陈寒14 小时前
JavaScript项目实战经验分享
前端·人工智能·后端
用户479492835691515 小时前
6w star,GitHub 趋势第一的 Ponytail,这个agent插件到底在火什么
前端·后端
薛定喵的谔16 小时前
我开源了一个精致的 Next.js 博客模板:Skyplume
前端·前端框架·next.js