本文由雲+社區發表做者:elsonhtml
在定位外網問題時,最怕的是遇到沒法復現或者是偶現的問題,咱們沒法在用戶的設備上經過抓包、打斷點或日誌來分析問題,只能靠僅有的頁面截圖和用戶的片面描述做爲線索。此時,也只能結合「猜測法」和「排除法」進行分析定位,排查了半天也頗有可能沒有結果,最後只能回覆「多是緩存或者app的緣由,請清下緩存或者從新安裝app試試」。前端
致使咱們定位外網問題時效率低下,主要仍是由於缺少定位線索;其次因爲用戶並不瞭解技術層面的來龍去脈,他們可能會忽略掉一些關鍵信息,或者提供了帶有誤導性的線索。node
從筆者實際上所遇到的外網問題進行歸類,主要有如下成因:android
針對頁面JS報錯,咱們已有腳本異常上報監控機制,業界也不乏相關的優秀開源產品,如sentry。但每每不少狀況下的用戶反饋以及外網異常並非腳本異常引發的,此時沒法觸發異常上報。所以針對這部分場景,咱們須要有另外一套機制進行上報監控,輔助咱們定位分析。ios
從上面的問題成因能夠得出,若是咱們能採集到並結合如下幾方面數據,那外網異常的定位天然會事半功倍:nginx
咱們能夠經過時間戳將以上數據串聯起來,造成時間線。這樣一來,頁面的運行環境、頁面中每一個動做相關的數據、動做之間前後關係就會一目瞭然,就像一部案發現場的錄像。所以這裏強調「軌跡」的重要性,可以把散亂的數據串聯起來,這對咱們分析定位問題很是有幫助。git
基於上面的分析結論,咱們搭建了一套用戶行爲軌跡追蹤系統,大體工做流程爲:在頁面中加載JS SDK用於數據記錄和上報,服務器接收並處理數據,再以接口的方式提供數據給內部查詢系統,支持經過用戶UIN以及頁面地址進行查詢。github
下面咱們從報什麼、怎麼報、服務器如何處理數據、數據怎樣展現四方面具體談一下總體的設計思路。ajax
根據上面的分析,咱們已經初步得出了須要上報的數據內容。算法
上報的內容最終須要落地到查詢系統中,所以首先須要肯定怎樣查詢。咱們將用戶在某頁面的單次訪問做爲基本查詢單位,假設某用戶訪問了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
值相同時,做爲輔助的排序依據。
記錄頁面中全部ajax通訊的數據,方便排查異常是否與後臺數據有關。
字段名 | 描述 | 可選參數 |
---|---|---|
FajaxSendTime | ajax請求發起時間點 | |
FajaxReceiveTime | ajax數據接收到時間點 | |
FajaxMethod | ajax請求類型 | 0:get 1:post |
FajaxParam | ajax請求參數 | |
FajaxUrl | ajax請求連接 | |
FajaxReceiveData | ajax請求到的數據 | |
FajaxHttpCode | http返回碼(200, 404) | |
FajaxStateCode | 後臺返回的業務相關code碼 |
記錄打點數據以及用戶點擊操做的DOM上的數據
字段名 | 描述 | 可選參數/格式 |
---|---|---|
FtraceContent | 自定義上報內容 | String |
FdomPath | 操做目標DOM的xpath | |
Fattr | 目標DOM的全部data-attr屬性及其值 | {att1: '123', att2: '234'} |
記錄JS報錯信息以及咱們手動拋出的異常信息
字段名 | 描述 | 可選參數/格式 | 備註 |
---|---|---|---|
FerrorType | 錯誤類型 | 0:原生錯誤 1:手動拋出的異常 | |
FerrorStack | 錯誤堆棧 | 僅原生錯誤報 | |
FerrorFilename | 出錯文件 | ||
FerrorLineNo | 出錯行 | ||
FerrorColNo | 出錯列位置 | ||
FerrorMessage | 錯誤描述 | 原生錯誤的errmsg或者開發自定義 |
上述的數據須要經過頁面加載SDK進行採集,那麼怎樣採集,如何上報?
從業務場景以及常見的外網問題考慮,咱們只關注帶有登陸態的場景。對於未登陸或獲取不到登陸態的場景,SDK不作任何數據採集和上報。
( 1 ) 基礎信息
FtraceId
能夠直接搜 uuid 的生成算法,用戶每進入頁面時自動生成一個,後續採集的子記錄共用此 ID。
其餘字段則能夠從 cookie 或者原生 API 中獲取,這裏再也不贅述。
( 2 ) ajax 通訊數據
這裏用到了一個開源組件 Ajax-hook ,源碼很簡練,GZIP 後只有 639 字節。主要原理是經過代理 XMLHttpRequest
以及相關實例屬性和方法,提供各個階段的鉤子函數。
hookAjax({ open: this.handleOpen, onreadystatechange: this.handleStage });
一次 ajax 通訊包含 open
,send
,readyStateChange
等階段,所以須要在不一樣階段的鉤子函數中採集從請求發起到接收到請求響應的各方面數據。
具體來講
open
中能夠採集:請求發起時間點、請求方法、請求參數等。須要注意過濾掉無用的請求,如數據採集後的上報請求。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); }; }
readyStateChange
中,當 xhr.readyState
爲 2(HEADERS_RECEIVED) 或 4(DONE) 時,分別採集 FajaxReceiveTime
和 響應數據相關數據。這裏須要注意的,爲了把前期從 open
和 send
中採集到的數據傳遞下來,咱們將數據對象掛載在當前 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錯誤報錯,屬於緊急狀況,這時則再也不遵循「緩存優先」,而是直接上報錯誤信息以及當前採集到的其餘數據。
上報策略流程圖:
白名單機制流程圖:
獲取到白名單用戶的數據須要用戶再次訪問頁面,一方面從性能和開發成本考慮,另外一方面反饋外網問題的用戶很大機率是會再次訪問當前頁面的。只須要再次進入頁面,無需額外操做,這樣對用戶來講也沒有沉重的操做成本和溝通成本,簡單易操做。
( 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
進行解析處理後入庫。
查詢平臺
採集到的數據,在內部查詢平臺經過用戶 UIN 進行檢索,同時支持輸入特定的頁面 URL,進一步聚焦檢索結果。
在以前咱們提到,將用戶在某頁面的單次訪問做爲基本查詢單位,假設某用戶訪問了3次A頁面,那麼在左側就會檢索出3條記錄(每條記錄都有惟一標識 FtraceId
)。
爲了查詢平臺的性能考慮,每次查詢只會返回左側的記錄列表以及第一條記錄的詳細信息。點擊其餘記錄再根據 FtraceId
進行異步查詢。
右側展現的是某條記錄的詳細信息,經過時間線的形式將用戶在某次頁面訪問期間的行爲軌跡直觀地展現出來。經過客觀且直觀的用戶軌跡數據,咱們就能夠更高效更有針對性地分析定位外網問題。
咱們經過報什麼(上報內容及協議)、怎麼報(SDK採集及上報策略)、數據如何處理、數據怎樣展現,四個方面介紹瞭如何搭建用戶行爲軌跡追蹤系統。目前只是個初級版本,有不少地方須要繼續完善和改進。有了追蹤用戶軌跡數據,可以從很大程度上有效靈活地應對用戶反饋和外網異常,從而也很好地提高了咱們的工做效率。
此文已由騰訊雲+社區在各渠道發佈
獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號