客户线上反馈:从信息搜集到疑难 bug 排查全流程经验分享

发布时间 2023-05-28 23:45:43作者: 听风是风

写在前面:本文是我在前端团队的第三次分享,应该很少会有开发者写客户反馈处理流程以及 bug 排查的心得技巧,全文比较长,写了一个多星期大概1W多字(也是我曾经2年工作的总结),如果你有耐心阅读,我相信在未来的问题排查上,一定会对你的思路拓展有些许帮助,考虑到篇幅问题,建议在阅读前泡上一杯茶,那么本文开始。

一、引

在过去,我曾有两年的时间每天跟各种奇怪或者难以解释的 bug 打交道,2年时间累积处理 bug 1200+,所以在问题处理这块还算有一丢丢经验。修复 bug 研发往往需要先复现,而复现需要依赖一些关键信息,比如用户操作路径,日志信息等等;但站在组织架构角度,研发同学一般不会直接跟客户打交道,所以在客户提出问题的同时尝试搜集必要的 bug 信息对于整个bug修复流程很有重要,而这一部分是与客户对接的同学应该掌握的部分技巧。当然在拿到信息后,对于 bug 提出猜想,快速论证就是研发同学应该思考的问题了。

回到本文,面对CS团队和研发团队同学,我想分别进行一次分享,对于与客户对接的同学:

  • 帮助大家掌握一些更为通用的引导排查方式,这会直接影响后续研发同学的排查方向

  • 知晓在与客户沟通过程中,哪些信息是研发希望得到的,让沟通更有效。

  • 教会大家在不同浏览器上如何拿到这些信息,只有大家先会才能引导客户操作。

对于研发同学:

  • 问题复现的一些技巧

  • 在掌握初步信息后如何拟定排查方向

  • 帮助大家培养问题分析的思路,当一个可能是你完全不了解的业务 bug ,如何切入问题提出猜想

  • 案例分析,我会结合之前排查的疑难问题,说明我是如何运用这些经验一步步推导结论。

通过一次分享让大家问题分析能力立马成长起来有些天方夜谭,但起码这些经验多多少少能帮助大家,帮大家建立一些基础。

我个人很喜欢侦探剧,福尔摩斯,金田一耕助,柯南,在面临案件时细心的搜集线索并推导真相,这个过程真的太帅了。修复 bug 其实也是一场小小的侦探剧,希望大家在面对一些奇怪问题时,也能享受解决问题的过程(当然不出问题是最好的)。

二、客户沟通原则

  • 可以必现的问题不需要再找客户搜集信息,所以在与客户沟通前最好先尝试是否可复现客户反馈的问题。

  • 尽量少的打扰客户,尽管这与我们从客户处获取信息相矛盾。当客户愿意主动反馈问题时,从心理上讲对方是期望得到回复以及问题得到解决,因此在第一次沟通时获取信息客户大多数都愿意配合,随着沟通次数逐渐变多,耐心递减,因此我们才需要列出一份清单,尽可能一次沟通拿到更可能多的信息。

三、前期通用排查手段

产生 bug 的原因往往千奇百怪,即使是Chrome浏览器自身也可能会出现 bug,从而导致我们的业务功能无法按预期执行,因此在搜集信息前我们可以引导用户做一些常见的排查,如果这些操作能直接帮客户解决问题,那就万幸了!!!!

3.1 是否受缓存影响?

这里所说的缓存不仅仅是浏览器缓存,还包括浏览器对于代码资源的缓存,我们分开解释:

浏览器缓存:包裹cookies、本地缓存、indexDB等等,这些缓存一般存储用户数据,账号信息,关键逻辑缓存等等。验证是否受浏览器缓存影响最直接的是让用户新开一个无痕浏览器测试功能是否正常即可,如果无痕正常但非无痕有问题,那就可以确定是这个问题了。

比如我在四月我就遇到了只有我一个人无法登录sentry的问题,新开无痕正常,原先的浏览器怎么刷新都不行,最后通过修改密码强制更新本地token解决了这个问题。关于清除本地缓存的操作,这里以Chrome为例,步骤为:打开浏览器控制台(关于如何打开控制台见浏览器开发者工具篇章)> 选择Application > 点击Storage > 勾选你想清楚的缓存,比如Cookies、indexedBD,不知道就全勾了 > 点击 Clear site data 即可。

浏览器资源缓存:除了浏览器缓存外,浏览器本身也会存在对于代码资源的缓存,比如我们的功能版本之前是1.0,发版后版本升级为1.1且功能有了大变动,用户在刷新浏览器后就应该使用我们1.1版本的代码资源,但是可能会存在浏览器资源更新不彻底造成的功能问题(我以前遇到过多次)。这种情况也可以使用无痕浏览器先验证排查,如果发现有问题回到原先的页面,使用快捷键 command(mac)/ctl(Windows) + shift + R 进行强制刷新即可,注意是强制刷新而非普通刷新,除此之外我们还可以打开控制台后,鼠标长按浏览器刷新图标,之后选择 清空缓存并硬性重新加载,注意这个操作只有打开控制台才行。

其实说来说去,缓存这块都用无痕来验证就好了,只是清理缓存有一些差异。

3.2 是否受浏览器插件影响?

客户自己安装的浏览器插件也可能影响我们的业务功能,而验证手段同样还是使用无痕模式,因为无痕模式默认不会加载你安装的插件,同理让用户使用无痕验证功能也是排查插件的影响,我以前也使用这个办法定位过具体是哪个插件造成的bug,非常实用。

到目前为止,无痕模式能帮我们排查是否是浏览器缓存,浏览器资源缓存,以及插件的影响。

3.3 更换其它浏览器是否解决?

会存在Chrome自身就有 bug 的情况,比如之前我们遇到了 mac Chrome 107 版本录音音频30S之后被加速以及出现噪音的问题,issue地址,而这个问题如果使用Windows版本的Chrome,或者其它浏览器均不会遇到,这就能证明就是Chrome官方的bug,因此更换浏览器也是一个在早期尝试解决问题的好办法。

3.4 网络问题更换WiFi或者链接手机热点是否可解决?

有时候用户会遇到打开我们项目页面非常缓慢的情况,除去资源加载性能优化相关,用户自身的环境网络也会与这些问题息息相关,比如防火墙拦截导致某个接口无法调用页面加载不出来,自身网络慢导致页面加载缓存等等;

比如我以前排查过一个客户反馈,用户提供的截图中好巧不巧把自己的360加速球给截进去了,在这个图标上可以看到用户在加载项目页面时网络上传下载只有几kb,可以确定就是客户自身网络太差所导致,因此更换网络环境也是前期一个不错的排查方法,比如切换WiFi,使用移动热点等等,前提用户反馈与加载缓慢有关。

四、信息搜集清单

在前期排查引导中,如果更换浏览器,使用无痕模式都无法解决客户问题,且这个问题我们还不是很好复现,那就需要在与用户第一次沟通中搜集一些关键信息,这对于我们模拟复现场景有很大的作用,这里我先列举需要的信息清单以及为什么需要,如何获取见下一章节:

4.1 用户出现问题的复现路径

为什么需要这个?我在以前遇到过客户反馈,说工作项列表的筛选失效了,但是我发现我怎么都复现不出来,无奈让售后找客户要了张页面截图,才发现客户说的是全局筛选而不是具体项目下工作项筛选,然后组件都是用的同一个,但不同场景下数据传输就是有差异,除此之外甚至用户的操作顺序差异都会干扰你,所以能问清楚一点是最好的。

4.2 客户出现问题使用的操作系统和版本

操作系统一般是 Mac 或者 Windows,为什么需要这个呢?我曾经遇到过用户反馈说微信赋值图标粘贴到产品富文本编辑器中每次都是两张....但是我们的功能近期又没变动,且我根本没法复现,最后与客户沟通要到了电脑操作系统类型以及版本,然后通过微信赋值就稳定复现了,追溯了粘贴板里面的数据,发现用户在电脑CV时这个数据就已经不对了,最终确认是Window10 + 微信图片赋值 + Chrome粘贴就能浮现,而且对比了当时的语雀,金山云文档都出现了此问题,最后给客户解释了原因也能接受,之后前面提到的软件某一个版本变动,平衡被打破,这个bug就不存在了。

4.3 客户使用的浏览器类型与具体版本

为什么需要,前面其实已经举了一个例子,Chrome 107 音频加速的问题就是通过浏览器类型以及版本对比最终排查出来的结论。

4.4 控制台信息

有没有报错信息。引导用户打开控制台,点击console这一栏,点击清除之后,让用户刷新页面并再进行一次有bug的操作,看看控制台是否会出现红色的报错信息,如果有麻烦用户提供控制台截图。

4.5 客户遇到问题时的截图或录屏

这个是针对出现问题时的功能界面的截图,或者用户操作的路径的录屏,可能会涉及用户隐私,所以最好征求下客户的意见。

4.6 此问题是否是近期突然出现还是一直存在

用于判断是否跟最近的新功能有关,也方便研发同学快速缩小范围定位。如果问题一直存在,那就可能是一个隐藏的历史问题。

4.7 对于公司群体用户是否只有特定用户遇到了这个问题

比如一个企业购买了我们的产品,如果某个人反馈问题,最好麻烦下ta询问下其ta同事是否也会遇到这个问题,如果只有ta自己遇到,前端角度那就可能与本地缓存,插件,特定浏览器类型有关,站在后端角度可能与此客户的用户数据有关,比如脏数据,这个方法曾经帮助我解决过不少脏数据以及环境问题问题。

4.8 能否提供har文件(不一定可行,涉及数据隐私)

har文件是指浏览器记录用户操作过程中的接口数据文件,所以这个其实涉及用户隐私,是否能提供需要征求用户的同意;在过往除非是很严重的问题但我们又毫无头绪,用户又急着解决,才会尝试与客户沟通提供har文件,这里单纯提一嘴,如何提供见第五小节。

4.9 性能问题的performance文件

Chrome可以通过performance录制页面渲染的过程,比如什么脚本执行比较耗时,这些performance都能以火焰图的形式展示出来,这个不涉及用户个人隐私,只是展示页面渲染的情况。在用户反馈使用卡顿但我们无法复现时,可能需要用户帮忙提供。比如在以前我们就接受过用户反馈页面加载直接崩溃,但是我们自己怎么无法复现,在拿到相关信息后才知道客户员工就有上万人,用户数据格外庞大,我们没这个用户数据,怎么想的上去哪段逻辑有性能缺陷呢。

4.10 是否是私有部署用户(我们暂无私有部署,但还是先列在这吧)

先说下SaaS 客户与私有部署客户的区别,SaaS 客户所使用的软件版本受我们发版影响,我们发到哪个版客户就得用什么版本(比较被动),且用户数据均在我们的服务器保管。而私有部署的区别在于,客户可以选定一个他们认为稳定的版本单独部署(比如 LTS 版本),且用户数据客户自行管理,我们后续只提供售后以及升级服务。

后者优势在于版本不会受 SaaS 版本影响,功能会更稳定(有的客户并不追求功能新特性,企业大了对于软件就是求稳),要不要升级自行决定,且用户数据自行保管,站在企业角度肯定更安全更稳定了。
因此如果未来我们也存在私有部署客户,在客户沟通过程中需要确认客户使用版本类型,如果是SaaS用户那默认就是最新的代码版本,而如果是私有部署则需要了解用户所部署的版本,这样我们才好根据对应版本复现问题,不然在 SaaS 版本分支可能就是大海捞针,完全复现不了也不知道怎么回事。

4.11 电脑多久没关机了?(虽然这样跟客户说客户可能会觉得你不专业)

你看到这个问题肯定想笑,怎么还问这么抽象的问题,如果同一个公司网络环境跑同一个项目,大家都没问题,只有你自己有问题,除开浏览器缓存插件干扰外,重启电脑也大概率也能解决你的问题。电脑在长期不关机运行下,性能体验就是会变得很糟糕,不管你是不是MAC。我在之前遇到过测试给我提 bug ,他说他发现一个标签 hover 的 title 每次要 2S 左右才显示出来,感觉是不是一个 bug ,但是站在我的角度我非常清楚这里的 hover 是原生行为,我根本无法操控,于是我问他是不是很久没关过机了,让他重启,你还真别说,重启就恢复正常了,还是那句话,bug 之所以是 bug,有些时候它就是难以解释。

五、信息提取方法以及开发者工具使用说明

5.1 不同浏览器如何打开控制台

前面提到控制台console看有没有报错信息,清除用户缓存,长按刷新图标强制刷新,以及导出har文件,导出performance文件前提都是打开控制台,所以先说这个操作。

台式机一般都是直接按F12直接打开控制台,但笔记本因为键位比较少,有些 F12 单按就是音量键,所以需要 Fn + F12 可以打开控制台,如果用户不会使用快捷键也没关系,这里以 Chrome 和 Edge 和 Safari 举例,直接鼠标右键,然后选择检查或者检查元素即可。

5.2 控制台信息

当打开控制台后,前面所说的页面报错均可以在 console 栏查看。为了方便用户截图而不被无关的信息感染,可以先点击清空控制台后再让用户操作,之后再截图。

5.3 查看操作系统和版本

Mac:点击左上角苹果图标 > 点击关于本机 > 这里能看到系统名称和版本以及芯片信息

Windows:开始 > 设置 > 系统 > 关于。在设备规格 > 系统类型下,查看你运行的 Windows 是 32 位还是 64 位版本;在 Windows 规格下,查看设备运行的 Windows 版本和版本号。

5.4 查看浏览器版本

如果是 Mac 用户就比较简单,注意此时电脑前台一定是运行了浏览器了,比如运行 Chrome 后点击左上角 Chrome > 关于 Google Chrome 即可看到版本号信息,其它浏览器类似。

Windows用户可以点击浏览器右上角的三个点 > 帮助 > 关于 Google Chrome

5.5 如何提供 performance 信息

同样需要先打开控制台 > 点击 performance > 点击 Record 按钮(一个黑色的圆) > 开始操作等待操作完成 > 等待 performance 生成数据 > 点击下载。

5.6 如何提供har文件

操作与获取 performance 文件类似,同样需要打开控制台 > 点击 Network 后点击清空(避免无用信息干扰)> 刷新页面等待页面加载完成 > 点击下载即可。

六、复现问题技巧

6.1 根据反馈信息造复现环境

在信息搜集清单这一栏我们介绍了十几条可能需要注意的点,说了那么多其实都是在为这里做铺垫。

再次强调一遍,在复现问题时,如果你发现一个问题怎么都复现不了,但是对方可以复现,这个时候需要留意下对方所说的复现步骤,操作系统,版本等等,这些关键信息的能够很好的帮助你复现问题。

6.2 注意出现问题的代码版本

这个在之前确认用户是私有部署版本还是SaaS用户已经说过了一次,如果客户是私有部署版本,一定要问到客户所用的版本,这样你才好去对应的代码分支复现以及解决问题,不然你在SaaS分支玩一年不一定能发现问题在哪....

6.3 跳出惯性逻辑思维

其实挺多研发在测试自己开发完成的功能时,经常拍拍胸部说没问题,也确实没测出问题,但是一旦进入测试环节被测试同学接手,几十条用例一过甚至能测出七八个问题,很大一部分原因就是研发在思考问题上没跳出惯性思维。

我用什么逻辑去实现它,于是我就惯性思维按照这个逻辑流程去测试它,流程在自己心中是符合预期的,自然测试出来的结果也是符合预期的。而测试同学在测试时是不知道你代码怎么写的,ta们往往不受这个思维限制,所以能很轻松的测出、复现出你的问题。

这里的惯性思维比如,不考逻辑的虑边界情况,例如值为空的情况,值类型不符合预期的情况,超出极限长度对于样式的影响,功能的压力测试等等,我们在写代码以及测试功能时都需要考虑,这不仅能让你的代码有更好的健壮性,在测试上也会少出问题(扯的有点远了),同理在复现一个问题时,不要本能用你的惯性思维。

6.4 主动模拟更差的代码运行环境

永远不要在一个很理想的的开发环境去复现测试或者客户反馈的问题,客户的电脑配置一般都比你差,特别你用mac的情况,那因为操作系统不同加上你的性能更好,很多问题你复现不了那都很正常,毕竟像一些事业编单位,win7系统恨不得都给你整出来,那这时候客户给你反馈问题了,你拿你mac简单测测然后复现不了,说没问题没问题,所以主动降低自己的环境配置就很重要,这里的降低分为两种:

当你怀疑 bug 跟接口请求有关系,比如loading异常,接口先发后至等等,你都可以主动降低网速,我们可以打开performance,选择 network ,这里可以直接选择快慢 3G ,当然如果你觉得这个都不够,你可以点击 Add ,在这里自己配置上传和下载网速,比如你想模拟断网,直接设置两个 0kb即可。需要注意的是,在模拟过程你必须打开控制台,如果你关闭了就不生效了。

网络设置在network其实也有这个开关,效果一样,随便你设置哪里都行。

而对于性能问题,比如用户说加载缓慢,电脑开项目感觉很卡,这时候除了接口,你还能主动降低电脑 CPU,同样还是performance,选择降低六倍即可。

七、排查思路,提出猜想

我们从前期排查到信息搜集再到复现技巧,如果前面这些过程都没能帮你解决问题,那说明现在这个 bug 已经顺利摆在你面前了,现在你需要根据现有信息来解决这个问题,那么接下来我们来理解一些我以前经常使用也可能对你适用的排查思路。

7.1 分支对比法

如果一个功能之前没问题,但现在出现了 bug,且逻辑并不是你所写,这种问题在一开始接手确实很难下手,分支对比法是一种你不需要完全理解业务,起手比较粗糙但是非常有效的排查方法。

分支对比的目的有两个,一是你能利用旧分支理解这个功能正确时是什么样子,这个感觉是视觉和体验上的直观,不需要看代码就能明白,在知道正确的表现后,你才好基于现在不正确的现象提出猜想,可能是数据问题?可能是交互异常?它能帮你先理解问题的大概原因,这比你一开始就立马想着看代码要高效的多。

第二个目的就是真的去对比代码了,查下 commit 提交记录,对比文件代码改动差异等等,变化量一般就是问题出现的可能原因,加上对比过程中其实你脑子里也在印证之前的猜想,我用这种方法排查过很多问题,而且我都不是一开始就埋头去读代码。

注释代码缩小范围,比如性能问题,注释,比如某个功能数据有问题,注释

7.2 代码注释法

在一些你已经确定 bug 所在的大致流程,但是不确定具体代码的时候,代码注释就跟二分法一样,能快速帮你切割缩小问题代码的范围,比如一些数据流问题或者性能问题,注释某一行代码,观察问题是否存在,如果存在继续往前注释,从多个文件缩小到某一个文件,从某个几千行代码的文件缩小到某个方法体内,非常管用。

打个比方,现在业务有一个性能问题,根据问题表象我知道大概是哪一块的功能,根据经验我可能找到一个可能的组件,上手就先把这个组件的 return 注释掉,替换成一个最基础的 <div>111</div>,之后再观察问题在不在,如果在,那说明性能问题的代码在这个组件之上,继续往上找,如果不在,那说明问题代码在子组件中,接下的思路应该往下看,代码注释可以说是又笨又好用的方法,比你一行行debug块太多了。

7.3 理解业务、原理,提出猜想

如果一个问题,当你连猜想都提不出时,只能说明你对于问题不够了解,这是我两年前从一位架构师身上学来的道理。

比如一个问题你接手,别人问你对这个问题有什么看法,你发现连个猜想都提不出来,或者某个问题的现象你真的无法解释,推进不下去,那这个时候你就得沉下心来好好去理解问题所在的业务,深挖问题表象下的原理,不跨过这一关浮于表面,一直纠结为什么为什么,时间过去了你还是一样给不出一个合理解释。

我曾经接手过一个这样的问题,之前公司的项目应该是对滚动条做了统一美化,要么展示细长的滚动条,要么就是不展示滚动条。因为UI上统一,用户看到内容展示不全,心理默认就知道这里可能滚动了,但是有个功能的内容横轴展示不全,笔记本用户可能还好,触摸板左右拖动就行,但是你让Windows PC用户怎么玩,鼠标滚轮默认只能上下滚动,用户又不能横着滚,所以这个问题就是让我把这块的滚动条放出来。

没错,就是这么简单的一个客户反馈,我搞了一下午都没解决.....因为在我潜意识里,滚动条应该就是被隐藏了(透明,或者没有宽度),添加显示的代码,主动添加宽高,然后加一些美化让它与展示的地方一致应该就能顺利解决,但是这些代码都加上了,我发现代码好像根本就没生效,因为我怎么改都看不到滚动条!!这直接颠覆了我的认知了,因为这类问题都是这么解决的啊,无奈我就抱着电脑去找了前端负责人(前端巨佬),把我所遇到的问题阐述了一遍,没想到他直接也懵了,不过他对了说了一句这样的话,当你对一个问题非常疑惑的时候,那说明你不够了解它,这时候你就应该去补充对应的知识,而不是继续盲目的尝试。

对,就是他这句话,我意识到我可能完全陷入到自己的思维误区了,不止是我,不止是这一个问题,这真的太常见了,人习惯用已有的经验或者知识来解决问题,当行不通时我们更多会本能诧异这不应该,这不太可能,很少会想到自己的经验或者知识是否一开始就是错的,否定自己或者意识到这一点真的很难,不是吗?

于是我去MDN查了下滚动条属性,才意识到我眼睛所谓的看不到其实是错的,滚动条的颜色,宽高都是独立生效,我以为我看不到代码没生效,但其实它一直都在只是因为没设置背景色,而我就是被所谓的解决经验拖了一下午。

这个问题我在后面记录了一篇文章,感兴趣可以看看 如何修复被隐藏的滚动条,记一个看似简单的样式问题所引发的一系列思考。

所以在面对其它问题也是,如果一个问题你真的走不下去了,思路全断,现在要做的不是抱怨或者无意义的尝试,去补充对应的知识,去真的理解业务,沉进去,你才可能发现问题真正的原因。

九、案例分析(跟着我的思路走)

接下来来分享一个案例,前段时间处理了一个 zoom机器人入会失败的问题,正好演示下整个问题的分析过程。首先说下,我在此之前从未接触过这个业务,也不知道 zoom 参会的原理,所以符合排查一个你完全不了解的领域问题的清净,这个问题面前我们完全平等。

9.1 问题现状:

先解释下功能,现在就是有一个基于 zoom sdk 实现的机器人参加 zoom 会议的功能,假设 B 开启了zoom 会议,现在邀请 A 来参会,如果 A 没空,那么 A 通过这个功能输入会议链接,点击参会,这时候代码逻辑会派遣一个机器人(真人参会也是数据,用数据模拟一个“人”就好了)去参会,然后对会议过程做记录和转写,这样 A 即使不在也能记录整个会议过程。

关于参会的大概过程我顺带也说下,用户输入参会链接后,页面会先进入 zoom 等候室,这个界面用于你设置是否启动摄像头音频,点击入会后会跳转到是否音频入会的页面(第二个页面),这个页面有个电脑音频是否自动参会设置,假设你设置了电脑音频自动参会,那么之后你再从等候室进入页面2,zoom 会自动基于页面 2 跳转到会议室。

如果你没设置电脑音频自动入会那么跳到页面 2 则会弹出一个电脑音频入会的按钮,你需要点击按钮后才能入会(如下图)。

但现在有一个偶现 bug ,派遣机器人参会后一定概率会参会失败,这个问题在线上5次可能失败3次,即使增加了 retry 机制都不行,而这个入会过程如果在本地尝试可以百分百成功,这是最头疼的。

关于 retry 机制:我们默认启动了音频自动入会,假设过几秒还在这个页面,脚本会尝试获取这个按钮的元素,并执行 click 事件,假设尝试多次还不能成功则会完整刷新页面,再走自动入会以及点击按钮的逻辑。

因为后端已经提前排查过了,这次也是希望站在前端视角能不能给一些建议和猜想。

9.2 现有线索与现象

我能掌握的线索包括一份 zoom sdK 源码,可以本地跑起来,但是无法复现问题,降低网速,故意模拟一些极端情况都尝试过,都是徒劳。

两份后台日志,一份成功入会的日志,一份入会失败的日志,日志内容都是通信过程(如下图,都是一些 zoom 的事件派发)。

现在你跟我拥有相同的信息,那么你接下来要怎么切入这个问题?

9.3 猜想与验证过程

a.是否是按钮获取失败了?

一开始我就考虑过这种情况,但是后续发现只要没设置电脑音频自动入会,过几秒按钮会自动弹出,外加上retry机制,所以不可能拿不到,这种情况直接排除(本地复现也从未出现按钮未弹出的情况)。

b.是否因网络波动导致websocket断连?

在前面信息搜集时发现整个入会过程控制台一个接口信息都没有,所以基本确定入会采用了websocket 通信,很自然的就会怀疑是否跟网络波动有关导致的websocket 断连,我在后续故意模拟网络差以及降低性能的情况,以及多次重复入会的操作,结果一次都没成功。

后续有一次我从等候室进入到音频加入的界面,之后挂机跟同事聊一些额外猜想去了,大约过了十五分钟左右回来发现我点击音频入会按钮真进不去,而且控制台出现了 websocket 链接失败的报错,不过这只是一次偶现,大概线上问题有极少部分情况可能真的与网络波动断连有关,但这也不足以解释核心问题。

c.是否是用户信息获取失败导致入会失败?

我尝试在错误日志中搜索了 error 字段,看看有没有什么报错信息,唯一就看到了一个 userRole 获取不到的报错,所以也有了个是否人员信息或许不到导致入会失败的猜想。于是我基于 sdk 源码尝试去搜了下相关的逻辑,遗憾的时候我发现这个字段是个只读字段,尝试跟踪此字段相关的逻辑,全程都是作为数据在被传递,并未有任何加工,所以跟这个可能关系不大。其实到这里,现有线索以及猜测就全部断了。

d.观察websocket通信

之后由于我无法再提出更多猜想,所以接下来就只能从源码入手了,只有再知晓了 zoom 入会的过程,我才好基于过程再提出可能得假象,源码其实都可以先别急着读,我第一想法是先观察 zoom 入会时 websocket 的通信过程到底是什么样的。于是我打开控制台,反复的入会观察通信,初步确认了在入会成功时,客户端会发送一条 audioConnectionStatus 更新的通知,表示状态需要从 1 变更为 2 ,如下图:

这个关键点知道了,源码就好切入了,于是我运行项目直接查找 audioConnectionStatus 也成功定位到了音频入会事件派发源码的地方,那么接下来通过这个节点分别向上以及向下阅读源码,从而理解整个通信过程就非常简单了。

e.skd源码分析以及过程猜想

源码阅读因为比较枯燥,这里直接汇总当时我在源码中的发现以及新的猜想:

在点击 Join Audio by Computer 按钮后,客户端需要先派发一个事件通知到 zoom 服务端(上图),表示我现在要音频入会,所以需要将 audioConnectionStatus 从 1 改为 2。关于 audioConnectionStatus 的状态说明:

  • 0:还没入会
  • 1:人入会了,但是音频还没入会
  • 2:音频也入会了

服务端收到 audioConnectionStatus 变更的通知,确认成功后再推送一个消息到客户端,双方确认后没问题,此时音频正式入会,下图是 websocket 通信截图。

我又对比了后端同学给我的两份日志,确实在成功中发现了跟上面提到的 websocket 通信完全相同的过程;而在失败日志中,我没找到 audioConnectionStatus 由 1 到 2 的通知,所以按钮点不了很正常,因为这部分通信缺失了。

关于缺失我有三个猜想:

  • 猜想一,可能会有少部分 websocket 通信断开的情况,这时候前端和服务端没办法再通信,怎么点按钮都没用(前面提到我复现过1次)。
  • 猜想二,链接其实就没断开,但是点击加入音频时,前端 audioConnectionStatus 的状态是错的,一直在发 1 到服务端(正确预期是发送一个2给后端表示现在要音频入会)。
  • 猜想三,audioConnectionStatus 这个字段只要点击按钮默认就传递 2 过去,不会受其它任何因素影响,但因为 script 脚本错误导致并没有成功走到事件发送这一步(逻辑断了导致状态更新2的事件没派发成功)。

针对于猜想二,我的想法是这个 audioConnectionStatus 是不是只要点了音频加入就一定给个 2 ,还是会受其它影响,然后我去看了下 SDK 源码,确认了这里只要条件符合一定会给 2 ,这个 2 是一个常量,所以它不可能被篡改。

但有趣的是,虽然是常量,但其实它还是依赖不同条件派发不同 audioConnectionStatus 的值,这里我通过 GPT 反编译了上图中被压缩的代码:

(我的prompt为:这是一段 zoom sdk 前端源码,请帮我编译成 react 未压缩的格式,以及补充对应的注释)

const handleAudioConnectionStatusChange = (isConnecting, currentUserOverride = null) => {
  return (dispatch, getState) => {
    const state = getState();
    const showInviteDialog = state.meetingUI.showInviteDialog;
    let currentUser = state.meeting.currentUser;
    const lastSentAudioConnectionStatus = state.audio.lastSentAudioConnectionStatus;

    // If invite dialog is active, hide it
    if (isConnecting && showInviteDialog) {
      dispatch({
        type: SET_SHOW_INVITE_DIALOG,
        showInviteDialog: false
      });
    }

    // If currentUserOverride is provided, use it instead of the current user
    if (currentUserOverride) {
      currentUser = currentUserOverride;
    }

    // Update audio connection status and broadcast to other participants
    if (isConnecting && !isEmpty(currentUser)) {
      // 注意观察这里的派发,前面的图片解释了这几个常量的意思,这里的 CONNECTING 是 1
      dispatch(updateAudioConnectionStatus(lastSentAudioConnectionStatus, AudioConnectionStatus.CONNECTING));
    } else if (!isConnecting && isEmpty(currentUser)) {
      // 这里的 CONNECT_SUCCESS 状态是 2,也就是我们期望的状态
      dispatch(updateAudioConnectionStatus(lastSentAudioConnectionStatus, AudioConnectionStatus.CONNECT_SUCCESS));
    } else if (!isConnecting && !isEmpty(currentUser)) {
      dispatch(updateAudioConnectionStatus(lastSentAudioConnectionStatus, AudioConnectionStatus.CONNECT_FAIL));
    }

    // Finally, dispatch an action indicating that audio connection has changed
    dispatch(setIsAudioConnected(isConnecting));
  }
}

这就验证了猜想 2 和猜想 3 ,虽然是常量,但其实 sdk 会根据不同情况派发 1 和 2 到服务端,现在问题就是,是什么导致了一直点击入会按钮,但是一直发状态1,于是线索又来到了代码中的isConnectingisEmpty(currentUser) ,我觉得这段逻辑很奇怪,为什么只有isConnectingfalse,且isEmpty(currentUser)true才会派发 2(当前用户为空才让入会成功?感觉怪怪的) ,这个就得再投入到源码中去跟踪了。

(大概又读了几个小时源码....)

重新梳理了下 Join Audio by Computer 点击到加入音频成功的整个过程,先说结论,音频入会过程中必须要获取到用户信息,不然不可能音频入会成功。
点击音频入会按钮后客户端大致会有 3 个派发:

  1. UPDATE_CURRENT_USER:通知服务端进行人员信息更新,但这个用户信息也是用户一入会zoom服务端就提前通知给客户端的,并不是客户端自己组装的数据。
  2. WS_AUDIO_VOIP_JOIN_CHANNEL_REQ:更新 audioConnectionStatus 的派发,正常情况是由 1 到 2。
  3. SET_JOIN_AUDIO_DIALOG_VISIBLE:关闭按钮展示的弹窗,整个音频入会基本就算结束了(关闭音频加入会议这个页面的弹窗)。

前面我提到派发 audioConnectionStatus 1 还是 2 跟 isConnectingisEmpty(currentUser) 两个属性有关,这块逻辑我详细读了源码,isConnecting属性也是个常量,一定是 false,只要第一个派发能正常走,那这个就是 false,如下:

所以到这里嫌疑就只能是跟isEmpty(currentUser) 有关,我还纳闷这个用户信息为空才派发 2 ,GPT 其实给我编译错了(还是不能盲目相信 gpt),阅读了isEmpty(currentUser) 这个方法内部的逻辑的源码,才理解其实这个方法预期还是希望用户不能为空,这样这个方法的结果才能返回是true,如下:

function V(e) {
  // 用户不能为空,且入会设备只能是手机或者电脑,且具备入会权限
  return !o().isEmpty(e) && ("phone" === e.audio || "computer" === e.audio && s.AK.getJoinVoIPTimes() > 0)
}
  • !o().isEmpty(e):这里的 e 就是用户信息(我们所谓的机器人入会,这里的机器人身份就是 computer ),前面取反,表示用户信息必须不能为空。

  • “phone” === e.audio || “computer” === e.audio:设备只能是手机或者电脑
  • s.AK.getJoinVoIPTimes() > 0:用于检查当前用户是否具有通过计算机连接音频的权限,正常情况下取值是1,也就是 true。

总结来说,!isConnecting && isEmpty(currentUser)这一段判断,由于isConnecting一直是false所以前半段一定为真,isEmpty(currentUser)这一段内部也是预期用户不能为空,

派发 1 和 2 都会走这个方法,以上三个条件只要有一个为假,那么用户信息更新不会派发,且第二个派发 audioConnectionStatus 更新会一直是1(失败的日志就是一直派发1),但是前面我说了,用户信息一开始是服务端先给过来的,所以现在怀疑用户信息是否一开始就没推送过来之后无论怎么点击按钮都不可能入会成功。

f.错误日志与正确日志的信息对比

在前面的分析中,我怀疑还是 zoom 在一开始未成功推送用户信息到客户端,导致未成功派发状态 2 到服务端,我想到正好手上有两份日志,那就加上之前的理解,来逐步对比两份日志的过程,看看是不是缺少了用户信息的推送。

又过了几个小时,我结合日志又去对了下源码,过程中我分别使用了点击按钮音频入会以及 zoom 音频自动入会,并跟踪了两种入会方式下源码的执行,发现两种入会只有前部分会有少微差异,后面的派发都会走同一套逻辑,而后半段逻辑我们前面已经分析过了,过程中不存在变量的改动以及其它不可控的因素,那问题只可能是出现在自动入会逻辑执行之前,导致执行到自动入会逻辑时,判断没办法让客户端成功派发状态 2 到服务端,自动入会失败了即便有兜底的获取按钮点击入会的逻辑,那你在错误的前提下再怎么点击按钮,结果自然是完全没办法入会了。

于是我现在的注意力集中在了入会状态更新之前,经过多次的自动入会以及按钮点击入会时对于 websocket 的观察,我发现只要准备入会,客户端一定一条如下的信息派发到服务端:

{"evt":8203,"body":{"bOn":true},"seq":13}

接着服务端会推动更新的用户名,用户ID到客户端,客户端这边是会监听到此消息,然后根据不同类型做不同的处理,当是更新用户,则会派发我们前面说的更新音频连接状态的消息到服务端,状态更新成功,音频就入会成功了,这个过程可以看下图,自动入会或者点击按钮入会我测试了很多次,四条发送四条响应,一定遵守这个通信过程

我对比了成功和失败的日志行为,成功日志的通信过程完全吻合。在失败日志中通过“bOn”:true发送的密集程度,还有bOn的初始化过程可以清晰分析出整个入会失败的过程。

自动入会发起第一次请求,遗憾的是 zoom 服务器没响应,之后触自动捕获按钮并点击的行为,可以看到有大量的"bOn":true的发送(注意id 8203),但是服务端还是没有正确的响应(没提供用户更新的信息回来),客户端缺少了用户更新的响应就没法触发更新音频加入会议的派发,这就是为什么最开始说好像这个入会按钮点不动的原因了。

(理解 zoom 入会过程后,你会发现日志完全是可读的,甚至能根据日志信息频率脑补出当时的场景)

错误日志一共有四次 welcome to zoom 的日志信息,说明除了第一次自动入会,还有 3 次浏览器刷新(retry 机制,假设入会失败了就刷新浏览器,再尝试走自动入会以及获取按钮点击入会的逻辑,如果过一段时间还不行就再次刷新浏览器),但一起四次都是上面的情况,不管尝试几次结果服务端一直没响应过。

我尝试在 websocket 通信日志中找到服务端错误的response,结论就是完全没有,没有报错,zoom 服务器直接就是没响应。

那么到这里已经证明其实就是 zoom skd 的问题,某种情况下客户端发送通知到服务端,但 zoom 服务器并未按预期响应,我询问了我们使用的 sdk 版本得知我们并非最新 sdk 版本,我猜测未来可能 skd 以及修复了此问题,所以提出升级 zoom skd 版本验证问题。

为什么最早出问题没第一时间升级 sdk ?因为业务已经运行很久了,冒然升级 sdk 可能造成无法预料的问题,而且服务端部署测试 sdk 还挺麻烦(我忘记具体细节了),我承认我有赌的成分,但矛头已经完全指向 zoom 自身了,很有尝试的必要不是吗?

结论就是,在升级 sdk 后机器人入会失败的问题彻底被解决,经过多次测试对比,旧版本 sdk 依旧会存在5次入会3次失败的可能,而新版 sdk 已经能做到10次入会10次成功了,那么此问题也顺利解决了。

这里我们再汇总下过程:

  • 根据表象提出可能性,网络波动?入会按钮获取不到?入会用户信息获取不到?
  • 观察 websocket 通信过程,确认入会成功会有 audioConnectionStatus 为 2 的派发。
  • 通过 audioConnectionStatus 切入源码,分别往上以及往下阅读源码,梳理大概流程,同时查询 Status 不同数字的含义,以及梳理入会时客户端有哪些核心事件派发。
  • 最终定位到派发 2 的前置条件,怀疑可能入会失败跟 isConnecting 以及 用户为空判断这两个有关。
  • 分别跟踪 isConnecting 与用户判断的逻辑,是否存在中间被修改导致派送错误的可能性?最终确认还是跟用户为空有关系。
  • 对比自动入会与点击按钮入会的逻辑,同时对比两份日志的信息,找差异。
  • 证明用户更新时 zoom 服务端未响应,导致接下来更新入会状态的逻辑未成功执行。
  • 再次结合问题现状(retry 那一块)来阅读失败日志,发现完全对的上,证实是 zoom sdk 自身的问题。
  • 尝试升级 sdk 版本验证问题,运气不错赌对了,顺利解决了。

八、耐心

你也许会想怎么会有这么鸡汤且废话的标题。

问题排查本身是一个提出猜想并论证的过程,很多时候我也会遇到所有猜想都被排除的情况,没有办法只能再去搜集信息给出额外的假定,这个过程就需要各位有足够的耐心,举个最简单的例子,上面 zoom 入会源码分析过程,我其实读了3天,过程中其实我还提出了不少失败的猜想,篇幅问题我就没记录。

而且你可能还会遇到这种问题,还是以 zoom 入会这个为例,我从 audioConnectionStatus 这个变量入手开始排查,结果发现控制派发它的是变量isConnecting以及用户信息为空,变量由一个变成了2个,于是我得分别追踪这两个变量的变化过程;事实上你可能会遭遇变量因素从 1 个变 2 个,2个变4个的情况,但如果你想解决问题,你就必须沉下心来梳理并验证每种可能性,即便是福尔摩斯也会留意每个常人不会察觉的细节不是吗?

而现在大家现在修复的 bug 大多数都是自己亲自写的迭代 bug,假设让你处理一个你完全不了解的领域且代码并非你所写的问题,你真的有耐心解决吗?所以说耐心很重要。