如何優雅地定位外網問題?

騰訊雲加社群發表於2019-03-19

本文由雲+社群發表

作者:elson

現狀分析

在定位外網問題時,最怕的是遇到無法復現或者是偶現的問題,我們無法在使用者的裝置上通過抓包、打斷點或日誌來分析問題,只能靠僅有的頁面截圖和使用者的片面描述作為線索。此時,也只能結合“猜想法”和“排除法”進行分析定位,排查了半天也很有可能沒有結果,最後只能回覆“可能是快取或者app的原因,請清下快取或者重新安裝app試試”。

導致我們定位外網問題時效率低下,主要還是因為缺乏定位線索;其次由於使用者並不瞭解技術層面的前因後果,他們可能會忽略掉一些關鍵資訊,或者提供了帶有誤導性的線索。

常見的外網問題成因

從筆者實際上所遇到的外網問題進行歸類,主要有以下成因:

  1. 後臺資料返回異常,或部分資料為空;
  2. 針對邊界情況,頁面未做相對應的容錯措施,導致頁面報錯;
  3. 使用者的網路環境、APP版本問題;
  4. 通過上一級入口進入頁面時,漏傳部分引數;
  5. 與使用者特定的操作步驟相關所引發。

針對頁面JS報錯,我們已有指令碼異常上報監控機制,業界也不乏相關的優秀開源產品,如sentry。但往往很多情況下的使用者反饋以及外網異常並不是指令碼異常引起的,此時無法觸發異常上報。因此針對這部分場景,我們需要有另一套機制進行上報監控,輔助我們定位分析。

使用者的行為軌跡的重要性

從上面的問題成因可以得出,如果我們能採集到並結合以下幾方面資料,那外網異常的定位自然會事半功倍:

  • 頁面的執行環境
  • 頁面所載入的資料
  • 頁面JS報錯資訊
  • 使用者的操作日誌(時間線)

我們可以通過時間戳將以上資料串聯起來,形成時間線。這樣一來,頁面的執行環境、頁面中每個動作相關的資料、動作之間先後關係就會一目瞭然,就像一部案發現場的錄影。因此這裡強調“軌跡”的重要性,能夠把散亂的資料串聯起來,這對我們分析定位問題非常有幫助。

基於上面的分析結論,我們搭建了一套使用者行為軌跡追蹤系統,大致工作流程為:在頁面中載入JS SDK用於資料記錄和上報,伺服器接收並處理資料,再以介面的方式提供資料給內部查詢系統,支援通過使用者UIN以及頁面地址進行查詢。

下面我們從報什麼、怎麼報、伺服器如何處理資料、資料怎樣展示四方面具體談一下整體的設計思路。

設計思路

報什麼:確定上報內容及協議

根據上面的分析,我們已經初步得出了需要上報的資料內容。

上報的內容最終需要落地到查詢系統中,因此首先需要確定怎樣查詢。我們將使用者在某頁面的單次訪問作為基本查詢單位,假設某使用者訪問了3次A頁面,那麼在查詢平臺中就可以查出3條記錄,每條記錄可以包含多條不同型別的子記錄,它們共用“基礎資訊”。大致的資料結構如下:

const log = {
    baseInfo: {},
    childLogs: [{...}, {...}, ...]
};
複製程式碼

基礎資訊

baseInfo中記錄的是頁面的執行環境,可以稱為“基礎資訊”,具體包括以下欄位:

欄位名 描述 可選引數
FtraceId 某次頁面訪問的唯一標識(自動生成)
Fua navigator.userAgent
FclientType 客戶端型別 0:未知 1:qqmusic 2:weixin 3:mqq
Fos 系統 0:未知 1:ios 2:android
Furl 頁面地址 navigator.userAgent
Frefer 頁面上級入口 document.referrer
FloginType 帳號型別 0:wx 1:qq
Fuin 使用者帳號

childLogs中儲存所有子記錄,以下是子記錄的公用欄位以及三種不同型別。

子記錄公共欄位

每條子記錄需要記錄時間戳、標識上報型別,因此需要定義以下的公共欄位:

欄位名 描述 可選引數/格式 備註
Flogtype 上報型別 0: ajax通訊 1:使用者操作 2:報錯異常
FtimeStamp 時間戳 串聯不同型別的上報記錄,形成軌跡
Forder 數字順序 Number 當前記錄在整條軌跡中的自增序號

Forder的作用在於當兩條記錄的 FtimeStamp 值相同時,作為輔助的排序依據。

子記錄型別1:ajax通訊

記錄頁面中所有ajax通訊的資料,方便排查異常是否與後臺資料有關。

欄位名 描述 可選引數
FajaxSendTime ajax請求發起時間點
FajaxReceiveTime ajax資料接收到時間點
FajaxMethod ajax請求型別 0:get 1:post
FajaxParam ajax請求引數
FajaxUrl ajax請求連結
FajaxReceiveData ajax請求到的資料
FajaxHttpCode http返回碼(200, 404)
FajaxStateCode 後臺返回的業務相關code碼

子記錄型別2:使用者操作行為

記錄打點資料以及使用者點選操作的DOM上的資料

欄位名 描述 可選引數/格式
FtraceContent 自定義上報內容 String
FdomPath 操作目標DOM的xpath
Fattr 目標DOM的所有data-attr屬性及其值 {att1: '123', att2: '234'}

子記錄型別3:報錯異常

記錄JS報錯資訊以及我們手動丟擲的異常資訊

欄位名 描述 可選引數/格式 備註
FerrorType 錯誤型別 0:原生錯誤 1:手動丟擲的異常
FerrorStack 錯誤堆疊 僅原生錯誤報
FerrorFilename 出錯檔案
FerrorLineNo 出錯行
FerrorColNo 出錯列位置
FerrorMessage 錯誤描述 原生錯誤的errmsg或者開發自定義

怎麼報:SDK的資料採集及上報策略

上述的資料需要通過頁面載入SDK進行採集,那麼怎樣採集,如何上報?

資料採集方式

從業務場景以及常見的外網問題考慮,我們只關注帶有登入態的場景。對於未登入或獲取不到登入態的場景,SDK不做任何資料採集和上報。

( 1 ) 基礎資訊

FtraceId可以直接搜 uuid 的生成演算法,使用者每進入頁面時自動生成一個,後續採集的子記錄共用此 ID。

其他欄位則可以從 cookie 或者原生 API 中獲取,這裡不再贅述。

( 2 ) ajax 通訊資料

這裡用到了一個開源元件 Ajax-hook ,原始碼很簡練,GZIP 後只有 639 位元組。主要原理是通過代理 XMLHttpRequest 以及相關例項屬性和方法,提供各個階段的鉤子函式。

hookAjax({
    open: this.handleOpen,
    onreadystatechange: this.handleStage
});
複製程式碼

一次 ajax 通訊包含 opensendreadyStateChange 等階段,因此需要在不同階段的鉤子函式中採集從請求發起到接收到請求響應的各方面資料。

具體來說

  1. open 中可以採集:請求發起時間點、請求方法、請求引數等。需要注意過濾掉無用的請求,如資料採集後的上報請求。
  2. send 中主要用於採集 POST 請求的請求引數。
handleOpen(arg, xhr) {
        const urlPath = arg[1] && arg[1].split('?');
        xhr.urlPath = urlPath[0];
        
        // 過濾掉上報請求
        if (/stat\.y\.qq\.com/.test(urlPath[0])) {
            return;
        }

        curAjaxFields = $.extend({}, ajaxFields, {
            FtimeStamp: getNowDate(),
            FajaxSendTime: getNowDate(),
            FajaxMethod: arg[0] ? methodMap[arg[0].toUpperCase()] : '',
            FajaxUrl: urlPath[0],
            FajaxParam: urlPath[1],
            Forder: logger.order++
        });
        
        xhr.curAjaxFields = curAjaxFields;

        const _oriSend = xhr.send.bind(xhr);
        xhr.send = function(body) {
            // POST請求 獲取請求體中的引數
            if (body) {
                curAjaxFields.FajaxParam = body;
            }
            _oriSend && _oriSend(body);
        };
    }
複製程式碼
  1. readyStateChange 中,當 xhr.readyState 為 2(HEADERS_RECEIVED) 或 4(DONE) 時,分別採集 FajaxReceiveTime 和 響應資料相關資料。這裡需要注意的,為了把前期從 opensend 中採集到的資料傳遞下來,我們將資料物件掛載在當前 xhr 物件上: xhr.curAjaxFields = curAjaxFields;
handleStage({ xhr }) {
        // 過濾掉上報請求
        if (/stat\.y\.qq\.com/.test(xhr.urlPath)) {
            return;
        }
        switch (+xhr.readyState) {
            case 2: // HEADERS_RECEIVED
                $.extend(xhr.curAjaxFields, {
                    FajaxReceiveTime: getNowDate(),
                    FajaxHttpCode: xhr.status
                });
                break;

            case 4: // DONE
                const xhrResponse = xhr.response || xhr.responseText;
                let jsonRes;
                
                try {
                    // 如果回包不是json格式的話會報錯
                    jsonRes = xhrResponse ? JSON.parse(xhrResponse) : '';
                    ...
                } catch (e) {
                    console.error(e);
                }
                
                $.extend(xhr.curAjaxFields, {
                    FajaxReceiveData: xhrResponse,
                    FajaxStateCode: jsonRes ? getStateCode(jsonRes).join(',') : ''
                });
                
                break;
        }
 }
複製程式碼

( 3 ) 使用者操作行為

通過事件代理,在 document 上監聽指定類 .js_qm_tracer 的事件回撥。在回撥中通過event.path 取到當前 dom 的路徑;通過 event.currentTarget.attributes 取到當前 dom 上的所有屬性。

同時還提供 API 實現自行上報 action.report(data)

$(document).on('click', '.js_qm_trace', e => {
    const target = e.currentTarget;
    // 時間戳
    let FtimeStamp = getNowDate();

    // Dom的xpath
    let FdomPath = _getDomPath(e.path);

    // dom的所有data-attr屬性以及值
    let Fattr,
        FtraceContent = null;
    if (target.hasAttributes()) {
        let processedData = _processAttrMap(target.attributes);
        Fattr = processedData.Fattr;
        FtraceContent = processedData.FtraceContent;
    }
    ......
});
複製程式碼

上報策略

上面的資料,如果我們記錄一條就上報一條,這無疑是給自己製造DDOS攻擊。此外,我們的初衷在於幫助排查外網問題,因此在我們需要用的時候再報上來就行了。所以需要引入本地快取和使用者白名單機制,採集完先在本地快取起來,需要的時候再根據使用者白名單“撈取”。

本地快取機制我們選用的是 IndexedDB,它容量大( 500M ),非同步讀寫的特性保證其不會對頁面渲染產生阻塞,此外還支援建立自定義索引,易於檢索,更適合管理採集到的資料。

使用者白名單機制則是通過一個後臺服務,SDK初始化後都會先查詢當前使用者和頁面URL是否均在白名單中,是的話則將之前快取的資料進行上報,而之後的使用者行為操作也會直接上報,不再先快取。

但如果遇到JS錯誤報錯,屬於緊急情況,這時則不再遵循“快取優先”,而是直接上報錯誤資訊以及當前採集到的其他資料。

上報策略流程圖:

img

白名單機制流程圖:

img

獲取到白名單使用者的資料需要使用者再次訪問頁面,一方面從效能和開發成本考慮,另一方面反饋外網問題的使用者很大概率是會再次訪問當前頁面的。只需要再次進入頁面,無需額外操作,這樣對使用者來說也沒有沉重的操作成本和溝通成本,簡單易操作。

資料處理:伺服器對資料的處理策略

img

( 1 ) 首先,資料上報請求經過 nginx 伺服器後,會生成 access.log。

http {
    log_format trace '$request_body';
    
    server {
        location /trace/ {
           client_body_buffer_size 1000m;
           client_max_body_size 1000m;
           proxy_pass http://127.0.0.1:6699/env;
           access_log /data/qmtrace/log/access.log trace;
       }
    }
    
    server {
        listen 6699;
        location /env/ {
            client_max_body_size 1000m;
            alias /data/qmtrace/;
       }
    }
}
複製程式碼

使用 nginx 日誌進行記錄,主要是因為 nginx 優異的效能,能抗住高併發;此外其接入和維護成本也較低。

這裡在處理 POST 請求的日誌時,遇到一個坑。如果不經過 proxy_pass 轉發一次的話,nginx 無法對 POST 請求產生日誌記錄。

此外需要注意的是緩衝區的大小, client_body_buffer_size 預設只有 8K 或 16K,如果實際請求體大小超過了它,那就會被忽略,無法產生日誌記錄。

( 2 ) 通過 crontab 每五分鐘定期處理一次 access.log

access.log 移動到相應的以年月日小時命名的目錄下,生成 access_${minute}.log

移走 access.log 之後,此時需要執行以下命令,傳送通知給 nginx,收到通知後會重新生成新的 access.log

kill -USR1 `cat ${nginx_pid}`
複製程式碼

最後用node指令碼,對 access_${minute}.log 進行解析處理後入庫。

資料展示:搭建查詢平臺

img
查詢平臺

採集到的資料,在內部查詢平臺通過使用者 UIN 進行檢索,同時支援輸入特定的頁面 URL,進一步聚焦檢索結果。

在之前我們提到,將使用者在某頁面的單次訪問作為基本查詢單位,假設某使用者訪問了3次A頁面,那麼在左側就會檢索出3條記錄(每條記錄都有唯一標識 FtraceId )。

為了查詢平臺的效能考慮,每次查詢只會返回左側的記錄列表以及第一條記錄的詳細資訊。點選其他記錄再根據 FtraceId 進行非同步查詢。

右側展示的是某條記錄的詳細資訊,通過時間線的形式將使用者在某次頁面訪問期間的行為軌跡直觀地展示出來。通過客觀且直觀的使用者軌跡資料,我們就可以更高效更有針對性地分析定位外網問題。

總結

我們通過報什麼(上報內容及協議)、怎麼報(SDK採集及上報策略)、資料如何處理、資料怎樣展示,四個方面介紹瞭如何搭建使用者行為軌跡追蹤系統。目前只是個初級版本,有很多地方需要繼續完善和改進。有了追蹤使用者軌跡資料,能夠從很大程度上有效靈活地應對使用者反饋和外網異常,從而也很好地提升了我們的工作效率。

參考

  1. 前端異常監控解決方案研究
  2. 監控平臺前端SDK開發實踐
  3. 瀏覽器資料庫 IndexedDB 入門教程
  4. Ajax-hook 原理解析

此文已由騰訊雲+社群在各渠道釋出

獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號

相關文章