JS錯誤監控 上報後臺你瞭解多少?

lq_prototype發表於2019-02-21

1.try-catch

1. 執行時錯誤

try {

    fn() 
    
} catch (e) {
    
    console.log(e);
}

2. 非同步錯誤

try {
  setTimeout(() => {
  
    fn()        // 非同步錯誤
  })
} catch(e) {

  console.log('我感知不到錯誤');
  
  console.log(e);
}

複製程式碼

總結: 只能捕獲捉到執行時非非同步錯誤,非同步錯誤就顯得無能為力,捕捉不到

2. window.onerror

1.同步錯誤

/**
 * @param {String}  msg    錯誤資訊
 * @param {String}  url    出錯檔案
 * @param {Number}  row    行號
 * @param {Number}  col    列號
 * @param {Object}  error  錯誤詳細資訊
 */
window.onerror = function(msg, url, row, col, error) {
    console.log('我知道錯誤了');
    return true;
};

new Error();

2.非同步錯誤

window.onerror = function(msg, url, row, col, error) {
    console.log('我知道非同步錯誤了');
    return true;
};

setTimeout(() => {
   new Error();;
});
複製程式碼

注意:在事件處理程式中返回false,可以阻止瀏覽器報告錯誤的預設行為

window.onerror = function(msg, url, row, col, error) {
    return false;
}
複製程式碼

當我們遇到 "img src="./404.png" 報 404 網路請求異常的時候,window.onerror 是無法幫助我們捕獲到異常的。

3.資源載入錯誤

  • 1. object.onerror
  • 2. performance.getEntries()
  • 3. Error事件捕獲 (全域性監控靜態資源異常)
  1. object.onerror

    如script,image等標籤src引用,會返回一個event物件 ,TIPS: object.onerror不會冒泡到window物件,所以window.onerror無法監控資源載入錯誤

var img = new Image();
img.src = 'http://xxx.com/xxx.jpg';
img.onerror = function(event) {
    console.log(event);
}
複製程式碼
  1. performance.getEntries()

    返回已成功載入的資源列表,然後自行做比對差集運算,核實哪些檔案沒有載入成功

var result = [];
window.performance.getEntries().forEach(function (perf) {
    result.push({
        'url': perf.name,
        'entryType': perf.entryType,
        'type': perf.initiatorType,
        'duration(ms)': perf.duration
    });
});
console.log(result);

複製程式碼

JS錯誤監控 上報後臺你瞭解多少?
3. Error事件捕獲

**404.png**
複製程式碼
window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我知道 404 錯誤了');
  console.log(
    msg, url, row, col, error
  );
  return false;
}, true);

複製程式碼

4. 全域性去捕獲promise error

window.addEventListener("unhandledrejection", function(e) {
    e.preventDefault()
    console.log('我知道 promise 的錯誤了');
    console.log(e.reason);
    return true;
});
Promise.reject('promise error').catch((err)=>{
    console.log(err);
})
new Promise((resolve, reject) => {
    reject('promise error');
}).catch((err)=>{
    console.log(err);
})
new Promise((resolve) => {
    resolve();
}).then(() => {
    throw 'promise error'
});
new Promise((resolve, reject) => {
    reject(123);
})

複製程式碼

5. 跨域的js錯誤捕獲

一般涉及跨域的js執行錯誤時會丟擲錯誤提示script error,但沒有具體資訊(如出錯檔案,行列號提示等), 可利用資源共享策略來捕獲跨域js錯誤

  1. 客戶端:在script標籤增加crossorigin屬性(客戶端)
  2. 服務端:js資源響應頭Access-Control-Allow-Origin: *

6. Iframe錯誤

<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
  window.frames[0].onerror = function (msg, url, row, col, error) {
    console.log('我知道 iframe 的錯誤了,也知道錯誤資訊');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
複製程式碼

7. Node 錯誤監控

  1. uncaughtException來全域性捕獲未捕獲的Error

未捕獲的 JavaScript 異常一直冒泡回到事件迴圈時,會觸發 'uncaughtException' 事件。 預設情況下,Node.js 通過將堆疊跟蹤列印到 stderr 並使用退出碼 1 來處理此類異常,從而覆蓋任何先前設定的 process.exitCode。 為 'uncaughtException' 事件新增處理程式會覆蓋此預設行為。 或者,更改 'uncaughtException' 處理程式中的 process.exitCode,這將導致程式退出並提供退出碼。 否則,在存在這樣的處理程式的情況下,程式將以 0 退出

process.on("uncaughtException", function(a) {
    
})


複製程式碼
  1. unhandledRejection區域性錯誤

如果在事件迴圈的一次輪詢中,一個 Promise 被 rejected,並且此 Promise 沒有繫結錯誤處理器, 'unhandledRejection 事件會被觸發。 當使用 Promise 進行程式設計時,異常會以 "rejected promises" 的形式封裝。Rejection 可以被 promise.catch() 捕獲並處理,並且在 Promise 鏈中傳播。'unhandledRejection 事件在探測和跟蹤 promise 被 rejected,並且 rejection 未被處理的場景中是很有用的。

process.on("unhandledRejection", function(a) {
   
});
複製程式碼

8. console.error

var consoleError = window.console.error; 
window.console.error = function () { 
    alert(JSON.stringify(arguments)); // 自定義處理
    consoleError && consoleError.apply(window, arguments); 
};
複製程式碼

9. 介面錯誤

  1. xmlHttpRequest封裝
if(!window.XMLHttpRequest) return;
var xmlhttp = window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
    if (event && event.currentTarget && event.currentTarget.status !== 200) {
          // 自定義錯誤上報 }
}
xmlhttp.prototype.send = function () {
    if (this['addEventListener']) {
        this['addEventListener']('error', _handleEvent);
        this['addEventListener']('load', _handleEvent);
        this['addEventListener']('abort', _handleEvent);
    } else {
        var _oldStateChange = this['onreadystatechange'];
        this['onreadystatechange'] = function (event) {
            if (this.readyState === 4) {
                _handleEvent(event);
            }
            _oldStateChange && _oldStateChange.apply(this, arguments);
        };
    }
    return _oldSend.apply(this, arguments);
}
複製程式碼
  1. fetch封裝
if(!window.fetch) return;
    let _oldFetch = window.fetch;
    window.fetch = function () {
        return _oldFetch.apply(this, arguments)
        .then(res => {
            if (!res.ok) { // True if status is HTTP 2xx
                // 上報錯誤
            }
            return res;
        })
        .catch(error => {
            // 上報錯誤
            throw error;  
        })
}

複製程式碼

統計每個頁面的JS和CSS載入時間

在JS或者CSS載入之前打上時間戳,載入之後打上時間戳,並且將資料上報到後臺。載入時間反映了頁面白屏,可操作的等待時間。

<script>var cssLoadStart = +new Date</script>
<link rel="stylesheet" href="xxx.css" type="text/css" media="all">
<link rel="stylesheet" href="xxx1.css" type="text/css" media="all">
<link rel="stylesheet" href="xxx2.css" type="text/css" media="all">
<sript>
   var cssLoadTime = (+new Date) - cssLoadStart;
   var jsLoadStart = +new Date;
</script>
<script type="text/javascript" src="xx1.js"></script>
<script type="text/javascript" src="xx2.js"></script>
<script type="text/javascript" src="xx3.js"></script>
<script>
   var jsLoadTime = (+new Date) - jsLoadStart;
   var REPORT_URL = 'xxx/cgi?data='
   var img = new Image;
   img.onload = img.onerror = function(){
      img = null;
   };
   img.src = REPORT_URL + cssLoadTime + '-' + jsLoadTime;
</script>

複製程式碼

上報頻率

錯誤資訊頻繁傳送上報請求,會對後端伺服器造成壓力。 專案中我們可通過設定採集率,或對規定時間內資料彙總再上報,減少請求數量,從而緩解服務端壓力。

// 借鑑別人的一個例子
Reporter.send=function(data) {
    // 只採集30%
    if(Math.random() < 0.3) {
        send(data); // 上報錯誤
    }
}
複製程式碼

異常上報後臺伺服器

  1. window.onerror
window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // 構建錯誤物件
    var errorObj = {
        errorMessage: errorMessage || null,
        scriptURI: scriptURI || null,
        lineNo: lineNo || null,
        columnNo: columnNo || null,
        stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
        var xhr = new XMLHttpRequest();

        xhr.open('post', '/middleware/errorMsg', true); // 上報給node中間層處理
        xhr.setRequestHeader('Content-Type', 'application/json'); // 設定請求頭
        xhr.send(JSON.stringify(errorObj)); // 傳送引數
    }
}
複製程式碼
  1. sourceMap解析
const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);

// 定義post介面
router.post('/errorMsg/', function(req, res) {
    let error = req.body; // 獲取前端傳過來的報錯物件
    let url = error.scriptURI; // 壓縮檔案路徑

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map檔案路徑

        // 解析sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一個promise物件

        smc.then(function(result) {

            // 解析原始報錯資料
            let ret = result.originalPositionFor({
                line: error.lineNo, // 壓縮後的行號
                column: error.columnNo // 壓縮後的列號
            });

            let url = ''; // 上報地址

            // 將異常上報至後臺
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // 報錯資訊
                    source: ret.source, // 報錯檔案路徑
                    line: ret.line, // 報錯檔案行號
                    column: ret.column, // 報錯檔案列號
                    stack: error.stack // 報錯堆疊
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) {
                res.json(json);         
            });
        })
    }
});

module.exports = router;
複製程式碼

JS錯誤監控 上報後臺你瞭解多少?

我們何時上報後臺呢?

if (window.requestIdleCallback) {
    window.requestIdleCallback()
} else {
    setTimeout()
}
複製程式碼

體外活

  1. Vue 2.x中我們應該這樣捕獲全域性異常:
Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 異常資訊
        name, // 異常名稱
        script,  // 異常指令碼url
        line,  // 異常行號
        column,  // 異常列號
        stack  // 異常堆疊資訊
    } = err;

    // vm為丟擲異常的 Vue 例項
    // info為 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
}
複製程式碼
  1. React 16.x 版本中引入了 Error Boundary:
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });

        // 將異常資訊上報給伺服器
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return '出錯了';
        }

        return this.props.children;
    }
}
複製程式碼

原文連結蘿蔔

JS監控總結

相關文章