查看原文
其他

二分 debug 法 yyds

Jim Bytebase 2022-12-19


开篇


周三的时候,我和 Danny 几乎同时发现,我们 Bytebase 的网页标题,也就是显示在浏览器 tab 栏上的那行字,它错了!


准确的说,是它不会随着页面的切换而更新了。比如进来的时候是 Sign In,那么即使登录以后,也还是 Sign In;这时候刷新了一下,因为已经登录过了,它进来就是 Bytebase,那么无论怎么切换页面,都还是 Bytebase。


这么严重的问题一定是牵一发而动全身的,也就是说它是某一个确定的地方引发的 bug,而不是因为不符合预期的输入和数据引发的边界问题。考虑到网页标题和 document.title 之间存在明确的关系,我一开始认为它将会很容易排查和修复,然而事情却并没有那么简单。



第一招 - 全局搜索


在 frontend 目录里全局搜索 document.title,找到两个点对 document.title 进行了赋值。一个是在路由的全局 afterEach 钩子里,有一个根据 route meta 动态获取和修改 document.title 的逻辑。哼哼,那么肯定是这里出问题了,然而打开文件一看


太真实了,一年没动过,是啊,这里是一个几乎是框架级别的逻辑,又怎么可能轻易会去改到它呢。(BTW 这个插件叫 GitLens,可以看每一行的 git 历史)


基于简单的排除法,那么就是另一个了,在 useLanguage 里,切换语言的时候,会重新从 route meta 里获取一次 title,然后赋值给 document.title。然而,令人惊讶的是,在切换语言的时候,页面标题却是会正确地更新的。


基于简单的排除法,那么,还得是回去看第一个。于是我又到全局 afterEach 的逻辑里面打了一些日志以及断点,试图在它不符合预期的时候,看看是不是因为第一个分支除了问题导致 fallback 到下面的 else 分支。但却并没有任何的收获,不论是日志还是断点,to.meta.title(to) 的值都非常对,而这行代码也仅仅涉及一个 function call 和一个赋值,简单得让人完全无法相信它会出 bug。



第二招 - 移花接木


既然排除了我们自己的代码的问题,那么问题应该就出在了第三方的代码身上。这时候问题来了,node_module 里有海量的代码,不好找也不好调,要怎么才能找到元凶呢?


我们可以通过 Object.defineProperty,将 document.title 重写,只要对 document.title 赋值就会踩到我们的 setter 陷阱。像这样


然后从控制台里,我们可以看到


这……这看起来也太对了,随着点击导航栏,我们打出来的 log 也是对的。但是浏览器的标题还是不会随着它一起跟新。



第三招 - 斗转星移


这时候 E0 在群里一句话提醒了我,他说「还可以 document.querySelector('title').textContent 来改」。


这时候需要用到一个 Chrome DevTools 里非常强大,但是用的人却不多的功能——DOM Breakpoint


对 <title> 节点开启 subtree modifications break point,满怀期待,点一下导航栏,自然是中了断点


中了我们自己的代码,没问题,这里 to.meta.title(to) 执行出来的是「Issue」,符合预期。心中一凉,按下 F8 跳过,又中了


这次断在了一个奇怪的文件,格式化一下,再看看 call stack,也没有什么地方自报家门表名身份的,文件名 tabTitleInstaller 也看不出是什么。但是,根据我模糊的经验,前面显示 VM**** 这是一个 Chrome 扩展里的 JS。



第四招 - 日取其半


开启 Chrome 的隐身模式,把所有扩展在隐身模式都屏蔽掉


果然问题就不复现了。这时候就可以用二分 debug 大法了,开一半关一半,<del>这样就可以在O(logn)时间内快速定位</del>,很快就定位到了问题来自于 o/slash。


这时候已经可以大概明白了,o/slash 会对收藏的页面标题进行处理,比如打开 https://demo.bytebase.com,窗口的标题就会变成


大概扫了一眼它的代码(混淆过的也看不出个所以然),它有一个叫 clean-up-tab-title 的逻辑,我猜测是会对原来的窗口标题进行备份,然后在浏览器历史发生变化的时候将它还原。可能因为这里逻辑太过粗放,或者是和我们的 afterEach 逻辑先后顺序有点误差,导致我们所修改的 document.title 又被它改回去了。仔细一点观察会发现有些时候我们的窗口标题会一闪而过,先变成预期的然后又变回去,点 Environments 的时候非常明显。


既然定位了,那么查代码的事情暂时就告一段落了。在 Danny 的建议之下我写了一篇文档,给出了一个固定的 reproduce route 和相关的录屏。



结尾 - 打完收工


回想一下,为什么用 Object.defineProperty 制作的赋值陷阱并没有被 o/slash 踩中呢?这和 Chrome 扩展的执行机制息息相关。


Chrome 的扩展里提供了两种 JS,一种叫 background script,一种叫 content script,只有 content script 是可以访问 DOM 的。这样很容易让人以为它和网页里的 JS 一样是运行在 UI 线程上的,其实不然,在 Chrome Developers 的相关文档里解释到了它 Work in isolated worlds(https://developer.chrome.com/docs/extensions/mv3/content_scripts/)


结合 devtools 里显示的 VM****,我猜想它应该是运行在一个独立的 VM 里面。而 DOM 是 VM 封装过的,所以虽然它们都可以透过 document.title 来读写页面的标题,但是我们包装过的 document 和 content script 里访问的 document 却并不是同一个对象。这也就解释了我们的陷阱无法捕获扩展代码的现象。


干货整理|SQL 审核最佳实践活动回顾

BBer | 来自名校的他们不再做学霸,却选择做创造者

迟来的作业 —— HashiConf Europe 2022 我的走马观花

Bytebase 加入阿里云 PolarDB 开源数据库社区


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存