前端 JavaScript 錯誤分析實踐

做者:帥哥紅javascript

前言

在平日的工做中前端 badjs 是一個比較常見的問題, badjs 除了咱們自身業務 js 腳本里比較明顯的報錯外還有依賴其餘資源的一些報錯,對於自身業務 js 裏出現的錯誤很容易進行定位並修復,但對於依賴資源的錯誤即常見的 script error (外部 js、接口錯誤)定位就沒那麼容易了。css

前端開發的工做除了完成平常的業務特性外還有一項重要的工做就是線上頁面質量的運營(其中 badjs 監控及異常分析是工做內容的重要部分),本文主要講述 script error 採集、定位、統計以及分析的的一些方法及思路,但願對你們在分析定位問題時有必定的幫助。html

script error 由來

咱們的頁面每每將靜態資源( js、css、image )存放到第三方 CDN,或者依賴於外部的靜態資源。當從第三方加載的 javascript 執行出錯時,因爲同源策略,爲了保證用戶信息不被泄露,不會返回詳細的錯誤信息,取之返回 script error。前端

webkit 源碼:vue

bool ScriptExecutionContext::sanitizeScriptError(String& errorMessage, int& lineNumber, String& sourceURL) {
  KURL targetURL = completeURL(sourceURL);

  if (securityOrigin()->canRequest(targetURL)) return false;
  // 非同源,將相關的錯誤信息設置成默認,錯誤信息置爲 Script error,行號置成0
  errorMessage = "Script error.";
  sourceURL = String();
  ineNumber = 0;
  return true;
}

bool ScriptExecutionContext::dispatchErrorEvent(const String& errorMessage, int lineNumber, const String& sourceURL) {
  EventTarget* target = errorEventTarget();
  if (!target) return false;
  String message = errorMessage;
  int line = lineNumber;
  String sourceName = sourceURL;
  sanitizeScriptError(message, line, sourceName);
  ASSERT(!m_inDispatchErrorEvent);
  m_inDispatchErrorEvent = true;
  RefPtr<ErrorEvent> errorEvent = ErrorEvent::create(message, sourceName, line);
  target->dispatchEvent(errorEvent);
  m_inDispatchErrorEvent = false;
  return errorEvent->defaultPrevented();
}
複製代碼

常見的解決方案

外部 script error 常見的上報詳細錯誤日誌如如下兩種方法:java

1. 開啓 CORS 跨域資源共享

a) 添加 crossorigin="anonymous" 屬性:react

<script src="http://domain/path/*.js" crossorigin="anonymous"></script>
複製代碼

當有 crossorigin="anonymous",瀏覽器以匿名的方式獲取目標腳本,請求腳本時不會向服務器發送用戶信息( cookie、http 證書等)。jquery

b) 此時靜態服務器須要添加跨域協議頭:c++

Access-Control-Allow-Origin: *
複製代碼

完成這兩步後 window.onerror 就可以捕獲對應跨域腳本發生錯誤時的詳細錯誤信息了。web

2. try catch

crossorigin="anonymous" 確實能夠完美解決 badjs 上報 script error 問題,可是須要服務端進行跨域頭支持,而每每在大型企業,域名多的使人髮指,致使跨域規則配置很是複雜,因此很難所有都配置上,並且依賴的一些外部資源也不能確保支持,因此咱們在調用外部資源方法以及一些不確認是否配置跨域頭的資源方法時採用 try catch 包裝,並在 catch 到問題時上報對應的錯誤。

function invoke(obj, method, args) {
  try {
    return obj[method].apply(this, args);
  } catch (e) {
    reportBadjs(e); // report the error
  }
}
複製代碼

實例分析

一、對於擁有靜態服務器的配置權限的資源咱們能夠統一配置支持跨域頭信息,而且請求時統一增長 crossorigin="anonymous",這樣能夠完美將對應的錯誤堆棧信息進行上報。

二、jsonp 請求問題。

爲了解決頁面請求中的跨域問題,每每咱們頁面接口以 jsonp 的方式進行數據獲取,對於 jsonp 請求的方式通常引發 badjs 的有兩種狀況:

  • a) 接口請求異常,線上常見的就是在出現接口異常時 302 返回一個 error 頁面,該種狀況因爲返回的內容不可以解析因此直接致使 script error; 對於這種狀況雖然咱們不能直接對 script error 進行詳細上報,可是能夠根據回調與加載接口的 onload 進行接口的錯誤上報,具體的方法以下面僞代碼:
// 資源加載完成觸發 onload 事件
el.onload = el.onreadystatechange = function () {
 	if(!cgiloadOk) { // 沒有正常的回調,則上報對應的錯誤信息
 		report(cgi, 'servererror');
 	}
}

window.newFunction = function(rsp) {
 	cgiloadOk = true;
 	window.originFunction(rsp);
}
複製代碼

如上僞代碼,咱們攔截用戶的回調函數,在回調函數進行打標,當資源加載完後會觸發 onload,而後在 onload 裏判斷接口是否正常回調了,若是沒有正常回調那就上報對應的 cgi 跟定義的錯誤信息。這樣就能夠在監控系統裏結合 servererror 來分析是不是因爲接口致使的頁面 badjs 上漲,同時將對應的問題反饋給對應的接口負責人,避免接口上線,或者線上運行出現問題時致使的頁面異常。

  • b) 接口返回數據異常(非標準 json ),這種狀況也會直接致使 script error。

對於這種狀況咱們能夠改造對應的接口將 json 數據以 json string 類型的形式進行返回,而後在回調中進行轉換解析數據,在解析時採用 try catch 進行包裝,當捕獲到錯誤時進行錯誤上報。

let sc = document.createElement('script');
  let head = document.getElementsByTagName('head')[0];
  sc.setAttribute('charset', charset || 'utf-8');
  sc.src = url;
  head.appendChild(sc);

  window.newFunction = function(text) {
    // 採用try catch捕獲異常
    try {
      let jsonStr = JSON.parse(text)
    } catch(e) {
      // 出現轉換異常,則將對應的錯誤數據進行上報
      reportBadjs(text);
    }
  }
複製代碼

jsonp 請求數據方式的缺點就是隻支持 get,而且出現異常時不可以獲取對應的返回狀態碼。

三、ajax 請求方法。

ajax 方法就比較靈活了,可以獲取接口返回的狀態碼、返回數據,進而區分兩種錯誤並進行上報,僞代碼以下:

let xmlHttp = new XMLHttpRequest();

xmlHttp.onreadystatechange = function() {
 	if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
 		let str = xmlHttp.responseText;
 		try {
 			let json = JSON.parse(jsonstr);
 			//TODO,渲染對應的數據
 		} catch(e) {
 			report(jsonstr, 'data parse err'); // 數據解析錯誤,則將原數據進行上報,用於錯誤分析,修正接口返回
 		}
 	} else if (xmlHttp.readyState == 4) {
 		report(cgi, xmlHttp.status); // 接口返回重定向,則將對應的接口以及對應的status進行上報
 	}
}

xmlHttp.open('GET', cgi, true);            
xmlHttp.send();
複製代碼

該方法的優勢是可以獲取用戶返回的錯誤數據,並將錯誤數據進行上報,同時也可以獲取到接口請求的狀態;缺點是接口必須支持跨域。

四、依賴的外部資源不支持配置跨域頭。

這種狀況咱們只能對調用外部資源方法是進行 try catch 捕獲並上報異常。

平常工做中最經常使用到的 jqeury、zepto 可採用以下方法進行包裝:

function myBind (obj) {
  let o = {
    'type': 'click', // 事件類型
    'src': '.', // jquery 選擇器
    'fun': function () {}, // 方法
    'isStop': true // 阻止事件冒泡
  };
  let i;

  for (i in obj) {
    o[i] = obj[i];
  }

  if (typeof o.src === 'string') {
    o.$src = $(o.src);
  }

  $(o.src).off(o.type).on(o.type, function (e) {
    try {
      o.fun.apply(o, [e, $(this)]);
    } catch (ea) {
      reportBadjs(ea.stack);  //上報錯誤
    }
    if (o.isStop) {
      return false;
    }
  });
}
複製代碼

另類分析思路

對於外部依賴的資源未設置跨域頭信息,而且自己執行產生的 badjs 以及用戶刷頁面引發的一些錯誤,目前沒有很好定定位方法,可是咱們能夠根據一些輔助方法來分析問題大致產生的緣由,以及出現問題時頁面運行的情況。其實當線上頁面忽然出現大量的 script error 時,咱們最主要的就是要確保頁面是否健康正常的運行。接下來提供幾種分析的方法用於幫助確認當前頁面是否健康運行。

1. 客戶端分析

a)渠道佔比:

客戶端分析主要是根據根據上報 script error 的 ua 進行統計分析。渠道佔比即頁面 script error 各個渠道的佔比狀況,如微信端、sq 端、H5 端(其餘瀏覽器),能夠根據頁面流量的佔比進而判斷某個渠道的 script error 是否正常,如:當忽然某個時間點badjs上漲,而且無發佈時,當流量微信佔絕大部分,且 sq 渠道或者其餘渠道量明顯上漲時可推斷有多是刷子流量致使的 badjs,事實證實也常常如此。 以下圖爲平時正常請求下的badjs各個渠道的佔比狀況

下圖爲出現異常時badjs各個渠道的佔比狀況

經過該圖能夠很明顯發如今jdpingou渠道佔比不正常,經查看錯誤日誌以及與客戶端同事分析,發現是在該app內頁面點擊物理返回時作了一次上報,上報時未獲取到對應的方法而產生。

b)ua 佔比:

ua 佔比就是分析各個 ua 佔比的狀況了,因爲 ua 分佈比較散,客戶端版本衆多,因此通常狀況下從中不容易發覺,但當發佈某個版本時若是某個 ua 佔比明顯,那能夠推斷有個能是寫js時裏面存在不兼容某個客戶端的狀況(每每是用來新的語法,各客戶端對語法的支持不同)。

2. 結合用戶行爲分析

a) 記錄用戶操做(點擊),以移動端爲例:

let eventElemArr = [];
  document.body.addEventListener('touchend', function (e) {
    // 記錄點擊的dom相關信息
    eventElemArr.push([e.target.tagName, className].join('_'));
  }, false);

  window.onerror = function(msg, url='', line='', col='', error='') {
    // 將用戶點擊dom相關信息拼接到錯誤信息中並進行上報
    let errStr = [JSON.stringify(msg), url, line , col, error, eventElemArr.join('|')].join('$');
    report(errStr);
    return false;
  };
複製代碼

在分析系統中咱們再結合用戶在頁面中的行爲進行斷定用戶當前訪問的頁面是否正常。

b)script error 每每很差重現,客戶端分析只能推斷錯誤是否因爲異常操做所引發(刷子),可是真正要確認 badjs 對頁面是否有影響,是否影響用戶正常操做,能夠結合服務端進行判斷。具體的思路是進入頁面時前端生成一個 traceid(traceid 生成能夠是時間戳+業務+隨機碼,基本惟一),頁面請求全部的接口時帶上該 traceid 而且後臺記錄對應的日誌(也能夠前端進行上報),當出現badjs上報時也將 traceid 進行上報,這樣就能夠記錄當用戶出現 badjs 時總體訪問記錄了(經過 traceid 進行串聯)。

// 頁面配置場景值,用於生成traceid
  window.initTraceid = {
    bizId // 頁面bizId
    operateId // 頁面traceid
  }

  // 公共代碼(公共頭)生成traceid
  (function(){
    window.initTraceid {
      window.traceid = genTraceid(window.initTraceid);
    }
  })()

  // 上報badjs時帶上對應的traceid
  window.onerror = function(msg, url='', line='', col='', error='') {
    let errStr = [JSON.stringify(msg), url, line , col, error, window.traceid || ''].join('$');
    report(errStr);
    return false;
  };

  // 請求接口帶上對應的traceid,用於與badjs進行關聯,這樣就能夠記錄用戶進入頁面頁面接口的請求情況
  function myRequst(url) {
    url = url.addParam({traceid: window.traceid || ''});
    requst(url);
  }

複製代碼

該種方式能夠根據 badjs 中的 traceid 進行關聯用戶進入頁面時的接口請求情況,用於輔助定位用戶訪問頁面是否有問題。如:工做中常常碰到 script error 毛刺,就能夠查詢該時間段的錯誤日誌,而後經過 traceid 查詢訪問記錄,每每致使 script error 的是因爲某個熱銷商品被刷的特別厲害,一些刷子的非正常操做致使的頁面 badjs。

3. 現場還原

3.1 錄製視頻

當出現 script error 時將用戶進入頁面的屏幕快照錄製成一個視頻隨 script error 一塊兒上報。實現該功能目前主要有兩種方法,一種是利用 canvas 畫圖截取屏幕圖片;第二種就是記錄頁面dom變化,並將對應的記錄上報,而後在分析系統根據快照和操做鏈進行播放。

a) canvas 截取圖片,該方法的實現思路是利用 canvas 將網頁生成圖片,而後緩存起來,爲了使得生成的視頻流暢,咱們一秒中須要生成大約 25 幀,也就是須要 25 張截圖,而後在出現 script error 時將緩存起來的頁面圖片進行上報,再在分析系統經過技術將頁面瀏覽進行還原。

該方式的缺點很明顯,用戶訪問一個頁面若是停留時間長必然會生成大量的圖片,會帶來很大的網絡開銷以及存儲開銷。

b) 用戶操做重現

該方法主要是記錄用戶頁面 dom 的變化,而後在出現 script error 時將對應的記錄進行上報,而後在分析系統裏經過技術將頁面還原。

大致的思路就是:

  1. 進入頁面,生成頁面的虛擬dom全量快照;

  2. 運用 API:MutationObserver,記錄用戶變化的 dom,同時記錄用戶的一些行操做(click,select,input,scroll 等事件);

3)當出現 script error 時將對應快照信息上報;

4)在分析系統中將快照與用用戶的操做還原。

3.2 頁面數據上報

該方法在使用數據驅動框架(vue,react)的頁面中很是的方便,當出現錯誤時能夠將頁面當前端數據信息與錯誤一塊兒上報,而後在分析系統經過必定的技術將頁面還原,復現出現問題時的頁面。

日誌上報、統計、分析以及監控

前面講到了 badjs 中 script error 的由來、常見的解決方案、實例分析以及另類思路,最後再講一下日誌的上報、統計、分析以及監控。

1. 日誌上報

// 全局的 onerror 用於捕獲頁面異常
  window.onerror = function(msg, url, line, col, error) {
    let excludeList = ['WeixinJSBrige']; // 剔除一些確認的自己客戶端引發的問題,避免對上報後的數據分析引發干擾
    // 拼接錯誤信息
    let errStr = obj2str(msg) + (url ? ';URL:' + url : '') + (line ? ';Line:' + line : '') + (col ? ';Column:' + col : ''
    // 剔除白名單內錯誤上報,避免對上報結果乾擾
    for (let item in excludeList) {
      if (errStr.indexof(item) > -1) {
        return;
      }
    }
    // 構造圖片請求,用於上報
    let g = new Image()
    // 存在 traceid 則拼接 traceid 用於日誌串聯
    g.src = reportUrl + errStr + '&t=' + Math.random()+(window.traceid ? '&traceid=' + window.traceid : '')
  }
    return false;
};

複製代碼

「對於使用了promise以及框架(vue,react)自己內部會攔截錯誤,須要添加對應的方法進行手動上報」

// promise 錯誤上報
window.addEventListener('rejectionhandled', event => {
  // 錯誤的詳細信息在 reason 字段
  window.onerror('', '', '', '', event.reason)
});

// vue 錯誤上報
Vue.config.errorHandler = function (err, vm, info) {
  window.onerror(info, '', '', '', err)
}
// ...其餘的就不一一列舉了
複製代碼

在服務端收集日誌是除上報過來的日誌還須要根據請求採集 IP、userinfo、traceid、netType、ua、time 等等

2. 日誌統計與分析

a) 數量視圖。最直白的統計莫屬實時的錯誤數量視圖了,經過該視圖能夠查看當前頁面實時的錯誤數量,同時頁能夠配置規則,當 badjs 異常上漲時設置對應的告警,避免發版本時出現錯誤而未發現,進而影響用戶正常的頁面訪問。

b)日誌聚合展現(errmsg);以錯誤信息進行日誌聚合,能夠直觀查看哪些錯誤比較多。

c)明細日誌展現;統計錯誤日誌的詳細信息,經過詳細信息能夠查看錯誤發生時用戶渠道、網絡類型、用戶信息、ua 信息等,最主要的是能夠經過 traceid 查看用戶訪問頁面的詳細信息,用於判斷頁面訪問是否正常。

d)多維度統計分析(運營商、用戶、機型、網絡、系統、渠道等);經過多維度的統計聚合,能夠很直白的查看錯誤在不一樣的維度展現(如另類分析思路中的渠道佔比,ua 佔比),幫助分析定位問題。

3. 錯誤監控

在筆者的工做中將 badjs 根據是否由接口致使的區分爲普通 badjs 與 servererror badjs 與 servererror 的波動狀況。普通的 badjs 能夠根據對應的日誌以及分析視圖來幫助輔助定位並修復,對於 servererror 則通知對應的接口負責人進行問題定位修復。 在建立頁面時爲每一個頁面自動生成兩個key(badjs 與 serveerror)。badjs 和 serveerror),badjs 上報以後,refer就是頁面的URL,分析服務依照頁面url進行聚合計算,從而進行實時監控。

a) 規則配置。咱們將告警規則設置爲黃燈告警與紅燈告警,黃燈起提示做用,紅燈則屬於嚴重告警,當觸發規則時則自動推送消息給對應的負責人並告警,這樣就能夠快速響應處理問題。

下圖爲 badjs 告警規則配置:

下圖爲 servererror 告警規則配置:

b) 視圖監控。以下圖就是在公共頁面某個版本後致使的 badjs 毛刺(普通 badjs,未影響到頁面的正常訪問)。在收到告警後即刻進行版本回滾,定位問題修復後再二次上線。

在收到 servererror 告警時,咱們還須要定位到對應的接口,在前面的上報中咱們已經上報了對應的接口信息,因此能夠經過監控系統查詢對應的接口。

結尾

本文主要總結了本身工做中前端 badjs 經常使用的一些上報、定位、分析方式與思路以及日誌的上報、統計、分析與監控,對 badjs 定位分析以及 script error 提供一種推斷思路,但願對你們有所幫助。

參考文獻


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam

img10.360buyimg.com/wq/jfs/t1/4…

相關文章
相關標籤/搜索