活动回顾 / T SALON

编辑内容

卢依宁谈前端监控:从 SDK 采集到业务问题排查

卢依宁结合货拉拉业务介绍前端监控的需求、方案取舍与 SDK 设计,并通过白屏、渠道下滑、定位异常和请求突增四个案例,说明怎样把日志、曲线和业务上下文连起来。

页面的访问量大概是多少,用户主要使用什么平台,业务高峰出现在什么时候?卢依宁用这三个问题打开分享:前端监控不仅是收集报错,也能帮助工程师理解业务、定位问题,并主动发现体验改进的空间。

这段分享来自深圳“前端的挑战与机遇”录播第 1 段,所属稿件发布于 2022 年 5 月 9 日。卢依宁介绍的是当时货拉拉团队的建设与案例,产品能力、价格和监控指标均保留历史语境。

先确定监控要回答什么

卢依宁把需求分成三类:理解用户怎样使用产品,追查一次具体问题,以及观察真实页面与资源加载性能。对应的数据也不同:访问与自定义事件帮助理解业务,用户访问过程、请求与错误帮助排查,性能记录帮助找优化空间。

开场三个问题分别对应业务规模、用户特征与时间趋势:能否大致说出项目的 PV、UV,Android 和 iOS 用户谁更多,高峰在哪个时段?他并不要求现场报出精确数字,而是提醒做业务的前端工程师,如果连数量级和用户分布都不知道,就很难判断一次发布、一次反馈或一个性能优化究竟影响谁。

这些证据让前端能参与更多业务判断。遇到有争议的需求、用户反馈或体验问题时,工程师不只负责按图实现,还可以通过数据提出解释与建议。不过,有数据不等于已经有结论,仍需要理解业务含义。

为什么最终选择自研

他比较了当时接触过的几类方案。Sentry 的错误上下文和 source map 关联对定位代码有帮助,但团队常常从“谁在什么时间访问哪个页面”开始追查,因而更希望按用户、页面和请求组织信息。

他肯定 Sentry 提供的用户事件序列:一次错误前用户点过哪些元素、发过哪些请求,有机会与堆栈和 source map 一起看。但他在自己的业务中发现,大多数追查并非从一条 JavaScript 错误开始,而是业务同学给出一个人、一段时间或一张截图。以错误为中心组织的入口,与他们实际的排查顺序并不一致。这是团队使用习惯的差异,不能概括成 Sentry 没有价值。

其他方案的页面、接口、性能面板更接近所需形态,但日志规模带来的费用、私有部署和数据存放方式,又影响选择。团队最后决定自研,并借鉴成熟方案的信息组织方式。

卢依宁特别提到每天大量日志带来的成本,以及用户行为、请求参数等数据留在何处。他希望既有按页面、请求、性能拆分的查询入口,又能让团队控制部署和数据。自研并不是因为界面上没有可借鉴的东西;相反,他拿现成产品的功能面板反推自己究竟要采集哪几类数据。

这段比较说明的是当时团队的需求与选择,不能推出某个产品普遍无用,也不能把历史报价或当时没找到的部署选项当作今天的产品结论。

架构分成采集、处理和查询

方案由 Web 与小程序 SDK 采集日志,服务端接收后清洗、转换,再存入相应系统。原始日志进入 Elasticsearch,由 Kibana 查询细节;另一条链路产生时序指标,通过 Grafana 观察曲线。

他把这画成从采集、接收处理到查询的三段链路。原始访问、接口和错误记录适合在 Kibana 里逐条筛选;经聚合的页面访问、地域与接口曲线则在 Grafana 里发现趋势。只看曲线,难回答“这个司机当时做了什么”;只看明细,难发现某个时间窗口的整体变化。两条路径服务于不同粒度的问题。

两类界面服务于不同任务:大盘帮助发现什么时候、哪个范围发生变化,明细查询帮助深入到某一次访问或请求。页面访问、接口、错误、资源、性能及自定义事件,也应各自有明确用途,而不是尽可能多收集一切。

先设计接入方式,再组织 SDK

卢依宁希望接入尽量简单,例如引入脚本并指定项目。SDK 同时提供配置,控制监控项、忽略部分请求,以及在业务允许的范围内关联用户标识。用户关联用于把业务反馈与技术记录接起来,不意味着所有字段都应该无限制记录或公开访问。

最简接入是一段脚本加项目标识;业务在拿到用户 ID 后,再通过包提供的方法补上用户和类型等信息。监控项可以开关,请求也可设置忽略规则。这样的次序解决了一个实际问题:SDK 初始化时未必已经知道用户是谁,但用户反馈到来时,日志必须能与业务身份对应。

实现上,各监控模块负责采集,统一发送模块处理延迟、批量、重试和退出时的发送策略。这样可以避免每个采集项各自实现一套上报逻辑。

上报策略要考虑体积、连接和退出

片中比较图片请求、POST 和长连接。图片请求简单,但 URL 承载数据有限,不适合任意大的批量;POST 更便于在请求体传数据,但要处理跨域及页面退出等情况;长连接则增加连接管理和服务端资源成本。

团队选择批量 POST,通过队列聚合、定时发送并处理失败重试,页面退出相关场景再考虑 sendBeacon 等机制。这里的取舍取决于日志量和使用环境,不是说某种方式在所有应用都不可用;请求体和浏览器后台发送也都有实际限制,退出时发起发送不等于绝对保证送达。

图片请求的优势是实现简单、兼容性好,也避开了普通跨域请求的一些麻烦;代价是 GET URL 长度有限,不能把上百条日志无限拼接。POST 可以在 body 中放批量数据,却要处理监控域名与业务域名的跨域配置。WebSocket 只建立一次连接,但用户数上来后,长期维持连接会把压力移到服务端。卢依宁于是选择以 POST 为主,每隔一段时间把队列中的记录合并发送,失败时重试,页面离开时再用专门的发送机制减少遗漏。

Web 采集:保留原行为,再插入观察逻辑

请求。分享以 XMLHttpRequest 为例,在原有方法外记录需要的信息,再调用原实现。关键不只是取得数据,还要避免破坏业务原有调用和回调。这个示例说明一种采集入口,不代表已经覆盖所有浏览器网络通信方式。

具体做法是先保存原型上的 open 和 send,包装它们以记录请求信息和响应事件,再用原有方法继续完成请求。这样 SDK 能看到地址、耗时与业务需要的返回信息,同时不让采集逻辑代替真正的请求。监控代码若改变了业务行为,记录就失去可信度。

错误。除 window.onerror,还关注未处理的 Promise rejection,以补充异步失败的线索;记录内容包括消息、堆栈与位置等可获取信息。采集到事件后,仍要结合页面和请求判断影响。

页面访问。单页应用不会每次路由变化都整页刷新,因此需分别观察 hash 与 History 路径。片中展示监听 hashchange、包装 pushState 和 replaceState,以及记录前后地址的思路。这是核心示例,完整接入仍要覆盖应用实际导航行为。

他解释为什么需要记住上一次 URL:一次 PV 不只是“现在在哪个页面”,还要知道从哪里进入。hash 路由可在变化事件里读取当前地址;History 路由需要观察应用调用的两个改写方法,再在地址确实改变时触发采集。两条路径最终都要形成“前一页—当前页”的访问关系。

资源。通过 Performance API 查看资源条目,再按类型筛选需要的静态资源,避免把接口或 SDK 自己的发送混入资源统计。不同类型、时序与浏览器条件下可取得的信息并不完全相同。

他现场展示资源条目中 URL、发起类型与 duration 等字段,用于找出脚本、样式、图片和媒体的加载情况。列表也会包含请求与上报一类条目,因此不能把 API 返回的全部记录直接叫作“静态资源耗时”;应按 initiatorType 与监控目标筛选。

体积优化与小程序适配

SDK 自身也会增加页面负担。卢依宁展示当时精简版实现的体积,并解释功能范围较窄是原因之一;同时通过可压缩的局部变量、复用引用和较简单的打包封装减少重复代码。压缩前后数字对应特定实现,不能只比较 KB 就判断不同监控系统优劣。

他给出的当时数字是压缩后约 9.3 KB,gzip 传输后约 4.3 KB。演示中的一个小技巧是把反复出现的全局对象或原型属性先交给局部变量,让压缩器把变量名缩短;另一处是避免为这个小 SDK 引入较重的模块包装。这些是该版本的体积工程,不表示监控能力可以不计功能差异只按包大小排名。

小程序部分则先抽象不同宿主的全局接口,再包装请求、页面生命周期和应用错误入口。这样公共逻辑不必到处写死某一平台变量,平台差异可以集中处理。总体原则仍是保留原业务行为,插入采集逻辑,再统一发送。

微信、支付宝及其他小程序的全局对象名称不同,SDK 先判断所处宿主,再向后续逻辑提供统一引用。请求采集包装宿主的 request,页面访问需要接到页面生命周期,应用级错误则接到 App 入口。卢依宁用这一组例子说明同一模式怎样迁移:保存原方法或配置,插入采集,再继续原行为。

两条排查路线:从人出发,从曲线下钻

司机反馈往往只有截图、身份和一句“按钮点不了”或“页面打不开”。团队先把业务标识转换成可查询入口,再按用户、时间、页面寻找记录,继续检查请求及错误,并利用链路标识和后端协作。这使前端日志成为连接业务反馈与服务端信息的桥梁。

团队把司机 ID 到监控查询链接的转换做成命令行工具,避免每次都手工拼条件。拿到日志后,再看页面访问、接口请求及透传到后端的 Trace ID。卢依宁把个案排查概括为“谁、什么时间、在哪个页面、做了什么”,然后沿这条链找异常,而不是先猜是哪一行前端代码坏了。

另一条路线从整体异常出发:先看访问或业务接口结果的曲线,再按时间、页面、状态分布筛选日志。业务返回码与 HTTP 状态不是同一个维度,需要按本产品语义解释;流量突变也只是线索,不能自动等同于故障。

案例一:白屏背后是参数与业务链路

收到点击入口后白屏的视频时,卢依宁先检查发布情况及影响范围。页面本身已经被打开,空白发生在内容显示,于是按反馈时间查相关请求和日志。

异常返回把范围缩小到具体请求,再对照入口和参数,发现其中一项参数有误,随后与后端共同处理。日志提供证据,但要知道参数代表什么、上下游如何调用,仍依赖对业务的理解。

案例二:某个渠道数据骤降,先与其他入口比较

运营发现一个渠道的数据突然下滑。团队对照网页和客户端的发布时序,再查看该渠道入口页面的访问量:相应时段确实下降,而其他渠道没有同样变化。

这种比较让疑点转向渠道投放,提供给运营继续确认。它不是仅凭“某天没有发布”就排除技术问题,而是把多条时间与流量证据拼在一起,缩小需要调查的范围。

案例三:城市定位异常,实际暴露了默认值问题

司机注册需要填写城市,原有流程会尝试自动填入位置。某段时间后,特定城市区域的无效注册明显增加,但相关前端功能没有相应修改。

团队分析异常数据,发现它们不是均匀落在各区,而是集中到特定区域;部分来源 IP 只能定位到省,不能准确到市。继续结合当时应用权限行为的变化,推测更多请求进入了 IP 定位与默认值路径,把不确定的信息填成了看似确定的城市。

片中的具体表现是广州越秀区的无效注册异常集中,其他省份也有类似问题。按当时的访问记录,广东省一部分 IP 只能查到省级信息,查不到城市;这些记录落入默认城市或中心区域的路径。前端这段代码没有发布,却在九月变得更严重,使团队进一步联系到当时 App 和浏览器对定位权限的收紧:拿不到更准确的授权位置后,更多用户走到了 IP 兜底。权限变化与故障加剧的关系是讲者对数据和背景的分析,而不是仅凭一条日志就能证明的因果。

最后推动产品调整:在可用且获得授权的环境获取更可靠的位置;无法可靠判断时,放弃自动填充,让用户选择。案例的要点不是某种定位永远准确,而是不要用一个确定外观的默认值掩盖不确定性。

案例四:请求突增,通过前后端证据协同处理

服务端发现接口请求突然增加后,前端先确认页面侧是否也有相应访问和请求变化,再按时间窗口与关联标识查找异常活动。

片中展示的记录包含短时大量请求、较少页面访问和其他异常特征,团队据此怀疑自动化异常请求,并联系安全人员一起限制异常访问。日志关联帮助提出可验证的判断,但浏览器标识并不等于对真实自然人身份的确定证明,仍需与服务端和安全团队的证据结合。

在缩小的时间窗口内,同一浏览器标识对应约一万次请求,却只有 28 次页面访问,还出现多个来源 IP。它说明请求确实沿页面链路出现,但频率与正常点击不相称;因此不能只把它当作“服务端接口被外部脚本直连”,也不能因为请求来自页面就认定为正常用户。团队与安全同事结合 IP 和访问频率采取限制。

告警必须理解业务节奏

同一次故障,不同页面的曲线可能朝相反方向变化:正常业务入口下降,用户求助或反复尝试的页面上升;恢复后又可能出现积压需求带来的反弹。监控因此需要读懂角色与业务路径。

团队尝试过按短时访问倍增或减半告警,但也遇到有明确上下班和午休规律的内部系统:规律性涨跌会让简单阈值反复误报。卢依宁最后强调,规则需要按业务特征调整,不能指望一条统一阈值适用于所有项目。

分享举的通用阈值是十分钟内页面访问 QPS 翻倍或减半。订单等业务故障可能让正常入口下降,却让客服和司机反复查看的页面上升;恢复后积压需求又会造成一波反弹。另一个给客服使用的内部系统每天上班、午休、下班都出现陡变,照搬阈值就会报出“故障”。同样的曲线形状,在不同业务里有不同含义。

这也呼应开场的问题:监控价值不止来自部署了 SDK,而在于团队会用数据理解业务,并把发现的问题推进到解决。大盘与明细、技术事件与用户行为,需要在真实排查中持续连接起来。

查看原视频与分段信息:大前端时代的挑战与机遇:深圳场 →