收集錯誤信息及堆棧-前端監控之數據收集篇

js錯誤是第一指標,任何一個js錯誤都有可能致使阻塞,影響咱們頁面的正常運轉。html

本篇主要對js錯誤收集的分享前端

1. 瞭解異常發生的狀況和影響

注: 瞭解異常發生的狀況及影響, 有助於咱們選擇合適方式進行異常捕獲處理vue

任何一個js異常的發生都會致使當前代碼塊不能正常執行react

那麼那些狀況會致使js異常阻塞呢?git

咱們都知道, js是單線程的、基於事件循環機制的的語言, 接下來咱們分別討論異常發生的狀況及影響github

狀況一: 同步代碼出現異常

同一個線程中運行的代碼,異常阻塞api

var a = 0
console.log("--step1")
a = a+b
console.log("--step2")
複製代碼

--step1
ReferenceError: b is not defined跨域

狀況二:多個代碼片斷,其中之一出現異常

當咱們在代碼中使用多塊scriptpromise

<body>
進入part1
<script>
    console.log("====step1")
    var a= 1
    a = a + b
    console.log("====step2")
</script>
 
進入part2
 
<script>
    console.log("====step3")
</script>
</body>
複製代碼

====step1
ReferenceError: b is not defined
====step3瀏覽器

狀況三:外鏈代碼,一個出現異常

多個外聯script, s1,s2代碼同上

<script src="./js/s1.js"></script>
<script src="./js/s2.js"></script>
複製代碼

結果同狀況2

====step1
ReferenceError: b is not defined
====step3

狀況四:同步異步代碼混合

var a = 0
console.log("--step1")
setTimeout(() => {
    console.log("--step3")
    a = a+b
    console.log("--step4")
},1000)
 
console.log("--step2")
複製代碼

結果以下

--step1
--step2
--step3
ReferenceError: b is not defined

狀況五:異步代碼外try...catch

對異步代碼進行異常捕獲

window.addEventListener("error", function() {
        console.log("全局錯誤捕獲",arguments)
    })
 
    try{
        var a = 1;
        setTimeout(function() {
            a = a+b
        }, 100)
    }catch(e) {
        console.log("===未在try..catch捕獲")
    }
複製代碼

狀況六:加異常捕獲

對可能出現異常的代碼加異常捕獲

var a = 0
console.log("--step1")
try{
    a = a+b
}catch(e){
    console.log(e.message)
}
 
console.log("--step2")
複製代碼

--step1
b is not defined
--step2

狀況七: await

包含promise的操做, 分別運行下述代碼中一、二、3的分支

async function a () {
    await Promise.reject("===step1")
}
 
//1
a()
console.log("===step2")        
 
//2
async function b() => {
    await a()
    console.log("===step3")
}
b()
 
// 3
async function c() {
    try{
        await a()
    }catch(e) {
        console.log(e)
    }
    console.log("===step4")
}
c()
複製代碼

結果

分支1:
===step2
UnhandledPromiseRejectionWarning: ===step1
分支2:
UnhandledPromiseRejectionWarning:===step1
分支3:
===step1
===step4

狀況八: promise代碼塊異常

window.addEventListener("error", function() {
        console.log("全局錯誤捕獲",arguments)
    })
    window.addEventListener("unhandledrejection", event => {
        console.warn("promise Error",event.reason.message, event.reason.stack)
    });
 
 
    function a() {
        return new Promise(function() {
            var a = 1
            a = a +b
 
        })
    }
    a()
複製代碼

測試結果

promise Error "b is not defined" "ReferenceError: b is not defined
at http://localhost:63342/jserror/test/thead9.html:20:20
at new Promise ()

以上測試,能夠得出如下結論

a. 同步代碼塊異常會阻塞後續代碼
b. 不一樣的script標籤之間互不影響
c. 異步代碼只會影響當前異步代碼塊的後續代碼
d.promise若是返回reject,須要使用catch捕獲處理
f. 若是使用async、await, 能夠轉換成try..catch捕獲

2. 瞭解js中異常拋出的內容

注: 異常拋出的內容,是咱們定位問題的關鍵

按1中異常出現的狀況,咱們知道,異常信息主要分兩類
一類是拋出Error相關錯誤
一類是promise reject未處理時異常

上述圖中,只描述了同域下錯誤及標準api提供的錯誤信息。

3.處理跨域狀況下的異常收集

非同域下錯誤是什麼樣子呢?

看範例

<script>
    window.onerror=function(e) {
        console.log("全局錯誤:", arguments)
    }
</script>
<script src="http://192.168.31.200:8080/js/s1.js"></script>
複製代碼

Chrome下獲得的結果是
Script error.
這是瀏覽器同源策略致使的, 會隱藏不一樣源資源的錯誤詳情

想處理以上狀況,有兩種方案,

方案一:crossorigin

對引入的資源加crossorigin="anonymous"

<script>
       window.onerror=function(e) {
           console.log("全局錯誤:", arguments)
       }
   </script>
   <script src="http://10.10.47.38:8080/js/s1.js" crossorigin="anonymous"></script>
複製代碼

Chrome下獲得結果
Uncaught ReferenceError: b is not defined

注: 該方法侷限性, 一是瀏覽器兼容性, 二是請求的資源須要添加CORS相關響應頭

方案二(只做爲不支持crossorigin的補充)

使用try..catch包裝須要捕獲錯誤的方法或代碼塊

如何肯定,哪些方法是咱們須要單獨封裝的?

咱們已經知道,異常是出如今外部js中,有多是cdn的資源或者引用網絡上其餘的資源,他們有個特色就是跨域, 一旦這些文件發生錯誤, 咱們沒法獲取到具體的錯誤信息,

那麼除了crossorigin,咱們還有那些方法能夠取到跨域js的異常呢?

a. 在同域js中調用跨域js的函數, 手動包裝try..catch

// s1.js
function m1 () {
    console.log("====step1")
    var a = 1
    a = a+b
    console.log("====step2")
}
// test.html
try{
    m1()
  }catch(e){
       throw e
}
複製代碼

m1爲跨域js提供的一個函數 這時候拋出的error等同於同域下的錯誤信息

b. 跨域js中有一些異步的代碼, 如setTimeout、eventListener等

對於這一類,咱們能夠對原生的方法進行封裝, 對參數包裹try...catch, 能夠達到手動包裝的效果

如 setTimeout, 咱們對函數入參進行封裝便可

// test.html
 window.onerror=function(e) {
            console.log("全局錯誤:", arguments[0])
        }
        var originTo = window.setTimeout
        function wrap (originTo) {
            return function(fun, arg) {
                var fun2 = function() {
                    try{
                        fun.call(this, arguments)
                    }catch(e){
                        throw e
                    }
                }
                originTo(fun2, arg)
            }
        }
        window.setTimeout = wrap(originTo)
 
m1()
 
// s5.js
function m1 () {
    setTimeout(function() {
        console.log("====step1")
        var a = 1
        a = a+b
        console.log("====step2")
    },100)
}
複製代碼

輸出結果爲:

全局錯誤: Uncaught ReferenceError: b is not defined

咱們使用自定義方法能夠對經常使用對象進行包裹, 可是並不能作到所有攔截, 如你們經常使用的sentry, 若是出現不在特定方法內的跨域錯誤, 會直接被sentry吞掉

基於以上思路, 咱們提供一個通用的封裝方法,可參考sentry或者badjs, sentry代碼以下

context: function(options, func, args) {
    if (isFunction(options)) {
      args = func || [];
      func = options;
      options = undefined;
    }
 
    return this.wrap(options, func).apply(this, args);
  },
 
  /* * Wrap code within a context and returns back a new function to be executed * * @param {object} options A specific set of options for this context [optional] * @param {function} func The function to be wrapped in a new context * @param {function} func A function to call before the try/catch wrapper [optional, private] * @return {function} The newly wrapped functions with a context */
  wrap: function(options, func, _before) {
    var self = this;
    // 1 argument has been passed, and it's not a function
    // so just return it
    if (isUndefined(func) && !isFunction(options)) {
      return options;
    }
 
    // options is optional
    if (isFunction(options)) {
      func = options;
      options = undefined;
    }
 
    // At this point, we've passed along 2 arguments, and the second one
    // is not a function either, so we'll just return the second argument.
    if (!isFunction(func)) {
      return func;
    }
 
    // We don't wanna wrap it twice!
    try {
      if (func.__raven__) {
        return func;
      }
 
      // If this has already been wrapped in the past, return that
      if (func.__raven_wrapper__) {
        return func.__raven_wrapper__;
      }
    } catch (e) {
      // Just accessing custom props in some Selenium environments
      // can cause a "Permission denied" exception (see raven-js#495).
      // Bail on wrapping and return the function as-is (defers to window.onerror).
      return func;
    }
 
    function wrapped() {
      var args = [],
        i = arguments.length,
        deep = !options || (options && options.deep !== false);
 
      if (_before && isFunction(_before)) {
        _before.apply(this, arguments);
      }
 
      // Recursively wrap all of a function's arguments that are
      // functions themselves.
      while (i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i];
 
      try {
        // Attempt to invoke user-land function
        // NOTE: If you are a Sentry user, and you are seeing this stack frame, it
        // means Raven caught an error invoking your application code. This is
        // expected behavior and NOT indicative of a bug with Raven.js.
        return func.apply(this, args);
      } catch (e) {
        self._ignoreNextOnError();
        self.captureException(e, options);
        throw e;
      }
    }
 
    // copy over properties of the old function
    for (var property in func) {
      if (hasKey(func, property)) {
        wrapped[property] = func[property];
      }
    }
    wrapped.prototype = func.prototype;
 
    func.__raven_wrapper__ = wrapped;
    // Signal that this function has been wrapped already
    // for both debugging and to prevent it to being wrapped twice
    wrapped.__raven__ = true;
    wrapped.__inner__ = func;
 
    return wrapped;
  }
複製代碼

咱們能夠調用wrap方法對函數進行封裝

如項目中使用了requirejs,那咱們能夠經過直接對require和define對象封裝,從而達到對跨域文件全內容封裝的目的

if (typeof define === 'function' && define.amd) {
    window.define = wrap({deep: false}, define);
    window.require = wrap({deep: false}, require);
  }
複製代碼

注: 該方法的侷限性在於,須要開發者發現項目中的一些關鍵入口並手動封裝

以上咱們討論了Error的類型、出現緣由及如何捕獲異常,然而上圖中標識的錯誤字段兼容性也是一大問題

好在前人栽樹後人乘涼,有一個庫能夠幫助咱們處理該問題 TraceKit O(∩_∩)O~~

4. 錯誤捕獲上報

結合2中內容,再加上萬能捕獲window.onerror, 便可對錯誤信息進行有效的獲取

若是你使用了traceKit庫, 那麼你能夠直接使用下面代碼

tracekit.report.subscribe(function(ex, options) {
          
               report.captureException(ex, options)
            
       })

複製代碼

若是沒有,那咱們能夠直接從新onerror方法便可

var oldErrorHandler = window.onerror
window.onerror = function(){
  // 上報錯誤信息
    
  if(oldErrorHander){
    oldErrorHandler.apply(this, argument)
  }
}
複製代碼

2圖中promise被單獨列出分支,由於咱們須要使用特定事件處理

if(window.addEventListener) {
            window.addEventListener("unhandledrejection", function(event) {
                report.captureException(event.reason || {message: "unhandlePromiseError"}, {frame: "promise"})
            });
        }
複製代碼

5. 框架類異常收集

針對如今流行的框架

vue

經過errorHandler鉤子處理

function formatComponentName(vm) {
    if (vm.$root === vm) {
        return 'root instance';
    }
    var name = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name;
    return (
            (name ? 'component <' + name + '>' : 'anonymous component') +
            (vm._isVue && vm.$options.__file ? ' at ' + vm.$options.__file : '')
    );
}
 
function vuePlugin(Vue) {
    return {
        doApply: function() {
            Vue = Vue || window.Vue;
 
            // quit if Vue isn't on the page
            if (!Vue || !Vue.config) return;
 
            var self = this;
 
            var _oldOnError = Vue.config.errorHandler;
            Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
                var metaData = {
                    componentName: formatComponentName(vm),
                    propsData: vm.$options.propsData
                };
 
                // lifecycleHook is not always available
                if (typeof info !== 'undefined') {
                    metaData.lifecycleHook = info;
                }
 
                self.captureException(error, {
                    frame: "vue",
                    extra: JSON.stringify(metaData)
                });
 
                if (typeof _oldOnError === 'function') {
                    _oldOnError.call(this, error, vm, info);
                }
            };
        }
    }
}
複製代碼

react

react16版本以後,引入錯誤邊界,有些非阻塞異常會經過該鉤子拋出

咱們能夠

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以顯示降級後的 UI
    return { hasError: true };
  }
 
  componentDidCatch(error, errorInfo) {
    // 你一樣能夠將錯誤日誌上報給服務器
    report.captureException(error, {
                    frame: "react",
                    extra: JSON.stringify(errorInfo)
                });
  }
 
  render() {
    if (this.state.hasError) {
      // 你能夠自定義降級後的 UI 並渲染
      return <h1>Something went wrong.</h1>;
    }
 
    return this.props.children; 
  }
}
複製代碼

至此,咱們就完成了對Error的信息獲取, 爲咱們作錯誤報警及堆棧還原作基礎

-=======================-
前端監控實踐系列

相關文章
相關標籤/搜索