首页
All Posts

前端全链路优化实战(二):前端日志数据模型如何支撑追踪和归因

全链路系统的底座是数据模型。没有稳定的数据结构,后面的查询、聚合、告警、可视化和归因都会变成补丁式开发。它就像一张统一台账:如果每个系统都用自己的格式记录时间、用户、页面、接口和结果,事后就无法判断问题到底发生在哪个环节。

日志数据模型台账

全链路系统的底座是数据模型。没有稳定的数据结构,后面的查询、聚合、告警、可视化和归因都会变成补丁式开发。它就像一张统一台账:如果每个系统都用自己的格式记录时间、用户、页面、接口和结果,事后就无法判断问题到底发生在哪个环节。

前端日志模型的目标,是让每条日志都能回答四个问题:谁、在什么环境、做了什么、结果怎样。围绕这四个问题设计字段,比盲目采集更多信息更重要。

第一层:基础信息让日志有坐标

基础信息用于确定日志发生的坐标。典型字段包括:

{
  "timestamp": 1718000000000,
  "appId": "mall-web",
  "env": "production",
  "pageUrl": "/checkout",
  "pageId": "pg_9f3a",
  "release": "2026.06.18-1",
  "sdkVersion": "1.4.0"
}

timestamp 用于排序,appIdenv 用于区分业务与环境,pageUrlpageId 用于把同一页面实例内的事件串起来,release 用于判断问题是否来自新版本。很多团队排查线上故障时发现“昨天没问题,今天有问题”,但日志里没有版本号,只能靠猜。版本字段看似普通,实际是排查灰度问题的关键。

第二层:身份关联让日志能串成链

身份字段是全链路的核心。登录场景可以使用 userId,未登录场景则需要匿名标识,比如 fingerprintIdsessionIddeviceId。它们不是互相替代,而是解决不同关联范围。

  • userId 适合业务归因,可以知道具体用户是否受到影响。
  • sessionId 适合同一次访问过程的串联。
  • pageId 适合一次页面生命周期内的事件归组。
  • fingerprintId 适合未登录用户或跨页面弱关联。
  • traceId 适合前后端请求链路贯通。

实际设计时,要避免把所有关联都压到一个 ID 上。比如 traceId 只在一次请求内有效,不能替代用户会话;fingerprintId 也可能变化,不能当成绝对身份。更稳妥的做法是多字段并存,在分析时根据问题类型选择合适的关联方式。

第三层:异常上下文决定能否复现

异常日志不能只有一句 message。对前端而言,异常至少要包含错误类型、错误位置、堆栈、触发行为和页面状态。

以脚本异常为例,一个较有价值的数据结构可以包含:

{
  "type": "js_error",
  "message": "Cannot read properties of undefined",
  "stack": "...",
  "filename": "checkout.8ad3.js",
  "line": 120,
  "column": 18,
  "route": "/checkout",
  "lastActions": ["click:submit", "input:phone", "click:coupon"],
  "networkState": "4g",
  "visibilityState": "visible"
}

这里的 lastActions 很关键。很多错误不是页面一加载就出现,而是用户做了某组操作后才触发。没有行为序列,就只能在本地反复猜复现路径。有了行为序列,排查人员至少能知道错误前发生过什么。

接口异常也一样,不能只记录“请求失败”。它应该包含 URL 模板、方法、状态码、耗时、超时标记、是否被用户中断、业务错误码、请求对应的页面动作。如果要关联后端,还需要把前端请求 ID 或 trace header 传给服务端。

第四层:体验指标让性能问题可衡量

体验指标不是锦上添花。很多“感觉慢”“感觉卡”的问题,如果没有指标,就很难判断修复是否有效。常见字段包括:

  • TTFB:首字节时间,帮助判断服务端处理、网络传输和 CDN 是否异常。
  • FCP:首次内容绘制,反映用户什么时候看到页面有内容。
  • LCP:最大内容绘制,反映主内容出现速度。
  • INP:交互响应,反映用户操作后页面是否及时响应。
  • CLS:布局偏移,反映视觉稳定性。

这些指标最好和页面、用户、版本、设备一起存储,而不是只上报全站平均值。平均值会掩盖问题。移动端低端机、弱网地区、某个渠道入口、某个灰度版本,往往才是真正出问题的地方。

数据模型设计的几个取舍

第一,字段要稳定。日志模型一旦被监控、报表、告警依赖,随意改字段会造成连锁故障。可以通过 schemaVersion 做演进,让新旧字段共存一段时间。

第二,字段要分层。基础字段每条日志都带,重字段按需带。比如行为快照、长堆栈、资源列表不一定每次都上报,可以在异常或采样命中时附加。

第三,要注意隐私和合规。用户输入、手机号、地址、token、cookie 等敏感信息不能直接进入日志。日志系统不是数据仓库,能定位问题即可,不能为了方便排查而扩大风险。

第四,要控制体积。全链路日志量很容易膨胀。可以对正常日志采样,对错误日志全量,对高价值核心路径提高采样,对重复错误做聚合。

一个可落地的日志分层

实践中可以把日志分成五类:

  1. 访问日志:页面进入、路由变化、页面退出。
  2. 性能日志:Web Vitals、资源加载、长任务。
  3. 异常日志:脚本、Promise、资源、接口、业务错误。
  4. 行为日志:点击、输入、滚动、提交、关键路径跳转。
  5. 自定义日志:业务方主动记录的关键状态。

这五类日志共用基础字段和身份字段,再在各自类型下扩展上下文。这样既能保持统一,又不会让所有日志都变成一个臃肿的大对象。

好的数据模型不一定复杂,但一定要可关联、可查询、可演进。只要模型能稳定回答“谁在什么场景下经历了什么问题”,全链路的后续能力就有了可靠基础。