前端異常捕獲上報

2022-01-25 前端學習棧
使用 try...catch

const func = () => {
console.log('fun start')
err
console.log('fun end')
}

try {
func()
} catch (err) {
console.log('err', err)
}

複製代碼

const func = () => {
console.log('fun start')
err
console.log('fun end')
}

try {
setTimeout(() => {
func()
})
} catch (err) {
console.log('err', err)
}
複製代碼

那應該怎麼捕獲異步的錯誤呢?

window.onerror 捕獲異步錯誤

const func = () => {
console.log('fun start')
err
console.log('fun end')
}

setTimeout(() => {
func()
})

window.onerror = (...args) => {
console.log('args:', args)
}
複製代碼

這裡我們可以發先,使用 window.onerror 捕獲到了我們的異步錯誤。

但是,它可以捕獲到所有類型的錯誤嗎?

比如:資源加載地址錯誤?

<img src="//xxsdfsdx.jpg" alt="">

window.onerror = (...args) => {
console.log('args:', args)
}
複製代碼

此時,我們看到該資源地址錯誤沒有被 列印出來,那麼我們該怎麼捕獲這種類型錯誤呢?

window.addEventListener('error)

資源地址錯誤怎麼捕獲?


<img src="/xxx.png" />

window.addEventListener('error', (event) => {
console.log('event err:', event)
}, true) // 第三個參數為 true ,選擇捕獲的方式監聽
複製代碼

promise 怎麼捕獲?

window.addEventListener('unhandledrejection', (err) =>{}) 捕獲

const asyncFunc = () => {
return new Promise((res) => {
err
})
}

try {
asyncFunc()
} catch(e) {
console.log('err:', e)
}
複製代碼

const asyncFunc = () => {
return new Promise((res) => {
err
})
}

asyncFunc()


window.addEventListener('unhandledrejection', (event) => {
console.log('event err:', event)
})
複製代碼

問題:能否使用一個捕獲方式捕獲所有的錯誤?

const asyncFunc = () => {
return new Promise((res) => {
err
})
}

asyncFunc()

// 主動拋出捕獲到的 promise 類型的錯誤
window.addEventListener('unhandledrejection', (event) => {
throw event.reason
})

window.addEventListener('error', (err) => {
console.log('err:', err)
}, true)
複製代碼

小結異常類型同步方法異步方法資源加載Promiseasync / awaittry/catchy


yonerroryy


addEventListener('error')yyy

addEventListener('unhandledrejection')


yy異常上報伺服器

異常上報伺服器主要有2 種方式,一是 動態創建 img 標籤,二是直接使用 ajax 發送請求上報。這裡主要講述第一種方式

動態創建 img 標籤

// 上報錯誤
function uploadError({lineno, colno, error: { stack }, message, filename }) {
console.log('uploadError---', event)
// 整理我們要的錯誤信息
const errorInfo = {
lineno,
colno,
stack,
message,
filename
}
// 錯誤信息序列化後使用 base64 編碼,避免出現特殊字符導致的錯誤
const str = window.btoa(JSON.stringify(errorInfo))

// 創建圖片,使用圖片給錯誤收集的後端伺服器發送一個 get 請求,
// 上傳的信息:錯誤資源,錯誤時間
new Image().src = `http://localhost:7001/monitor/error?info=${str}`
}

window.addEventListener('unhandledrejection', (event) => {
// 再次主動拋出
throw event.reason
})

window.addEventListener('error', (err) => {
console.log('error:', err)
// 上報錯誤
uploadError(err)
})
複製代碼

後端收集錯誤

搭建 eggjs 工程,具體參考 Egg.js官網

npm i egg-init -g

egg-init backend --type=simple

cd backend

npm i

npm run dev
複製代碼

// public/router.js

module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/monitor/error', controller.monitor.index)
};
複製代碼

// app/controller/monitor.js

'use strict';

const Controller = require('egg').Controller;

class MonitorController extends Controller {
async index() {
const { ctx } = this;
const { info } = ctx.query
// Buffer 接受一個 base64 編碼的數據
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('error-info', json)
ctx.body = 'hi, json';
}
}

module.exports = MonitorController;

複製代碼

const info = window.btoa(JSON.stringify({test: 'err'})) // "eyJ0ZXN0IjoiZXJyIn0="

// rest-client 測試接口測試
GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=

// 得到 log 結果:error-info { test: 'err' }
複製代碼

eggjs 記入錯誤日誌

方式:

可以使用 fs 寫入文件進行記錄

也可以使用 log4j 這種成熟的日誌庫

當然,在 eggjs 中是支持我們 自定義日誌 的,那麼我們使用這個功能定製一個前端錯誤日誌就可以了。

config.customLogger = {
frontendLogger: {
file: path.join(appInfo.root, 'logs/frontend.log')
}
}
複製代碼

async index() {
const { ctx } = this;
const { info } = ctx.query
// Buffer 接受一個 base64 編碼的數據
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('error-info', json)
// 寫入日誌
this.ctx.getLogger('frontendLogger').error(json)
ctx.body = 'hi, json';
}
複製代碼

// rest-client 測試
GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=
複製代碼

2021-04-03 11:58:48,543 ERROR 2180 [-/127.0.0.1/-/4ms GET /monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=] { test: 'err' }
複製代碼

Vue項目中異常如何採集

Vue3.x 官網

npm i @vue/cli -g

vue create vue-app

cd vue-app

yarn install

yarn serve
複製代碼

// src/components/HelloWorld.vue

// ... 省略部分代碼
export default {
name: 'HelloWorld',
props: {
msg: String
},
mounted() {
// methods 中沒有定義方法 abc,報錯 error
abc()
}
}
複製代碼

// /vue.config.js

module.exports = {
// close eslint setting
devServer: {
overlay: {
warning: true,
errors: true
}
},
lintOnSave: false
}
複製代碼

// src/main.js

// 在 vue 裡面統一使用這個 方式捕獲錯誤
Vue.config.errorHandler = (err, vm, info) => {
console.log('errHandler:', err)
uploadError(err)
}

function uploadError({ message, stack }) {
console.log('uploadError---')
// 整理我們要的錯誤信息
const errorInfo = {
stack,
message,
}
// 錯誤信息序列化後使用 base64 編碼,避免出現特殊字符導致的錯誤
const str = window.btoa(JSON.stringify(errorInfo))

// 創建圖片,使用圖片給錯誤收集的後端伺服器發送一個 get 請求,
// 上傳的信息:錯誤資源,錯誤時間
new Image().src = `http://localhost:7001/monitor/error?info=${str}`
}

new Vue({
render: h => h(App)
}).$mounted('#app')
複製代碼

yarn build

cd dist

hs
複製代碼

因為打包後的代碼 js 文件主要有 2 種

app.xxx.js
app.xxx.js.map
複製代碼

我們可以看看 .map 文件的內容結構:

{
"version": 3,
"sources": [
"webpack:///webpack/bootstrap",
"webpack:///./src/App.vue",
"webpack:///./src/components/HelloWorld.vue",
"webpack:///./src/components/HelloWorld.vue?354f",
"webpack:///./src/App.vue?eabf",
"webpack:///./src/main.js",
"webpack:///./src/assets/logo.png",
"webpack:///./src/App.vue?7d22"
],
"names": [
"webpackJsonpCallback",
"data",
//...
],
"mappings": "aACE,SAASA,EAAqBC...",
"file": "js/app.9a4488cf.js",
"sourcesContent": [" \t// install a JSONP callback..."],
"sourceRoot": ""
}
複製代碼

主要包含了這些東西:

後面,我們將從 app.xxx.js.map 中進行解析,還原錯誤代碼

sourcemap 上傳插件

編寫一個 UploadSourceMapWebpackPlugin 插件,用於每次打包代碼的時候自動上傳到伺服器指定目錄

// frontend/plugin/uploadSourceMapWebpackPlugin.js

class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')
}
}

module.exports = UploadSourceMapWebpackPlugin
複製代碼

// /vue.config.js
// refer:https://cli.vuejs.org/zh/config/#configurewebpack
const UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebpackPlugin')

module.exports = {
configureWebpack: {
plugins:[
new UploadSourceMapWebpackPlugin({
uploadUrl: 'http://localhost:7001/monitor/sourcemap'
})
]
},
// close eslint setting
devServer: {
overlay: {
warning: true,
errors: true
}
},
lintOnSave: false
}
複製代碼

yarn build

# 此時,我們可以看到命令行中的 log
Building for production...UploadSourceMapWebpackPlugin apply
複製代碼

接下來,完成 UploadSourceMapWebpackPlugin 插件的詳細功能

const path = require('path')
const glob = require('glob')
const fs = require('fs')
const http = require('http')


class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')
// 定義在打包後執行
compiler.hooks.done.tap('UploadSourceMapWebpackPlugin', async status => {
// 讀取 sourceMap 文件
const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
console.log('list', list)
// list [
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/app.d15f69c0.js.map',
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/chunk-vendors.f3b66fea.js.map'
// ]
for (let filename of list) {
await this.upload(this.options.uploadUrl, filename)
}
})

}
upload(url, file) {
return new Promise(resolve => {
console.log('upload Map: ', file)

const req = http.request(`${url}?name=${path.basename(file)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
Connection: 'keep-alive',
'Transfer-Encoding': 'chunked'
}
});
fs.createReadStream(file).on('data', (chunk) => {
req.write(chunk)
}).on('end', () => {
req.end()
resolve()
})
})
}
}

module.exports = UploadSourceMapWebpackPlugin
複製代碼

作用:

在每一次 build done 的時候:

Eggjs 伺服器 sourceMap 上傳接口

'use strict';

/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/monitor/error', controller.monitor.index)
+ router.post('/monitor/sourcemap', controller.monitor.upload)
};

複製代碼

'use strict';

const Controller = require('egg').Controller;
const path = require('path')
const fs = require('fs')
class MonitorController extends Controller {

// ...
async upload() {
const { ctx } = this
// 拿到的是一個 流
const stream = ctx.req
const filename = ctx.query.name
const dir = path.join(this.config.baseDir, 'upload')
// 判斷 upload 是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const target = path.join(dir, filename)
// 創建寫入流寫入信息
console.log('writeFile====', target);
const writeStream = fs.createWriteStream(target)
stream.pipe(writeStream)
}
}

module.exports = MonitorController;

複製代碼

config.security = {
// 可能存在 scrf 風險,這裡設置關閉
csrf: {
enable: false
}
}
複製代碼

yarn build

# egg-server log info
writeFile==== D:\Desktop\err-catch-demo\backend\upload\app.d15f69c0.js.map
writeFile==== D:\Desktop\err-catch-demo\backend\upload\chunk-vendors.f3b66fea.js.map
複製代碼

Stack 解析函數

編寫測試用例:

// /app/utils/stackparser.js


const ErrorStackParser = require('error-stack-parser')
const { SourceMapConsumer } = require('source-map')
const path = require('path')
const fs = require('fs')

module.exports = class StackParser {
constructor() {
this.sourceMapDir = sourceMapDir
this.consumers = {}
}

parseStackTrack(stack, message) {
const error = new Error(message)
error.stack = stack
const stackFrame = ErrorStackParser.parse(error)
return stackFrame
}

async getOriginalErrorStack(stackFrame) {
const origin = []
for (let v of stackFrame) {
origin.push( await this.getOriginPosition(v))
}
return origin
}

// 從 sourceMap 文件讀取錯誤信息
async getOriginPosition(stackFrame) {
let { columnNumber, lineNumber, fileName } = stackFrame
fileName = path.basename(fileName)
// 判斷 consumers 是否存在
let consumer = this.consumers[fileName]
if (!consumer) {
// 讀取 sourceMap
const sourceMapPath = path.resolve(this.sourceMapDir, filename + '.map')
// 判斷文件是否存在
if (!fs.existSync(sourceMapPath)) {
// 不存在則返回源文件
return stackFrame
}
const content = fs.readFileSync(sourceMapPath, 'utf-8')
consumer = await new SourceMapConsumer(content, null)
this.consumers[filename] = consumer
}

const parseData = consumer.originalPositionFor({line: lineNumber, columnNumber})
return parseData

}

}
複製代碼

// 如何通過sourcemap手工還原錯誤具體信息?https://www.zhihu.com/question/285449738
// /app/utils/stackparser.spec.js

const StackParser = require('../stackparser')

const { resolve } = require('path')
const { hasUncaughtExceptionCaptureCallback } = require('process')

const error = {
stack: '',
message: '',
filename: '',
}

it('stackparser-on-the-fly', async () => {
const stackParser = new StackParser(__dirname)
console.log('Stack:', error.stack)
const stackFrame = stackParser.parseStackTrack(error.stack, error)
stackFrame.map(v => {
console.log('stackFrame: ', v)
})

const originStack = await stackParser.getOriginalErrorStack(stackFrame)

console.log('originStack', originStack)

// 斷言,需要手動修改下面的斷言信息
expect(originStack[0]).toMathchObject({
source: 'webpack:///src/index.js',
line: 24,
column: 4,
name: 'xxx'
})
})
複製代碼

轉自:https://juejin.cn/post/6947147474943344647

相關焦點

  • 一篇文章教你如何捕獲前端錯誤
    隨著前端頁面承載功能越來越多,用戶本地瀏覽器環境也錯綜複雜,因此即使有完善的測試,我們也無法保證上線的代碼不會出錯。在這種場景下,前端頁面的監控就成了各個web項目必備的工具。一般對頁面的監控包含頁面性能、頁面錯誤以及用戶行為路徑獲取上報等。而本文將重點關注其中的錯誤部分,主要介紹一下常見的錯誤類型以及如何對它們進行捕獲並上報。
  • 如何優雅處理前端異常?
    前端一直是距離用戶最近的一層,隨著產品的日益完善,我們會更加注重用戶體驗,而前端異常卻如鯁在喉,甚是煩人。一、為什麼要處理異常?異常是不可控的,會影響最終的呈現結果,但是我們有充分的理由去做這樣的事情。
  • 一文徹底搞懂前端監控
    一、前端監控現狀近年來,前端監控是越來越火,目前已經有很多成熟的產品供我們選擇使用二、前端監控的目的 提升用戶體驗 更快的發現發現異常、定位異常、解決異常 了解業務數據,指導產品升級——數據驅動的思想三、前端監控的流程
  • 學習 sentry 源碼架構,打造屬於自己的前端異常監控平臺
    導讀本文通過梳理前端錯誤監控知識、介紹 sentry錯誤監控原理、 sentry初始化、 Ajax上報、 window.onerror、window.onunhandledrejection幾個方面來學習 sentry的源碼。開發微信小程序,想著搭建小程序錯誤監控方案。
  • python異常的捕獲與傳遞
    <5> else咱們應該對else並不陌生,在if中,它的作用是當條件不滿足時執行的實行;同樣在try...except...中也是如此,即如果沒有捕獲到異常,那麼就執行else中的事情try: num = 100print numexcept NameError
  • 你不知道的前端異常處理(萬字長文,建議收藏)
    除了調試,處理異常或許是程式設計師編程時間佔比最高的了。我們天天和各種異常打交道,就好像我們天天和 Bug 打交道一樣。因此正確認識異常,並作出合適的異常處理就顯得很重要了。我們先嘗試拋開前端這個限定條件,來看下更廣泛意義上程序的報錯以及異常處理。不管是什麼語言,都會有異常的發生。
  • WinForm捕獲全局異常(捕獲未處理的異常)
    ,但是,我們要防止不小心出現未知異常,導致軟體崩潰。也可採集系統未知的異常信息,防止出現異常,也無法下手。於是就有了如這篇文章標題所述的一個簡單的需求。代碼實現1、處理未捕獲的異常 static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { string str = ""
  • python中的異常捕獲
    ,比如除數為0的異常,可能初次測試時被除數都不為0,開發者就認為代碼是ok的,但是當處理的項目多了,某一天處理了一個除數為0的事務時,代碼報錯了,此時才意識到代碼存在bug, 這也是為什麼軟體開發不能一步到位,而是不斷迭代升級的原因,只有當代碼處理的項目足夠多的,範圍足夠廣,才能夠發現現有代碼的不足,從而做出改進。
  • Python異常捕獲與處理
    什麼是異常?異常即是一個事件,該事件會在程序執行過程中發生,影響了程序的正常執行。一般情況下,在Python無法正常處理程序時就會發生一個異常。異常是Python對象,表示一個錯誤。當Python腳本發生異常時我們需要捕獲處理它,否則程序會終止執行。當一個未捕獲的異常發生時,Python將結束程序並列印一個堆棧跟蹤信息,以及異常名和附加信息。
  • 基礎 | Java 中的異常捕獲及處理
    表示在編譯階段,Java 編譯器不要求必須進行異常捕獲處理或者拋出聲明,而要到運行期間才能由 JVM 去決定是否要拋出這類異常。使用 try 和 catch 關鍵字可以捕獲異常。try/catch 代碼塊放在異常可能發生的地方。我們一般遇到異常都儘量把異常當場捕獲,因為對於一個應用系統來說,拋出大量異常是有問題的,應該從程序開發角度儘可能的控制異常發生的可能。
  • junit5如何斷言已經被捕獲異常?
    前置:外部方法無法捕獲內部方法中已經被catch的異常(屏蔽catch後又throw異常的場景)junit測試框架場景2是本文重點場景1:如果方法中throw一個異常,沒有catch:public static Integer convertToInt(String str) {
  • php中try catch捕獲異常實例詳解
    具體方法分析如下:php中try catch可以幫助我們捕獲程序代碼的異常了,這樣我們可以很好的處理一些不必要的錯誤了,感興趣的朋友可以一起來看看。PHP中try{}catch{}語句概述PHP5添加了類似於其它語言的異常處理模塊。在 PHP 代碼中所產生的異常可被 throw語句拋出並被 catch 語句捕獲。
  • python學習筆記(9): try...except 異常捕獲
    當無法正確處理程序時就會出現異常。當異常發生時我們需要捕獲處理它,否則程序會終止執行。二、基礎語法捕捉異常可以使用tryexcept語句。如果你不想在異常發生時結束你的程序,只需在try裡捕獲它。finally:    print('不管怎樣,我都會執行')五、捕獲異常的操作1. 使用except而帶多種異常類型有多個expect的時候會首先執行第一個能被捕獲到的異常並且只執行一個。
  • 同樣都是捕獲異常,為啥要不一樣吶?
    」以及如何「處理異常」,如果你對這方面現在還不了解,可以先看一下這兩篇文章:零基礎學習 Python 之錯誤 & 異常零基礎學習 Python 之處理異常後來因為某些原因,我發現在 Python2 和 Python3 中對於「捕獲異常」是有區別的,雖然我一直用的是 Python3,但是還是依然有一部分讀者用的是 Python2,所以我準備再用這一篇文章來寫一下不同版本的 Python 對於捕獲異常的差異,順便再補充一下捕獲多個不同的異常應該如何去做。
  • 跟我學java編程—使用try和catch捕獲異常
    前面了解了Java異常和異常處理類,本節講述如何使用try和catch語句捕獲異常。Java程序在執行過程中如果出現異常,會自動生成一個異常對象,該異常對象將被自動提交給JVM,當JVM接收到異常對象時,會尋找能處理這一異常的代碼,並把當前異常對象交給其處理,這一過程稱為捕獲(catch)異常。如果JVM找不到可以捕獲異常的方法,則運行時系統將終止,相應的Java程序也將退出。
  • 【前端監控】頁面錯誤監控
    如你上面看到的數據,都需要上報上去,產生的跨域問題,就會導致無法捕獲到詳細錯誤。// 上報獲取錯誤信息處理邏輯....除了基礎的上報數據,這裡我們就只需要把 reason 錯誤信息欄位上報上去就行了1、未被catch的 promise 錯誤,不是指 promise 內的執行 錯誤
  • 一種Vue應用程式錯誤/異常處理機制
    這就需要在構建前端應用程式的時候考慮很多,錯誤/異常處理是最重要的方面之一。在應用程式中擁有良好的錯誤處理機制可以帶來很多的好處,如下:良好的錯誤處理機制可以避免應用程式在出現未處理的異常時崩潰在生產環境下,可以輕鬆地存儲或者跟蹤錯誤記錄日誌,以便異常的處理可以統一處理錯誤信息,例如在不破壞應用程式交互的情況下,更改錯誤信息展示UI在前端應用程式中,最常見的錯誤/異常類型可能包括以下幾種:有很多方法可以解決上面的問題,例如使用 eslint 來檢查語法錯誤,使用適當的 try-catch
  • 閒聊前端監控系統
    搭建前端監控系統(二)JS錯誤監控篇如果你是一位前端工程師,那你一定不止一次去解決一些頑固的線上問題,你也曾想方設法復現用戶的bug,結果可能都不太理想。怎樣定位前端線上問題,一直以來,都是很頭疼的問題,因為它發生於用戶的一系列操作之後。
  • 面試官:用一句話描述 JS 異常是否能被 try catch 捕獲到 ?
    之前代碼報錯的時候,線程執行未進入 try catch,那麼無法捕捉異常。比如語法異常(syntaxError),因為語法異常是在語法檢查階段就報錯了,線程執行尚未進入 try catch 代碼塊,自然就無法捕獲到異常。
  • ​if 我是前端團隊 Leader,怎麼制定前端協作規範?
    視團隊情況而定擴展:相關工具9 異常處理、監控和調試規範很多開發者常常誤用或者輕視異常的處理, 合理有效的異常處理可以提高應用的健壯性和可用性,另外還可以幫助開發者快速定位異常.資源:9.2 日誌對於前端來說,日誌也不是毫無意義(很多框架性能優化建議在生產環境移除console)。尤其是在生產現場調試代碼時,這時候可貴的控制臺日誌可以幫助你快速找到異常的線索.