文 | 元三 网易智企资深前端开发工程师
一、前言
笔者所在的公司业务主要是为企业提供全流程的企业服务和一整套 SaaS 解决方案。对于企业服务 SaaS 产品来说,客户完成购买并不意味着产品价值已经完全交付,因为客户在首次购买产品时,往往需要经过一系列培训并使用后,才能真正产生价值。因此从本质上来看帮助客户解决在使用过程中出现的问题是 B 端产品中提供的有偿服务,是产品价值链条中非常重要的一环。
本文将着重介绍开发者排查客户反馈问题这个场景下前端日志库的应用,以及如何设计开发适用于此类场景的前端日志库。
二、SaaS 业务下前端面临的挑战
笔者以前是做 C 端业务出身,天然带着 C 端业务的思维,觉得前端把产品交互体验做到极致就够了。当我这个做法套用到做 SaaS 业务上,着实吃了不少亏。B 端产品和 C 端产品在付费方式的差异、购买决策人和实际使用者不同、产品用途不同(B 端客户购买产品的根本原因是为了帮助企业赚钱,C 端产品购买决策有可能只是一时冲动好玩),导致 SaaS 企业经营关注的指标和 C 端产品存在较大差异,间接导致了对研发侧的导向不同。C 端业务前端在研发资源投入上可能为了用户体验不计成本,优化网页性能以提升用户粘性,表现在移动互联网、电商等行业往往关注 DAU、MAU、GMV 等量级指标。B 端产品的核心关注大部分能否帮助客户提升效率,产品能否帮助客户达成他的工作目标、能否帮他快速达成目标比产品界面是否美观重要得多。衡量一家优秀的 SaaS 企业有一项比较重要的指标——NDR(收入留存率),对 SaaS 业务的前端开发来说,首要解决的挑战来自如何通过软件研发工作去提升产品易用性、任务效率、服务效率等指标,从而为企业带来提高 NDR 的分子(存量客户的续费+增购)的效果。
B 端和 C 端对比
B 端 | C 端 | |
---|---|---|
用户场景 | 清晰的目的,帮助企业提升效率和质量。 | 用户目的不清晰,主要是为了愉悦和消磨时间。 |
页面交互方式 | 流程严谨、低风险、高效率 | 操作简便,信息简洁,有娱乐性、社交性 |
常见付费方式 | 按年预付 | 免费 |
常用经营指标 | NDR、CAC | DAU、MAU、GMV |
值得一说的是,在这方面《云计算软件产品使用体验质量 度量模型及度量方法》也提出5项指标维度用于衡量产品使用体验,非常具有参考性。这些指标维度包括易用性、任务效率、满意度、一致性、页面性能。其中易用性包括易操作性、易学性、清晰性,任务效率包括功能利用率、任务完成率、任务完成耗时。基于 SaaS 产品收入可持续性的考量,SaaS 企业的目标之一是提高依靠软件产品输出价值的比重,降低依靠人工服务输出价值的比重,因为只有软件产品输出价值边际成本最低,才能不断提升产品服务效率。在这一点上,纯粹依靠人工服务终归是边际成本非常高的,因此在 SaaS 业务场景下依靠技术创新去提升解决问题的效率是前端能够提供的非常大的产品价值。
三、如何解决客户反馈问题
当我们把视角聚集到客户反馈问题的解决上时,可以先将客户反馈问题分为功能咨询类、问题报障类、售前咨询类。其中开发者主要关注的是问题报障类,也存在一些技术支持回答不了的功能咨询类问题会流向开发者,针对这类问题一般可以采用建设内部的问题排查系统来解决。其中前端开发者主要遇到的反馈问题既有来自于 SDK 接入这一类的咨询,也有客户认为产品功能不符合预期的问题报告。
针对此,前端为了有效且快速定位这些问题原因,一方面可以在客户端打日志并上报到问题排查系统之中,另一方面,对于 S 类 A 类客户(基于 SaaS 企业针对客户企业规模的分层模型)的紧急问题,如有必要可以迅速和客户沟通,使用远程协助之类的工具在客户的设备中复现并定位问题原因。对于后者,我们设计开发了基于 Chrome 浏览器 Chrome DevTools 协议的远程调试解决方案 woodpecker-remote,它能够支持网站开发者对网站用户的 Chrome 浏览器直接进行远程调试。对于前者来说,我们设计开发了前端日志库 woodpecker-log 以支持将客户端运行状态等信息进行持久化存储供开发者调试排查问题。
四、前端日志的概念
这里先介绍一下前端日志的概念。通常来说一般在后端开发时经常会听到日志的概念,对后端来说日志是指一种用于记录服务端启动、运行状态的文件。这里的前端日志指的也是用于记录客户端运行状态在客户端存储或者上传到服务器存储形成的日志文件。一般前端在开发、测试环境使用 Console 记录运行状态就够了,但在生产环境就需要将客户端日志信息发送到服务端存储起来,方便日后排查定位用户反馈问题时使用。
五、传统的前端日志存在的问题及挑战
- 前端日志库普及率不高,感知差,和异常、性能监控等各自林立。
- 打日志不规范,各种日志格式乱象丛生。
- 日志库本身占用前端性能预算,在性能方面需要考虑尽量减少对 JS 主线程占用开销,保障主线程尽量空闲。一部分前端日志库在服务器端存储日志,日志产生后需要即时上传到服务器,需占用带宽,并挤占浏览器同域名请求最大并发数,从而拖慢正常业务 Ajax 请求,需要平衡上报频率和每次上报日志大小。
- 大型应用(如千万、亿级用户数的应用)容易产生巨量日志,日志与 Bug 之间存在的关联度低,检索和分析操作成本高,如使用 Elasticsearch 存储海量日志,kibana 查询效率低。
- 日志开发及部署不方便,这里有一些问题,前端开发者需要提前在代码中预埋打印日志的代码。否则,当需要定位问题的时候不存在相关的日志无法进行分析。
- 日志库缺少问题的上下文,无法对一个完整的会话进行追踪。如不支持记录用户访问页面发生问题前后的用户界面交互操作记录,以及页面跳转等行为轨迹。
- 问题反馈链路长,如果不能和产品深度集成则容易在反馈链路中间丢失线索,造成沟通成本高企,解决办法可以是在客户上报问题时自动带上当前的日志信息,关联内部的工单、Jira 等问题反馈解决系统。
六、基于 B 端业务的前端日志库设计
上述问题中,首要解决的是日志和用户反馈问题相关度的问题。核心思路是使用客户端进行日志存储,在发生问题时由用户或者程序发现进行主动上报,而不是定时定频率上报到服务器。这里留两个问题:用户如何发现问题?程序(员)如何发现问题?另外,性能问题和JS异常也是产生客户反馈的问题来源之一,但从日常SaaS业务的客户反馈问题来源统计来看,这两块并不是主要来源。另外的JS异常监控、性能监控两个领域已经有比较成熟的前端基建支持。因此,非JS异常和性能问题导致的客户反馈问题是前端日志库主要覆盖并解决的问题。
6.1 一些典型的需要记录前端日志的场景
首先在开始设计之前,先思考一下,前端会如何使用日志库。有这些典型场景可能需要前端记录日志。
- 调用第三方服务,做最坏打算如果第三方服务不可用怎么办。
- 性能预算很低、对性能非常敏感的页面,需要上报一些性能数据。
- 需要重点监控的网页核心流程,使用 JS 断言结果为 false 时,需要记录断言失败原因。
对第3种场景,这里简单列了在程序断言为 false 时使用前端日志库记录日志的 Demo:
6.2 日志库 SDK 的可维护性
相比于几千行代码在单一文件内维护,将 SDK 独立成项目并采取前端工程化方式开发更具备可维护性。前端工程化是指采用模块化、组件化、规范化、自动化的技术方案从软件工程的角度解决工程的质量、可维护性问题。这里列举了一些关键技术选型:
- 编程语言
对于 SDK 相对底层的代码来说,Typescript 语言天然提供的类型文档具备可读性和易读性,静态类型检查可以帮助框架或库的使用者在代码运行阶段之前发现错误,智能语法感知可以提供有用的 API 类型提示。
- 构建
需要考虑为 SDK 的使用者提供多种 JS 模块规范的支持,以 rollup 为例,配置如下:
- 自动化测试
在开发阶段对 SDK 的自动化测试主要关注单元测试和集成测试。单元测试是用于对模块、函数或类进行正确性检验,可以采用 jest 框架。值得注意的是,对 indexedDB 存储和查询进行单元测试时需要模拟数据库,可以使用 fake-indexeddb 来 Mock。集成测试的目的是将系统之间的各个模块组装起来并使用真正的外部依赖、访问真正的 indexedDB 数据库对代码进行正确性检验。此例中我们采用了 Karma + Mocha + chai 的方案,对 ChromeHeadless、FirefoxHeadless、Safari 浏览器进行测试。
- 版本控制
基于语义化版本规范 semver 进行版本控制。
- Demo 和文档
6.3 使用 indexedDB 在客户端存储日志
localStorage 适合对少量数据进行 key-value 存储,在客户端日志存储的场景中使用 indexedDB 更加合适,它具备以下优点:
- 存储和查询结构化数据,支持二进制
- 支持事务
- 异步
- 处理大量数据
假定使用 10M 容量,300bytes 的日志,可以存 34952 条;最长支持循环录制 8 天日均 4369 条。
6.4 性能开销
网络性能(延迟、请求失败率)——日志长度、请求体积
- 使用独立域名服务器处理日志请求 Chrome 对同一域名的并发最大连接数限制
- DNS 预获取 dns-prefetch
日志存储前进行字符串压缩
- 使用基于 Gzip 算法的 JS 实现 LZMA-JS, 实测 Level6,300bytes,压缩率79%
sendBeacon
- 数据可靠异步传输,并且不会延迟页面的卸载或影响下一导航的载入
合并请求
- 合并多个小体积的日志分页上报,单页约 1M
- HTTP/2 头部压缩
运行性能
- 全异步非阻塞式操作,存储、检索、上报
- 维护存储队列支持批量日志存储操作
6.5 API 设计
SDK 初始化设置
参数 | 类型 | 释义 | 默认值 | 是否可选 |
---|---|---|---|---|
options.appKey | String | 实例记录日志时会存储的应用名称,用于区分不同应用记录的日志,不传时实例使用 $anonymous 作为应用名 | $anonymous | 可选 |
options.bytesQuota | Number | 设定客户端可使用的 indexedDB 存储上限,单位为 MBytes。不同应用共用存储上限,超出上限后,将启用循环记录功能,自动删除最早的日志 | 10 | 可选 |
options.reportUrl | String | 传入后 report 方法将使用该地址作为上报日志的服务器地址,如不传,则需要在调用 report 时指定该参数 | -- | 可选 |
options.enableSendBeacon | Boolean | 开启后使用 sendBeacon 上报日志 | FALSE | 可选 |
options.debug | Boolean | 开启后在客户端 console 控制台打印调试信息 | FALSE | 可选 |
实例方法
方法 | 释义 | 示例 |
---|---|---|
trace/info/warn/error/fatal | 日志记录到客户端 | wpLog.trace(content: string); |
queryByDate | 按发生时间检索日志 | wpLog.queryByDate(startDate?: number, endDate?: number); |
queryByContent | 按关键字检索日志 | wpLog.queryByContent(content: string); |
report | 日志上报到服务器 | wpLog.report(startDate?: number, days?: number, reportUrl?: string, session?: boolean, env?: boolean); |
6.6 线上出现问题,但是代码中没有打日志怎么办
我们常常需要发布前就在代码中设计好业务关键流程执行时需要打印的日志。否则,当我们需要定位问题的时候,才发现自己并没有输出相关的日志,这样就会比较被动。此时只能临时改代码加日志,重新发布。有没有一种方案,可以在遇到问题的时候,再去代码中相应位置加日志,用户执行改业务流程时就能立刻打印出相关日志,而不用重新走一遍发布流程。 这里介绍一种在 woodpecker-proxy 中的实现,借助 MutationObserver 接口监听 script 插入 DOM 事件,改写浏览器 JS 请求,将其代理到目标服务器,从而实现在目标服务器修改 JS 加入日志代码即可在用户浏览器记录日志。Demo 地址:DEMO。
6.7 日志规范——分级别、分应用打印日志
遵循良好规范记录的日志,有利于排查问题时能够快速根据信息级别、应用进行日志筛选。
分级别
日志级别 | 释义 |
---|---|
trace | 主要输出调试性质的内容。 |
info | 记录系统的正常运行状态,某些重要的业务处理已经结束。 |
warn | 发生这个级别问题时,处理过程可以继续,但必须对这个问题给予额外关注。 |
error | 错误发生时,已经影响了用户的正常访问,也需要马上被处理,但是紧急程度要低于 FATAL 级别。 |
fatal | 致命错误,系统中发生了非常严重的问题,必须马上有人进行处理。 |
分应用
由于客户端存储受同源限制,日志访问只能在自身域名下进行。多个应用可能会在同一域名下记录日志,区分应用名进行存储易于隔离不同应用的日志信息。
6.8 问题的上下文需要收集哪些信息
- 设备、浏览器、页面信息
- 关联一次会话的用户界面交互操作
- 关联一次会话的页面跳转
6.9 如何在收到客户反馈时快速找到相关日志
- 将用户 id、会话 id 存储到日志中并建立索引。
- 将日志上报功能集成到 SaaS 应用,在客户反馈问题时自动查询当前会话日志并上报。
- 在客户反馈问题时将用户 id、会话 id 写入到内部的工单系统,提 Bug 时带到 Jira 系统。
七、未来努力的方向
加强可靠性
- 网页崩溃时如何保障前端日志库依然正常工作,记录此时的日志?使用 Service Worker 监控网页崩溃。
更直观的问题上下文环境
- 采用浏览器录屏方案录制出现问题前后的用户界面。
更友好的客户通知和告警
- 使用 Notification桌 面通知。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。