前端異常埋點系統(tǒng)初探
微信搜索 逆鋒起筆關注后回復編程pdf
領取編程大佬們所推薦的 23 種編程資料!
無法快速定位到發(fā)生錯誤的代碼位置,因為腳手架構建時會用webapck自動幫我們壓縮代碼,而上線版本又通常不會保留 source map(開源貢獻者除外)無法第一時間通知開發(fā)人員異常發(fā)生 不知道用戶OS與瀏覽器版本、請求參數(shù)(如頁面ID);而對于頁面邏輯是否錯誤問題,通常除了用戶OS與瀏覽器版本外,需要的是報錯的堆棧信息及具體報錯位置。
什么是埋點
前端-埋點-理念-通識-淺談
前端異常捕獲
基本的try…catch語句
function errFunc() {
// eslint-disable-next-line no-undef
error;
}
function catchError() {
try {
this.errFunc();
} catch (error) {
console.log(error);
}
}
catchError()
復制代碼
異步任務拋出的異常(執(zhí)行時try catch已經(jīng)從執(zhí)行完了) promise(異常內部捕獲到了,并未往上拋異常,使用catch處理) 語法錯誤(代碼運行前,在編譯時就檢查出來了的錯誤)
優(yōu)點:能夠較好地進行異常捕獲,不至于使得頁面由于一處錯誤掛掉 缺點:顯得過于臃腫,大多代碼使用 try ... catch包裹,影響代碼可讀性。
面試官:請用一句話描述 try catch 能捕獲到哪些 JS 異常
全局異常監(jiān)聽window.onerror
window.onerror 最大的好處就是同步任務、異步任務都可捕獲,可以得到具體的異常信息、異常文件的URL、異常的行號與列號及異常的堆棧信息,再捕獲異常后,統(tǒng)一上報至我們的日志服務器,而且可以全局監(jiān)聽,代碼看起來也簡潔很多。缺點:
此方法有一定的瀏覽器兼容性 跨域腳本無法準確捕獲異常,跨域之后 window.onerror捕獲不到正確的異常信息,而是統(tǒng)一返回一個Script error,可通過在<script>使用crossorigin屬性來規(guī)避這個問題

window.addEventListener('error', function() {
console.log(error);
// ...
// 異常上報
});
throw new Error('這是一個錯誤');
復制代碼
Promise內部異常
onerror 以及 try-catch 也無法捕獲Promise實例拋出的異常,只能最后在 catch 函數(shù)上處理,但是代碼寫多了就容易糊涂,忘記寫 catch。unhandledrejection。window.addEventListener("unhandledrejection", e => {
console.log('unhandledrejection',e)
});
復制代碼
vue工程異常
window.onerror并不能捕獲.vue文件發(fā)生的獲取,Vue 2.2.0以上的版本中增加了一個errorHandle,使用Vue.config.errorHandler這樣的Vue全局配置,可以在Vue指定組件的渲染和觀察期間未捕獲錯誤的處理函數(shù)。這個處理函數(shù)被調用時,可獲取錯誤信息和Vue 實例。//main.js
import { createApp } from "vue";
import App from "./App.vue";
let app = createApp(App);
app.config.errorHandler = function(e) {
console.log(e);
//錯誤上報...
};
app.mount("#app");
復制代碼
Vue項目JS腳本錯誤捕獲
import { createApp } from "vue";
import App from "./App.vue";
let app = createApp(App);
window.addEventListener(
"error",
(e) => {
console.log(e);
//TODO:上報邏輯
return true;
},
true
);
// 處理未捕獲的異常,主要是promise內部異常,統(tǒng)一拋給 onerror
window.addEventListener("unhandledrejection", (e) => {
throw e.reason;
});
// 框架異常統(tǒng)一捕獲
app.config.errorHandler = function(err, vm, info) {
//TODO:上報邏輯
console.log(err, vm, info);
};
app.mount("#app");
復制代碼
sourcemap


vue.config.js配置里通過屬性productionSourceMap: true可以控制webpack是否生成map文件
webpack自定義插件實現(xiàn)sourcemap自動上傳
vue.config.js中進行配置調整 webpack 配置
//vue.config.js
let SourceMapUploader = require("./source-map-upload");
module.exports = {
configureWebpack: {
resolve: {
alias: {
"@": resolve("src"),
},
},
plugins: [
new SourceMapUploader({url: "http://localhost:3000/upload"})
],
}
// chainWebpack: (config) => {},
}
復制代碼
//source-map-upload.js
const fs = require("fs");
const http = require("http");
const path = require("path");
class SourceMapUploader {
constructor(options) {
this.options = options;
}
/**
* 用到了hooks,done表示在打包完成之后
* status.compilation.outputOptions就是打包的dist文件
*/
apply(compiler) {
if (process.env.NODE_ENV == "production") {
compiler.hooks.done.tap("sourcemap-uploader", async (status) => {
// console.log(status.compilation.outputOptions.path);
// 讀取目錄下的map后綴的文件
let dir = path.join(status.compilation.outputOptions.path, "/js/");
let chunks = fs.readdirSync(dir);
let map_file = chunks.filter((item) => {
return item.match(/\.js\.map$/) !== null;
});
// 上傳sourcemap
while (map_file.length > 0) {
let file = map_file.shift();
await this.upload(this.options.url, path.join(dir, file));
}
});
}
}
//調用upload接口,上傳文件
upload(url, file) {
return new Promise((resolve) => {
let req = http.request(`${url}?name=${path.basename(file)}`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Connection: "keep-alive",
},
});
let fileStream = fs.createReadStream(file);
fileStream.pipe(req, { end: false });
fileStream.on("end", function() {
req.end();
resolve();
});
});
}
}
module.exports = SourceMapUploader;
復制代碼
錯誤上報
img標簽 這種方式無需加載任何通訊庫,而且頁面是無需刷新的,相當于get請求,沒有跨域問題。缺點是有url長度限制,但一般來講足夠使用了。 ajax 與正常的接口請求無異,可以用post
將異常數(shù)據(jù)從屬性中解構出來,存入一個JSON對象 將JSON對象轉換為字符串 將字符串轉換為Base64

function uploadErr({ lineno, colno, error: { stack }, message, filename }) {
let str = window.btoa(
JSON.stringify({
lineno,
colno,
error: { stack },
message,
filename,
})
);
let front_ip = "http://localhost:3000/error";
new Image().src = `${front_ip}?info=${str}`;
}
復制代碼
后端服務
上傳文件接口
router.post("/upload", async (ctx) => {
const stream = ctx.req;
const filename = ctx.query.name;
let dir = path.join(__dirname, "source-map");
//判斷source文件夾是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
let target = path.join(dir, filename);
const ws = fs.createWriteStream(target);
stream.pipe(ws);
});
復制代碼
錯誤日志
log4js記錄我們的錯誤日志,這個也是非常流行的日志插件了,直接貼代碼。log4js-node
const path = require('path')
const log4js = require('log4js');
log4js.configure({
appenders: {
info: {
type: "dateFile",
filename: path.join(__dirname, 'logs', 'info', 'info'),
pattern: "yyyy-MM-dd.log",
encoding: 'utf-8',
alwaysIncludePattern: true,
},
error: {// 錯誤日志
type: 'dateFile',
filename: path.join(__dirname, 'logs', 'error', 'error'),
pattern: 'yyyy-MM-dd.log',
encoding: 'utf-8',
alwaysIncludePattern: true
}
},
categories: {
default: { appenders: ['info'], level: 'info' },
info: { appenders: ['info'], level: 'info' },
error: { appenders: ['error'], level: 'error' }
}
});
/**
* 錯誤日志記錄方式
* @param {*} content 日志輸出內容
*/
function logError(content) {
const log = log4js.getLogger("error");
log.error(content)
}
/**
* 日志記錄方式
* @param {*} content 日志輸出內容
*/
function logInfo(content) {
const log = log4js.getLogger("info");
log.info(content)
}
module.exports = {
logError,
logInfo
}
復制代碼
錯誤解析

npm install source-map -S
復制代碼
router.get("/error", async (ctx) => {
const errInfo = ctx.query.info;
// 轉碼 反序列化
let obj = JSON.parse(Buffer.from(errInfo, "base64").toString("utf-8"));
let fileUrl = obj.filename.split("/").pop() + ".map"; // map文件路徑
// 解析sourceMap
// 1.sourcemap文件的文件流,我們已經(jīng)上傳
// 2.文件編碼格式
let consumer = await new sourceMap.SourceMapConsumer(
fs.readFileSync(path.join(__dirname, "source-map/" + fileUrl), "utf8")
);
// 解析原始報錯數(shù)據(jù)
let result = consumer.originalPositionFor({
line: obj.lineno, // 壓縮后的行號
column: obj.colno, // 壓縮后的列號
});
// 寫入到日志中
obj.lineno = result.line;
obj.colno = result.column;
log4js.logError(JSON.stringify(obj));
ctx.body = "";
});
復制代碼

數(shù)據(jù)存儲 日志可視化
ELK前端日志分析
www.cnblogs.com/xiao9873341…
npm install mongodb --save
復制代碼
// db.js
const MongoClient = require("mongodb").MongoClient;
const url = "mongodb://localhost:27017/";
const dbName = "err_db";
const collectionName = "errList";
class Db {
// 單例模式,解決多次實例化時候每次創(chuàng)建連接對象不共享的問題,實現(xiàn)共享連接數(shù)據(jù)庫狀態(tài)
static getInstance() {
if (!Db.instance) {
Db.instance = new Db();
}
return Db.instance;
}
constructor() {
// 屬性 存放db對象
this.dbClient = "";
// 實例化的時候就連接數(shù)據(jù)庫,增加連接數(shù)據(jù)庫速度
this.connect();
}
// 連接數(shù)據(jù)庫
connect() {
return new Promise((resolve, reject) => {
// 解決數(shù)據(jù)庫多次連接的問題,要不然每次操作數(shù)據(jù)都會進行一次連接數(shù)據(jù)庫的操作,比較慢
if (!this.dbClient) {
// 第一次的時候連接數(shù)據(jù)庫
MongoClient.connect(
url,
{ useNewUrlParser: true, useUnifiedTopology: true },
(err, client) => {
if (err) {
reject(err);
} else {
// 將連接數(shù)據(jù)庫的狀態(tài)賦值給屬性,保持長連接狀態(tài)
this.dbClient = client.db(dbName);
resolve(this.dbClient);
}
}
);
} else {
// 第二次之后直接返回dbClient
resolve(this.dbClient);
}
});
}
// 增加一條數(shù)據(jù)
insert(json) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
db.collection(collectionName).insertOne(json, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
});
}
//查詢 --
find(query = {}) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
let res = db.collection(collectionName).find(query);
res.toArray((e, docs) => {
if (e) {
reject(e);
return;
}
resolve(docs);
});
});
});
}
}
module.exports = Db.getInstance();
復制代碼
let db = require("./db");
...
log4js.logError(JSON.stringify(obj));
//插入數(shù)據(jù)
await db.insert(obj);
ctx.body = "";
復制代碼

router.get("/errlist", async (ctx) => {
let res = await db.find({});
ctx.body = {
data: res,
};
});
復制代碼

待完善的點
應該做錯誤類型區(qū)分,如業(yè)務錯誤與接口錯誤等 過多的日志在業(yè)務服務器堆積,造成業(yè)務服務器的存儲空間不夠的情況,在遷到mongodb后在考慮不要日志?? 上報頻率做限制。如類似mouseover事件中的報錯應該考慮防抖般的處理
后記
逆鋒起筆是一個專注于程序員圈子的技術平臺,你可以收獲最新技術動態(tài)、最新內測資格、BAT等大廠大佬的經(jīng)驗、增長自身、學習資料、職業(yè)路線、賺錢思維,微信搜索逆鋒起筆關注!
從0到1,Vue大牛的前端搭建——異常監(jiān)控系統(tǒng)
作者:violetrosez
https://juejin.cn/post/6965022635470110733
支持下
評論
圖片
表情

