記錄一個前端bug的解決過程

呂大豹發表於2019-02-28

人在江湖飄,哪能不挨刀。

我捱了重重一bug。嚴格來講這可能是我職業生涯以來的首個悲慘經歷,因為憑我的知識儲備和經驗,基本上任何可重現的bug都是可解的。然而這個bug卻困擾了我三個月之久,它具有以下生理特徵:

  1. 後臺日誌能統計到異常,偶發、低頻
  2. 報異常的使用者裝置不具有規律性,什麼手機都有
  3. 我們自己無法復現,任何裝置、任何環境都沒復現
  4. 打電話回訪報異常的使用者,確實存在問題
  5. 客服未接到使用者主動反饋這個異常

此bug並不是js報錯,而是一個業務邏輯的錯誤。表現是,使用者提交的資料莫名缺失。場景是以下這個介面

記錄一個前端bug的解決過程

當使用者填滿所有的空之後,提交按鈕變為可用狀態,資料放進一個陣列中提交上來。後臺有報錯日誌顯示,使用者提交上來的有些是空陣列,有些是陣列中缺了幾項。

問題在於,提交前是有校驗的,使用者不可能提交上來這種未通過校驗的資料。並且還是偶發的啊,如果是邏輯寫錯了,那應該全部會報錯,我們在測試的時候肯定會發現。

最棘手的地方是,我們壓根沒重現過這個情況,找各種同事、各種手機、各種胡亂操作,一次都沒重現出來。這就給除錯帶來很大的麻煩,只能是猜測哪裡可能出問題,然後去驗證。但是根本沒法去驗證啊。。。重現不了,又如何判斷是成功fix了。

看來能驗證的手段就只剩一個:線上日誌。猜問題、上線、看日誌。

這是一個痛苦的過程。介面雖然簡單,這卻是一個龐雜的專案。因為題型眾多,抽離了很多元件,為了公用和靈活擴充套件,元件巢狀深度有五層之多。其架構複雜程度在我的職業生涯中也能排TOP3.

拿題乾的渲染來說,就有:公式圖片轉LaTeX、mathjax渲染公式、渲染公式上的空、給空編號、模擬游標、自動focus空、動態計算字型大小等諸多流程。而且下方那個鍵盤還是我們H5模擬的,並不是系統鍵盤。更別提還有校驗邏輯、判分邏輯。

前n次嘗試

看距離上一版有哪些改動,抹去有嫌疑的改動,看日誌是否正常。尷尬的是,這是一次重大重構,改動的地方還特別多。於是一場盲人摸象式的遠端debug行動開始了。

一次又一次的上線、觀察日誌、下線。不斷排除了一些相關的功能,始終未能診斷到問題所在。甚至連我很確信的地方都嘗試了,還是找不到問題。前前後後嘗試了二十多次吧,改到我都懷疑人生了。領導看了這些上線記錄都怒了,說你這上上下下的搞雞毛呢。我也很崩潰啊。

看來用這個盲人摸象手段是搞不定了,我意識到了情況的嚴重性,暗暗感覺這可能不是輕易能解決的,呂某一定使出畢生所學,為民除害。

第n+1次嘗試

既然有那麼多的使用者日誌,我們自己為何重現不了?這是我一直糾結的。於是再次進行瘋狂測試。

皇天不負有心人,我竟然真的給重現出來了!操作是這樣的:填好空,兩個手指同時按下提交按鈕和刪除按鈕。這樣的話既通過了校驗,又能在提交之前把資料給刪了。

發現這個騷操作的時候我是很興奮的,但是會有那麼多使用者這麼操作嗎?顯然不太可能。此時我又想到,提交按鈕和刪除按鈕是挨著的,會不會是使用者按提交的時候誤觸了刪除鍵。這還算比較合理,畢竟使用者是小學生嘛,操作不一定那麼精準的。

我興奮不已的進行驗證。在刪除鍵和提交鍵之間加了“下一空”按鈕(通過配置),這樣使用者保證不會誤觸了。

上線,日誌依舊。我摔啊,看來並不是誤觸的事。

第n+2次嘗試

隨著bug拖的時間越來越長,我的心態也有點焦躁。但思路還是聚焦在刪除按鈕上,畢竟這是好不容易發現能重現的。

如何能夠既點提交又點刪除呢?這時候我想到了點選穿透(鍵盤為了響應快,使用了touchstart事件)。因為在點完提交的時候,模擬鍵盤會收起來,而收起的過程中刪除按鈕會經過提交按鈕的位置。根據點選穿透的原理,如果此時派發的click事件作用到了刪除按鈕上,那豈不是就算點到了?

我都有點佩服我的想象力了,黔驢技窮了啊,試吧。避免點選穿透有兩種方式,阻止click事件的預設動作,或者是讓元素收起的時間延遲。我選了後者。

上線,日誌依舊。我吐血。後來一想,刪除按鈕根本都沒監聽click事件啊,哪來的穿透。真是病急亂投醫了。

第n+3次嘗試

掃程式碼,發現一個很重的疑點。答案是個陣列,是引用型別。由於複雜的元件關係,這個引用型別的資料可以被多個元件訪問到。

使用可變資料的時候有個隱患,它可能在你不知道的地方被修改。程式碼是vue寫的,有些元件中含有watch,搞不好是意外進了哪裡的watch,在點完提交的時候也會把資料給更改了。

這個猜測我覺得是合理的,在開發階段我就曾因為未使用immutable資料而隱隱擔心過。好了,快速驗證吧。在點完提交按鈕的時候,我把答案資料給克隆了一份,然後再進行判分和提交的操作。這下就不擔心已經拿到的資料被篡改了。

上線,日誌依舊。繼續吐血。

不過這次也縮小了嫌疑範圍,看來資料不是在點完提交的時候被篡改了,而是提交上來的就有問題。匪夷所思的是,使用者是如何繞過校驗把資料提交上來的呢?難不成是我的校驗函式有問題,這個地方把資料給改了?掃了一遍程式碼,無果。

第n+4次嘗試

此時聚焦到了使用者在填寫答案的時候發生了什麼。我像偵探一樣用放大鏡一遍遍看程式碼,然而好多天的追蹤,並沒有找到什麼有用線索。

直到有一天,那天陽光明媚天空飄著朵朵白雲,感覺有什麼好事要發生。QA在反饋群裡發了一張截圖,說公式解析的那個點點點一直不消失(正在解析的狀態),而且空裡也輸不進內容去。如下圖:

記錄一個前端bug的解決過程

我敏感的神經頓時嗅到了一絲線索。題幹使用了mathjax來解析公式,而mathjax在解析的過程中會按需載入一些字型檔案,而且還會掃描頁面節點,並生成大量的DOM節點。這對瀏覽器來說是個壓力不小的事情,更何況是移動端。

我馬上再掃描公式處理的程式碼,由於有些空會在公式上出現,所以程式碼是在等公式渲染完後統一給空編序號,然後進行自動focus,而且自動focus的時候還會首先給答案賦值。天吶,問題該不會出現在這裡吧!公式的渲染過程可能有延時,使用者可能在這個時間進行點什麼操作!

首先這符合偶發這個事實,因為公式解析中出現抖動網路延遲什麼的也是偶然現象。再者公司的網路快,使用者的網路可能慢,這也符合我們一直未重現的事實。感覺這次很靠譜了!很多偵探電視都是這麼演的啊,主人公通過別人無意的一句話聯想到了線索,然後案件破解,真相大白!對對對,就是這個感覺!

趕快在程式碼層面做優化,儘可能早地處理沒有公式的空,有公式的地方也確保執行完後使用者才能輸入。

優化完畢,迴歸測試,萬事俱備,只等線上驗證,一錘定音!

結果……日誌還有啊!啊噗!,電視裡都是騙人的啊!

等等!日誌雖然還有,但好像少了耶!難道這次的優化是有作用的?雖然從理論上能解釋一些作用,但還存在的日誌又表示什麼呢?難道造成丟答案的原因不止一個?

第n+5次嘗試

時間一天天過去,我還是沒找到什麼有力線索。中陸續有一些猜測,打了一些日誌點後還是無果。看著QA同事緊縮的眉頭,領導關切的詢問,我也越發焦慮了起來。因為我這是一個公共元件庫,有其他專案在等著使用,如果我的bug解決不了,將影響其他專案的進度。

又是陽光明媚的一天,天空飄著朵朵白雲。我無意跟另一位後端同事聊到了這個話題,他隨口一說:應該是超時自動提交的吧。

什麼?什麼!自動提交?!我突然像被閃電擊中。因為我寫的這是個公共元件,同時也對外暴漏了一些API,比如提交答案就是其中一個。我提供的是答題介面的元件,但是別人專案中有倒數計時的場景,超時後會呼叫我的提交API,把使用者答案提交上去。

如果超時的時候,使用者什麼也沒填,那豈不是把空答案給提交上去了!!!根本沒有校驗函式什麼事,是別人調API提交的啊。

我哭了。難怪沒使用者反饋呢,時間到了自動提交空答案,他們沒理由反饋啊。難怪我們自己沒重現呢,一直沉浸在怎麼亂點出來。就算QA同學看到了超時提交的時候,也無法意識此時是空答案。

沒錯,真相就是它了,修改相關邏輯後上線,果然報錯日誌沒了。困擾我三個月的bug終於解決了!我閉上眼睛,心裡默默放了一把鞭炮。

總結

前前後後三個月時間,總算是找到了問題所在。其實第n+4次是解決了一些問題的,最後一次是徹底解決,我用實際行動證明了,真相不止一個。而這件沸沸揚揚的丟答案bug事件,也給了我很多啟示。

  1. 做重構的時候要格外小心邏輯更改,重構後一定要跑通所有case。

  2. 排查問題的方式,這期間我使用了各種對照試驗,各種原始碼級別的排查

  3. 使用vue做複雜專案的時候要格外注意元件的巢狀層數,少寫watch,避免程式執行順序的混亂

  4. 設計對外API時,要考慮健壯性,不光考慮傳入引數的不穩定,還要考慮當前上下文的不穩定。

相關文章