深藏若虛

如何敘述異常回報

How to describe a bug report

本文是今年在 IThome 鐵人賽所寫的文章,好像也是唯一有完整釋出的文章(艸),用心寫完這篇就好既無力了 XD。在這邊稍微把這篇原本分四天的文章進行整併、校閱,讓有興趣閱讀的人可以讀得比較舒適。

前言

今天來講講如何敘述一個議題(issue),通常議題有分兩種,一種是功能請求(feature request),另一種是異常回報(bug report)12。由於功能請求中關於需求的描述會涉及不少知識與經驗,使用者和工程師的相關描述可能又大不相同,這方面的知識很難一次論述完整;而異常回報的描述則較為通泛,主要是講述在回報異常時,應該要附上哪些資訊會對開發者更有幫助,不太區分角色,較為簡單。所以本篇先著重在怎樣描述異常。

情境

「為什麼我們要學會如何描述一個問題(異常)呢?問題描述有什麼難的?不就是把遇到的問題講出來就好了嗎?難道我講的還不夠清楚嗎?」我想這是多數使用者甚至少數開發者聽到這個主題會有的疑惑,事實上這是很正常的反應,對於多數異常回報者來說,他們已經盡力地把遇到的異常行為描述出來了,他們眼中的異常就是這樣,對於協助我們指出異常的使用者,我們很難用比較強硬的態度說:「這些資訊太少了,我不接受」。到最後我們也只能抱著疑惑的心情嘗試找出使用者遇到的異常,但在資訊不足的情況下,這些異常通常都石沈大海。尷尬的是,你若把異常議題關掉、結案,使用者可能還會抗議,於是這些議題就成為議題追蹤工具(issue tracker)的萬年大石頭,卡在那邊不上不下。

崩潰的是,在議題追蹤工具裡不只前述的項目卡在那裡,甚至有更多的陳年議題是前輩們留下來的。裡面只簡短地用兩三句話(甚至更短、或只有標題)描述遇到什麼問題,再沒有其他資訊。我們看不懂這個問題到底在說什麼,我們也不知道這個問題要怎麼重現,我們更不知道這個問題是哪個版本的事,是不是在現在的版本已經不會出現了?抱著疑惑、抱著頭,我們・真的・非常・苦惱。 _(:3」∠)_

目的

在回報異常時提供充足的資訊,能夠讓開發者更快解決。

是的,當我們在回報異常時提供充足的資訊,會讓開發者更能精準判斷這個「症狀」的「病因」是什麼,才能更精準的解決軟體的病灶3,從而快速解決問題,既省下開發者的時間成本,使用者也能更快的享受異常排除的成果。

所以學會如何描述異常是任何與這份程式有關聯的人都應該要知道的。使用者要知道、客服要知道、專案經理要知道、工程師更要知道。如此一來,使用者或客戶能夠明確將情況描述給開發團隊,讓這異常迅速排除,繼續愉快的使用產品。若是使用者沒有這份知識時,客服就要幫忙過濾,在與使用者的溝通中引導他們把這些資訊回答出來,避免讓不必要的雜訊干擾開發團隊的進度。專案經理更要嚴格把關將送進開發團隊的異常回報,要求客服把缺漏的資訊補上,讓開發團隊可以將時間專注在解決異常而不是重新尋找如何重現它。工程師則更應該要謹記,現在的詳細記錄異常資訊,可以讓我們都不再多背一袋技術債,絕對不要嫌麻煩而省略,不然未來將會耗費更多時間重新釐清這個異常。

作法

那在回報異常前,我們應注意哪些事項呢?那就是查看當前的議題清單裡,有沒有和我們所遇到的異常相同的情況,避免重複(duplicate)回報。同樣的異常應該集中在同個議題討論,而不是散落在清單各處,這樣只會增加開發團隊的排除成本。當我們發現已經有既定異常議題存在時,可以先嘗試了解該討論串的內容,若仍然無法排除,則可以將原本要回報的內容,於該議題討論串中回覆,增加樣本數。

那麼有哪些資訊是在回報異常時需要附上的呢?這邊先以清單的方式表述:

  1. 標題(Title)
  2. 版本(Version number)
  3. 問題簡述(Describe the problem)
  4. 重現步驟(Steps to reproduce)
  5. 預期行為( What I expected)
  6. 實際行為(What happened instead)
  7. 影像(Screenshot / Video)
  8. 環境(Environments)
  9. 脈絡或程式碼(Context / Source)
  10. 備註(Additional details)

標題(Title)

無論是什麼類型的文章,有個好的標題就是一個好的開始,議題的標題更是如此。如何用最少的字把異常的輪廓描述出來,就是這個資訊需要探討的。

在探討什麼是好的標題前,我們先說說什麼叫做不好的標題。讓人最討厭的標題莫過於內容農場式的標題,也就是講得很誇大,但卻什麼重點都沒提到,像是「這裡有一個很嚴重的問題造成程式崩潰了!」或是「緊急!這個異常讓客戶抓狂了!」等等。

那到底標題要如何說清楚呢?把握一個原則就是至少詳細到不容易和其他類似異常搞混,以現實生活的情境來舉例,假設今天台中某處有個路燈壞了,你想舉報請市府趕緊請人維修:

  • 南區有個路燈壞了(X,太籠統,這樣同名不同處的異常可能會太多)
  • 南區台中路上靠近二二八公園處有路燈壞了(O,把異常發生的地點指名)
  • 南區台中路上靠近二二八公園處有路燈不斷閃爍(O,把故障狀況請得更清楚)
  • 南區編號 641549 的路燈不會亮(O,善用產品本身提供的除錯輔助資訊)

另外多用客觀的描述取代主觀看法也會讓標題更好識別:

  • 網站的選單很難用,一直沒反應(X)
  • 網站的選單按鈕太小了,觸控容易失敗,或是誤觸其他按鈕(O)
  • 網頁會跳出奇怪的畫面,不知道發生什麼事(X)
  • 點擊編輯文章的按鈕,會跳出「We’re sorry, but something rent wrong」的畫面(O)

版本(Version number)

產品發生異常時的版本號非常重要,它可以開發者確認這個異常是在產品開發的什麼時間點發生的,是否已經在比較新的版本被修復了?或是該行為在以前沒問題,直到某個版本後才發生異常,開發者能透過版本號鎖定變動的位置,了解前後的脈絡,判斷是不是有一個變動提交(commit)沒有注意細節而引發了異常。

若說標題和描述是讓開發者了解空間與事件,那麼版本就是讓開發者知曉時間。在兩者資訊充足的情況下,雙方對談就容易聚焦,而不是在跟平行世界的對方雞同鴨講。

所以程式在釋出時應該要有一套版本號訂定風格,讓使用者在回報異常時,開發者能迅速聚焦在準確的範圍去除錯。若是程式是沒有明確版本號,提供程式當前版本控制紀錄中的資訊(通常是最後一次提交的雜湊值(Hash))也是個方式。

問題描述(Describe the problem)

如果好的標題是一個好的開始,那麼問題簡述就是把這個好的開始延續下去。標題通常必須一句話簡述整個異常的輪廓,那問題描述就是把異常的細節描繪出來。標題中省略的細節、脈絡,或是你對這個問題的觀察和看法,都可以在這裡好好詳述。若同樣以路燈為例,那大概就是:

南區台中路上靠近二二八公園處有路燈壞了,它時不時在閃爍。這附近只有這個路燈有異常現象,其他路燈都是好的,該路燈的編號是 641549 。看它的桿身似乎有被撞擊過的痕跡,或許是最近被車輛擦撞過導致線路異常?我沒有很確定。詢問過附近的居民,似乎在前天還是好的。

在這個例子,可以看到除了標題所提到的資訊外,還多了許多來自於回報者的觀察,讓我們可以有更多資訊的得以除錯。

  • 異常描述:閃爍
  • 同型比較:和其他路燈相比
  • 產品資訊:路燈編號
  • 額外表徵:被撞擊過的痕跡
  • 個人推斷:或許是被擦撞過導致線路異常
  • 其他資訊:詢問居民

用路燈來描述可能還是有點籠統,很難和實際程式做聯想結合,這邊改以登入發生錯誤的情境作為舉例:

用帳號密碼登入後,會跳到顯示 We’re sorry, but something went wrong 的畫面

輸入帳號密碼登入後沒有跳到正常頁面,而是顯示錯誤訊息的頁面(異常描述),出現錯誤訊息的網址是 https://xxx.xxx.xxx/ooo?a=aaa&b=bbb(產品資訊)。其他頁面似乎都正常,透過社交平台登入的功能也正常(同型比較)。不管輸入的帳號密碼是否正確都會報錯。我不知道發生什麼事,會不會是帳號密碼驗證的程序出錯?因為社交平台登入是正常的(個人推斷)。問過朋友,他們使用帳號密碼登入也有同樣的異常。有朋友說早上七點時還是正常的(其他資訊)。

當然,這些特徵與資訊項目都只是舉例,每個專案或情境可以提供的資訊類型都不相同,但希望這樣舉例能讓讀者們稍微了解「問題描述」與「標題」的差異,以及有什麼細節是可以提供的。

重現步驟(Steps to reproduce)

重現步驟就是指經過哪些動作後,就一定會引發這個異常。這是讓開發者代入異常發生情境很重要的資訊。如果沒有提供重現步驟,開發者就要耗費更多時間去分析到底發生了什麼事,然後嘗試要如何重現,能將異常重現後才能開始進行除錯。如果直接提供重現步驟,那開發者就能直接面對異常,然後依照他的經驗快速除錯。

有重現步驟的異常回報,也容易使回報者和開發團隊站在同一個視角,而不是使用者一直在幹譙,然後開發團隊只能不斷道歉,卻還是找不到異常發生的原因,或是直接丟給使用者「這在我電腦是正常的!(It works on my computer!)」交差了事。當開發團隊非常努力想透過回報者的描述去重現異常,卻一直失敗時,面對回報者的催促,有可能會產生負面情緒,認為回報者是來鬧的,畢竟在兩者的觀點上,對方描述的事情都是沒發生過的呀!

若是該異常無法百分之百重現,也要在這裡特別講明。儘管沒有一個肯定能重現的步驟,但仍可以試著把可能會發生異常的步驟寫出來,讓開發者了解到底再發生這個異常前,會經歷過什麼事,導致有機率觸發異常。回報者也需要有著「在沒有有永遠能重現異常的步驟時,這個議題可能會需要耗費較長時間除錯」的心理準備。

好的重現步驟在於一個指令一個步驟,先以上節提到的登入異常為例,大致如下:

  1. 進入網站(https://www.xxxx.tw)。
  2. 點擊首頁右上方的「登入」按鈕。
  3. 再登入表單中輸入帳號密碼。
  4. 點擊表單中的「送出」按鈕。
  5. 發生異常。

雖然這個例子這個重現步驟看起來似乎沒提供到什麼資訊,但某種層面上卻也讓開發者知道使用者是用合理的方式去進行登入,可以排除是非預期的行為模式導致異常發生。透過這樣一個指令就寫一個步驟的,讓開發者了解實際的行為模式,也讓開發者依照這樣的步驟執行時,能夠代入使用者的情境。若是開發者在執行步驟時是正常的,就可能會往是不是環境或版本的不同造成結果差異的方向去想,而不是不斷在程式碼中鑽牛角尖。

若回報者在使用產品時,有了一些非預期行為是開發者當初沒有想到的,就可以讓開發者不被既有的觀點束縛,突破盲點而瞭解錯誤的原因。畢竟我們都知道,身為開發者去執行一些情境,都一定會預設我們心中已經非常熟悉的合法步驟,而不會意識到有其他非預期行為的可能。

另外,若是能夠在每個步驟都附上畫面截圖,也會減少對步驟執行的誤解,並透過畫面提供了回報者可能沒意識到的額外資訊。

預期行為( What I expected)

預期行為就是指當我們透過「重現步驟」操作時,我們預想中理應該出現的行為、結果、狀態。

預期行為在於讓開發團隊暸解我們心中認為正常的認知是否與當初產品設計時相同,讓兩方在這方面的資訊是對稱的。在產品設計時,難免會有產品的行為與使用者的預期行為不同的情況,使用者會因為這種情況認定是程式發生異常,進而回報。但事實上,程式並沒有問題,只是當初產品設計時在使用者經驗(UX)的決策不夠理想,或是設計概念比較新穎,使用者和市場並還沒被教育過。所以若能知道使用者預期行為,就能避免把時間成本耗費在處理其實不是異常的回報中,而能讓客服先將這部分的回報篩選掉,讓開發團隊得以專注在真正的異常回報中。

實際行為(What happened instead)

實際行為就是指當我們透過「重現步驟」操作時,實際發生行為的描述,通常也就是對異常的描述。

與預期行為同理,這部分的描述能讓開發團隊暸解我們實際遇到的情況,並透過是否與產品設計相符或相異去進一步判斷發生什麼事。排除設計和使用這預期行為的非異常回報,這部分的資訊就是讓開發團隊搭配重現步驟去分析重要描述。與「問題描述」相似,但如同「問題描述」是「標題」的進一步說明,那「實際行為」就是「問題描述」中,關於異常更詳細的描述。

影像(Screenshot / Video)

影像的部分是比較選擇性提供的資訊,包括螢幕截圖和操作的實際錄影。有時候這些資訊會直接在其他章節中提供,像是在重現步驟中,每步驟提供一個螢幕截圖、或是在實際行為中,把看到的情況截圖附上。

影像是更真實的記錄,讓開發團隊若是對我們所描述的文字仍有點困惑時,能透過影像去暸解我們實際想表達的。或是如重現步驟中所說,透過畫面可以補足我們沒有意識到是有用處的資訊,但對開發者團隊來說是關鍵的細節。

環境(Environments)

環境的部分是指程式運作的環境,舉凡作業系統、瀏覽器、相依函示庫等等,各家產品和版本都可能會影響程式的運作。這部分亦是很著要的資訊,其重要性不亞於版本、重現步驟等描述。

環境資訊讓開發團隊在進行判斷時,可以先暸解異常會不會有可能只是環境造成的落差,而不是程式核心邏輯的錯誤,如此才能朝正確的方向進行異常排除。程式在開發時,很難保證在所有環境都能行為一致,尤其是網頁程式,在常見的四大瀏覽器(Chrome, Firefox, Safari, IE)的顯示可能都有細節的差異,甚至在同一種瀏覽器,在不同版本都會有不同。像是 IE 系列,在 6 ~ 11 的差異都滿大的。

有時候程式可能會被執行的環境太多種,開發團隊沒有太多成本一一測試與驗證,或是為了支援太舊環境的成本太高,所以決定不再適用。但是就算開發團隊有在產品某處或者安裝程式提及,使用者不一定會知道這些訊息(忽略說明訊息),導致誤以為程式出了異常,但事實上只是該環境已經不被支援,或是不在驗證過的支援清單中。若有此資訊,就可以在客服階段就先為使用者解惑,減少處理時間與成本。

若是該環境是在程式認定的支援清單中,開發團隊也能搭配重現步驟快速了解狀況,並透過交叉比對去判斷是核心邏輯的錯誤,還是環境支援沒有完善。

以下提供比較常見的環境資訊:

  • 作業系統:Windows, Mac OS X, Android, iOS, Win10 Mobile
  • 瀏覽器:Chrome, Firefox, Safari, IE, MS Edge, Opera 15+, Android Browser

在提供這類資訊,除了環境名稱與主要版本號外,建議透過說明或者關於的資訊,取得比較詳細的版本號一併附上,會對開發團隊更有幫助。

脈絡或程式碼(Context / Source)

這部分又稱最小可重現模型(Minimal reproduction),比較偏向當產品是開發用的函式庫、框架時,會需要使用這個產品的開發者提供,而不是一般使用者。

當我們使用某個函式庫、套件或框架時,若遇到異常,除了上述的資訊外,若能提供程式碼給產品的開發團隊去暸解,一定會更有幫助。但是我們也不能把我們自己產品的程式碼完全給開發團隊,一來可能是會有商業考量,這些程式碼是公司資產,不得任意外洩、二來是開發團隊也沒有時間和義務幫看你的產品的原始碼。這時候就需要針對發生異常的部分,實作最小可實現模型,不包含其他核心邏輯,純粹只有使用他們產品時的程式碼,越簡單且能重現異常越好。

而在實作小可實現模型時,也算是再次確認。我們自己在開發產品時,會由各種邏輯混雜在一起,若是架構沒設計好,很有可能就會互相影響。透過實作最小可重現模型,讓我們能夠排除是其他程式碼間接造成異常的可能性,而能聚焦該產品發生異常的原因與位置。

備註(Additional details)

只要是不屬於本章中所提到的任何項目,但認定可能會對開發團隊排除異常有幫助的資訊都可以寫在這部分。

像是這個異常沒辦法百分之百重現,甚至連重現步驟都難以確立,就可以在這邊加註。或是這個功能在以前都沒有發生過異常,是在某個版本後才故障。或是我們認為以我們的專業知識,能夠協助開發團隊排除異常,也可以在這邊寫下我們的看法。諸如此類。

困境

在前一章我們知道了哪些資訊應該在回報問題時附上,得以讓開發團隊善用該資訊儘速給予答覆或是除錯。但現實總是殘酷的,不見得所有的使用者都會知道這些要附上這些資訊,就算知道也不一定會勤於每次回報問題時都附上,講句難聽的,商業軟體有問題是開發團隊該負責的,使用者願意回報就已經是願意幫忙的,如果還要求每次都要制式風格回覆,不見得使用者都願意配合。(開源生態另當別論,開源生態是作者願意開發程式碼並開源提供給公眾使用已經仁至義盡了,並沒有義務要不斷維護。)

要讓使用者願意回報完整的資訊給開發團隊,比較直接的作法就是在填寫回報的表單裡提供完整的資訊,讓使用者能夠了解有什麼資訊是開發團隊需要的、該怎麼取得那些資訊,而避免直接丟一個空白的文字框要求使用者自己填寫,填寫後又以提供資訊不足拒絕受理。若是透過 Github、Gitlab 管理問題回報,可以透過 Issue Template 的功能去自訂問題回報時,文字框預設會出現哪些資訊,這部分將在下一章提及。另外也可以在如前面陸續有在提及的,請客服和使用者溝通,協助使用者提供該資訊,透過此方法,就是要對客服做相關知識的教育訓練。

另外,若是在自家團隊或是開源專案,頻繁遇到內部人員在回報問題時,不遵守培訓或是表單提供的格式回報問題,不應該遷就接受,而是制定一個固定的時間(例如一到三天),在這個時間沒有改善,就直接關閉、封存該回報,不予受理。在這類議題,比較常遇到的就是業務或是客服無心遵守,認為自己的描述已經夠清楚了,而不願意再多耗費時間協助提供完整的資訊。或是工程師認為自己只是先輸入個關鍵描述,之後「自己」會再處理,不用那麼繁瑣再輸入。但通常結果都是開發團隊並不了解那些不完整的資訊是在填寫什麼問題,或是最後那個異常的處理者不是回報的工程師了,導致這個回報到最後並沒有提供它該有的幫助,反而成為異常清單中想移除卻又不敢移除的臭石頭。所以,打從最初就規定沒有符合規格的異常回報不受理,對於內部團隊是一個必須的措施。別把回報時的偷懶,連本帶利的變成開發團隊要代償的債務。

輔助

在上一章有提到 Issue Template 可以協助我們自訂回報異常時的格式,本章將介紹在 GitHub、GitLab 這兩大開發者常用的程式碼託管與專案管理平台來如何善用此功能。

GitHub Issue Template

GitHub 文件中的 Helping people contribute to your project 有提到許多關於如何協助其他人貢獻自己專案的一些方法,其中 Creating an issue template for your repository 就有提到如何自訂 Issue Template。本節稍微摘自裡面重要的流程在此說明。

GitHub 的 Issue Template 是透過在預設分支(defaut branch)中,建立一個名為 issue_template.md (大小寫皆可)的檔案來設置,並可以選擇放置在可見的專案根目錄下、docs 目錄底下、或是隱藏的 .github 目錄底下。除了 md 以外,亦可以以 txt 為副檔名。

GitLab Issue Template

GitLab 文件中的 Description templates 有提到如何自訂 issue 和 merge request 描述的樣板。本節稍微摘自裡面重要的流程在此說明。

和 GitHub 相同,GitLab 也是透過在預設分支下建立檔案來設置 Issue Template。不同的是,GitLab 強制要求要以 Markdown 語法編寫,且必須放置在 .gitlab/issue_templates/ 目錄底下,檔案名稱即為樣板名稱,一個專案可以同時存在多種樣板,例如異常回報可以取名為 Bug.md,我們也可以針對異常的種類再去做更詳細的分1類。目前 GitLab Free 方案或是 CE 版,並沒有提供設置預設樣板的功能。

使用要點

在開頭前先附上說明文字

在樣板的開頭,就把回報異常應注意哪些事項的說明寫上,讓回報者一定會看到這些描述也是個不錯的方式。TensorFlow 的 Issue Template 就在開頭把說明講得很清楚。

透過 HTML 註解語法對回報者說明

由於 Markdown 是有支援 HTML 語法的,所以理當可以使用 HTML 的註解語法。我們就可以善用這個特性,將想要對回報者說但不想在回報後顯示在頁面的話,透過註解語法包覆起來。像是:

<!--
在最上面寫上回報時應該要注意那些事項
....
-->

## 項目一
<!--- 描述這個項目的意思,或是該如何取的這個項目的資訊 -->

Webpack 的 Issue Template 就有善用此特性。

將所希望的回報格式先填好

如果我們希望使用者在填寫重現步驟時,能使用有序條列的方式表達,那我們就可以在該節先將有序清單的 Markdown 語法寫上;如果我們希望使用者在貼上程式碼或是程式輸出的訊息時,能用程式碼區塊包覆起來,我們就可以將相關語法也先寫上。諸如此類,舉例來說就像這樣:

**這個異常的重現步驟**
1.
2.
3.

**當你輸入 `echo $PATH` 後所輸出的訊息**

~~~txt
(paste your output here)
~~~

這部分可以參考 Moby 的 Issue Template

結語

Issue Description 的部分講到這邊算是告一段落。希望讀者們能透過此主題的文章暸解回報異常時,需要回報哪些資訊,以及其背後的原因。並且能夠將相關知識再轉達給你身邊的工程師、客服、與使用者,讓彼此在溝通上能夠更加順暢,留下更多有效益的回報清單,而不再是一堆想刪又不敢刪議題的尷尬局面。

參考資料


  1. 除了此兩種外,還有一種類型容易被當作議題,但事實上是不建議提交在議題追蹤工具,那就是使用上的疑惑(usage question),這類型的議題通常建議先讀相關文件、使用者手冊、觀看影片教學、在論壇、社群、通訊平台上發問。如果是商業產品,則應該由客服處理,而不是丟給研發工程師。 
  2. bug 這個詞是比較口語的用法,通常會泛指 defect、error、fault 等,程式上看起來出錯或有異常的現象、行為。在翻譯上比較口語的情境我會使用「問題」作為對應,而在比較書面的情境我會使用「異常」作為代表。畢竟「問題」這個詞太過廣義,有可能是指 bug,也有可能是指 question,而「異常」則狹義的多(在中國社群,可能會另指 exception)。當然,平時在聊天時,大家還是繼續說 bug 唄。 
  3. 通常「症」指的是疾病的徵象,「病」才是問題起源。 

Information Technology