<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          #穩(wěn)定性-異常上報(bào) 前端異常埋點(diǎn)系統(tǒng)初探

          共 18844字,需瀏覽 38分鐘

           ·

          2021-07-06 18:01

          點(diǎn)擊上方關(guān)注 前端技術(shù)江湖,一起學(xué)習(xí),天天進(jìn)步


          前言

          開發(fā)者有時(shí)會(huì)面臨上線的生產(chǎn)環(huán)境包出現(xiàn)了異常?? ,在長期生產(chǎn)bug并修復(fù)bug的循環(huán)中總結(jié)出一下幾個(gè)痛點(diǎn):

          1. 無法快速定位到發(fā)生錯(cuò)誤的代碼位置,因?yàn)槟_手架構(gòu)建時(shí)會(huì)用webapck自動(dòng)幫我們壓縮代碼,而上線版本又通常不會(huì)保留 source map(開源貢獻(xiàn)者除外)
          2. 無法第一時(shí)間通知開發(fā)人員異常發(fā)生
          3. 不知道用戶OS與瀏覽器版本、請(qǐng)求參數(shù)(如頁面ID);而對(duì)于頁面邏輯是否錯(cuò)誤問題,通常除了用戶OS與瀏覽器版本外,需要的是報(bào)錯(cuò)的堆棧信息及具體報(bào)錯(cuò)位置。

          錯(cuò)誤埋點(diǎn)追蹤系統(tǒng)的出現(xiàn)就是為了應(yīng)對(duì)上述問題的解決方案,筆者正好最近接觸了不少前端埋點(diǎn)與錯(cuò)誤處理的博客內(nèi)容,按例階段性產(chǎn)出博客總結(jié)一下。

          什么是埋點(diǎn)

          還不了解的同學(xué)可以閱讀以下文章:

          前端-埋點(diǎn)-理念-通識(shí)-淺談

          大數(shù)據(jù)時(shí)代數(shù)據(jù)的重要性不言而喻,而其中最重要的就是用戶信息的采集。埋點(diǎn),無論是項(xiàng)目后期的復(fù)盤,還是明確業(yè)務(wù)價(jià)值,還是產(chǎn)品價(jià)值的挖掘,都具備很重要的意義。

          前端異常捕獲

          在ES3之前js代碼執(zhí)行的過程中,一旦出現(xiàn)錯(cuò)誤,整個(gè)js代碼都會(huì)停止執(zhí)行,這樣就顯的代碼非常的不健壯。從ES3開始,js也提供了類似的異常處理機(jī)制,從而讓js代碼變的更健壯,程序執(zhí)行的過程中出現(xiàn)了異常,也可以讓程序具有了一部分的異常恢復(fù)能力。js異常的特點(diǎn)是,出現(xiàn)不會(huì)導(dǎo)致JS引擎崩潰,最多只會(huì)終止當(dāng)前執(zhí)行的任務(wù)。

          回歸正題,我們?cè)撊绾卧诔绦虍惓0l(fā)生時(shí)捕捉并進(jìn)行對(duì)應(yīng)的處理呢?在Javascript中,我們通常有以下兩種異常捕獲機(jī)制。

          基本的try…catch語句

            function errFunc() {
                // eslint-disable-next-line no-undef
                error;
            }

            function catchError() {
                try {
                    this.errFunc();
                } catch (error) {
                    console.log(error);
                }
            }
            catchError()
          復(fù)制代碼

          能捕捉到的異常,必須是線程執(zhí)行已經(jīng)進(jìn)入 try catch 但 try catch 未執(zhí)行完的時(shí)候拋出來的,以下都是無法被捕獲到的情形。

          1. 異步任務(wù)拋出的異常(執(zhí)行時(shí)try catch已經(jīng)從執(zhí)行完了)
          2. promise(異常內(nèi)部捕獲到了,并未往上拋異常,使用catch處理)
          3. 語法錯(cuò)誤(代碼運(yùn)行前,在編譯時(shí)就檢查出來了的錯(cuò)誤)
          • 優(yōu)點(diǎn):能夠較好地進(jìn)行異常捕獲,不至于使得頁面由于一處錯(cuò)誤掛掉
          • 缺點(diǎn):顯得過于臃腫,大多代碼使用try ... catch包裹,影響代碼可讀性。

          面試官:請(qǐng)用一句話描述 try catch 能捕獲到哪些 JS 異常

          全局異常監(jiān)聽window.onerror

          window.onerror 最大的好處就是同步任務(wù)、異步任務(wù)都可捕獲,可以得到具體的異常信息、異常文件的URL、異常的行號(hào)與列號(hào)及異常的堆棧信息,再捕獲異常后,統(tǒng)一上報(bào)至我們的日志服務(wù)器,而且可以全局監(jiān)聽,代碼看起來也簡潔很多。

          • 缺點(diǎn):
          1. 此方法有一定的瀏覽器兼容性
          2. 跨域腳本無法準(zhǔn)確捕獲異常,跨域之后window.onerror捕獲不到正確的異常信息,而是統(tǒng)一返回一個(gè)Script error,可通過在<script>使用crossorigin屬性來規(guī)避這個(gè)問題
          image.png
          window.addEventListener('error'function() {
            console.log(error);
            // ...
            // 異常上報(bào)
          });
          throw new Error('這是一個(gè)錯(cuò)誤');
          復(fù)制代碼

          Promise內(nèi)部異常

          前文已經(jīng)提到,onerror 以及 try-catch 也無法捕獲Promise實(shí)例拋出的異常,只能最后在 catch 函數(shù)上處理,但是代碼寫多了就容易糊涂,忘記寫 catch。

          如果你的應(yīng)用用到很多的 Promise 實(shí)例的話,特別是在一些基于 promise 的異步庫比如 axios 等一定要小心,因?yàn)槟悴恢朗裁磿r(shí)候這些異步請(qǐng)求會(huì)拋出異常而你并沒有處理它,所以最好添加一個(gè) Promise 全局異常捕獲事件 unhandledrejection

          window.addEventListener("unhandledrejection", e => {
           console.log('unhandledrejection',e)
          });
          復(fù)制代碼

          vue工程異常

          window.onerror并不能捕獲.vue文件發(fā)生的獲取,Vue 2.2.0以上的版本中增加了一個(gè)errorHandle,使用Vue.config.errorHandler這樣的Vue全局配置,可以在Vue指定組件的渲染和觀察期間未捕獲錯(cuò)誤的處理函數(shù)。這個(gè)處理函數(shù)被調(diào)用時(shí),可獲取錯(cuò)誤信息和Vue 實(shí)例。

          //main.js
          import { createApp } from "vue";
          import App from "./App.vue";

          let app = createApp(App);
          app.config.errorHandler = function(e{
            console.log(e);
            //錯(cuò)誤上報(bào)...
          };
          app.mount("#app");
          復(fù)制代碼

          Vue項(xiàng)目JS腳本錯(cuò)誤捕獲

          綜上,可以將幾種方式有效結(jié)合起來,筆者這里是在vue-cli框架中做的處理,其余類似:

          import { createApp } from "vue";
          import App from "./App.vue";

          let app = createApp(App);

          window.addEventListener(
            "error",
            (e) => {
              console.log(e);
              //TODO:上報(bào)邏輯
              return true;
            },
            true
          );
          // 處理未捕獲的異常,主要是promise內(nèi)部異常,統(tǒng)一拋給 onerror
          window.addEventListener("unhandledrejection", (e) => {
            throw e.reason;
          });
          // 框架異常統(tǒng)一捕獲
          app.config.errorHandler = function(err, vm, info{
            //TODO:上報(bào)邏輯
            console.log(err, vm, info);
          };
          app.mount("#app");

          復(fù)制代碼

          sourcemap

          生產(chǎn)環(huán)境下所有的報(bào)錯(cuò)的代碼行數(shù)都在第一行了,為什么呢?

          通常在該環(huán)境下的代碼是經(jīng)過webpack打包后壓縮混淆的代碼,否則源代碼泄漏易造成安全問題,在生產(chǎn)環(huán)境下,我們的代碼被壓縮成了一行。而保留了sourcemap文件就可以利用webpack打包后的生成的一份.map的腳本文件就可以讓瀏覽器對(duì)錯(cuò)誤位置進(jìn)行追蹤了,但這種做法并不可取,更為推薦的是在服務(wù)端使用Node.js對(duì)接收到的日志信息時(shí)使用source-map解析,以避免源代碼的泄露造成風(fēng)險(xiǎn)

          image.png

          vue.config.js配置里通過屬性productionSourceMap: true可以控制webpack是否生成map文件

          webpack自定義插件實(shí)現(xiàn)sourcemap自動(dòng)上傳

          為了我們每一次構(gòu)建服務(wù)端能拿到最新的map文件,我們編寫一個(gè)插件讓webpack在打包完成后觸發(fā)一個(gè)鉤子實(shí)現(xiàn)文件上傳,在vue.config.js中進(jìn)行配置

          調(diào)整 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) => {},
          }
          復(fù)制代碼
          //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));
                  }
                });
              }
            }
            
            //調(diào)用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, { endfalse });
                fileStream.on("end"function() {
                  req.end();
                  resolve();
                });
              });
            }
          }
          module.exports = SourceMapUploader;

          復(fù)制代碼

          錯(cuò)誤上報(bào)

          兩種方式:

          1. img標(biāo)簽 這種方式無需加載任何通訊庫,而且頁面是無需刷新的,相當(dāng)于get請(qǐng)求,沒有跨域問題。缺點(diǎn)是有url長度限制,但一般來講足夠使用了。
          2. ajax 與正常的接口請(qǐng)求無異,可以用post

          這里采用第一種,通過動(dòng)態(tài)創(chuàng)建一個(gè)img,瀏覽器就會(huì)向服務(wù)器發(fā)送get請(qǐng)求。將需要上報(bào)的錯(cuò)誤數(shù)據(jù)放在url中,利用這種方式就可以將錯(cuò)誤上報(bào)到服務(wù)器了。

          確定上報(bào)的內(nèi)容,應(yīng)該包含異常位置(行號(hào),列號(hào)),異常信息,在錯(cuò)誤堆棧中包含了絕大多數(shù)調(diào)試有關(guān)的信息,我們通訊的時(shí)候只能以字符串方式傳輸,我們需要將對(duì)象進(jìn)行序列化處理。

          1. 將異常數(shù)據(jù)從屬性中解構(gòu)出來,存入一個(gè)JSON對(duì)象
          2. 將JSON對(duì)象轉(zhuǎn)換為字符串
          3. 將字符串轉(zhuǎn)換為Base64

          后端接收到信息后進(jìn)行對(duì)應(yīng)的反向操作,就可以在日志中記錄。

          1621581164(1).png
          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}`;
          }
          復(fù)制代碼

          后端服務(wù)

          用koa搭一個(gè)簡單后臺(tái)服務(wù),代碼比較簡單,按功能拆開來講

          上傳文件接口

          文件流寫入:

          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);
          });
          復(fù)制代碼

          錯(cuò)誤日志

          使用log4js記錄我們的錯(cuò)誤日志,這個(gè)也是非常流行的日志插件了,直接貼代碼。

          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'

                alwaysIncludePatterntrue
              },
              error: {// 錯(cuò)誤日志
                type'dateFile',
                filename: path.join(__dirname, 'logs''error''error'),
                pattern'yyyy-MM-dd.log',
                encoding'utf-8'
                alwaysIncludePatterntrue
              }
            },
            categories: {
              default: { appenders: ['info'], level'info' },
              info: { appenders: ['info'], level'info' },
              error: { appenders: ['error'], level'error' }
            }
          });


          /**
           * 錯(cuò)誤日志記錄方式
           * @param {*} content 日志輸出內(nèi)容
           */

          function logError(content{
            const log = log4js.getLogger("error");
            log.error(content)
          }
          /**
           * 日志記錄方式
           * @param {*} content 日志輸出內(nèi)容
           */

          function logInfo(content{
            const log = log4js.getLogger("info");
            log.info(content)
          }

          module.exports = {
            logError,
            logInfo
          }
          復(fù)制代碼

          錯(cuò)誤解析

          這個(gè)接口就是對(duì)上報(bào)的錯(cuò)誤信息進(jìn)行解析,得到錯(cuò)誤堆棧對(duì)象 上面我們已經(jīng)拿到colno為2319,lineno為1,接下來需要安裝一個(gè)插件幫助我們找到對(duì)應(yīng)壓縮前的代碼位置。

          npm install source-map -S
          復(fù)制代碼

          先讀取對(duì)應(yīng)的map文件(按filename對(duì)應(yīng)),然后只需傳入壓縮后的報(bào)錯(cuò)行號(hào)列號(hào)即可,就會(huì)返回壓縮前的錯(cuò)誤信息。打個(gè)比喻:簡單地說相當(dāng)于一本書的目錄,我們根據(jù)目錄可以快速找到某一部分內(nèi)容的頁數(shù)

          router.get("/error"async (ctx) => {
            const errInfo = ctx.query.info;
            // 轉(zhuǎn)碼 反序列化
            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")
            );
            // 解析原始報(bào)錯(cuò)數(shù)據(jù)
            let result = consumer.originalPositionFor({
              line: obj.lineno, // 壓縮后的行號(hào)
              column: obj.colno, // 壓縮后的列號(hào)
            });
            // 寫入到日志中
            obj.lineno = result.line;
            obj.colno = result.column;
            log4js.logError(JSON.stringify(obj));
            ctx.body = "";
          });
          復(fù)制代碼
          image.png

          數(shù)據(jù)存儲(chǔ) 日志可視化

          ELK前端日志分析

          www.cnblogs.com/xiao9873341…

          看了一下許多平臺(tái)對(duì)錯(cuò)誤日志的分析和可視化都使用了ELK,ELK在服務(wù)器運(yùn)維界應(yīng)該是運(yùn)用的非常成熟了,很多成熟的大型項(xiàng)目都使用ELK來作為前端日志監(jiān)控、分析的工具。我對(duì)運(yùn)維這一塊興趣不大,有興趣的可以自行搭建,整出來界面還是挺炫酷的。

          而我又不想每一次都跑去服務(wù)器查看日志,于是想到了可以建個(gè)表來把錯(cuò)誤信息給存起來。用起老三樣koa+mongodb+vue,我們這項(xiàng)目就算是齊活了。(mongodb,yyds??,省去了建表許多功夫)

          npm install mongodb --save
          復(fù)制代碼

          新建一個(gè)文件db.js封裝一下mongo連接,方便復(fù)用:

          // db.js
          const MongoClient = require("mongodb").MongoClient;
          const url = "mongodb://localhost:27017/";
          const dbName = "err_db";
          const collectionName = "errList";
          class Db {
            // 單例模式,解決多次實(shí)例化時(shí)候每次創(chuàng)建連接對(duì)象不共享的問題,實(shí)現(xiàn)共享連接數(shù)據(jù)庫狀態(tài)
            static getInstance() {
              if (!Db.instance) {
                Db.instance = new Db();
              }
              return Db.instance;
            }
            constructor() {
              // 屬性 存放db對(duì)象
              this.dbClient = "";
              // 實(shí)例化的時(shí)候就連接數(shù)據(jù)庫,增加連接數(shù)據(jù)庫速度
              this.connect();
            }
            // 連接數(shù)據(jù)庫
            connect() {
              return new Promise((resolve, reject) => {
                // 解決數(shù)據(jù)庫多次連接的問題,要不然每次操作數(shù)據(jù)都會(huì)進(jìn)行一次連接數(shù)據(jù)庫的操作,比較慢
                if (!this.dbClient) {
                  // 第一次的時(shí)候連接數(shù)據(jù)庫
                  MongoClient.connect(
                    url,
                    { useNewUrlParsertrueuseUnifiedTopologytrue },
                    (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();
          復(fù)制代碼

          然后就可以在項(xiàng)目中愉快使用

           
            let db = require("./db");
            ...
            log4js.logError(JSON.stringify(obj));
            //插入數(shù)據(jù)
            await db.insert(obj);
            ctx.body = "";
          復(fù)制代碼

          數(shù)據(jù)插入成功??

          增加一個(gè)查詢接口:

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

          為了豐富錯(cuò)誤信息,我們還可以在上報(bào)的時(shí)候增加報(bào)錯(cuò)時(shí)間,用戶瀏覽器信息,自定義錯(cuò)誤類型統(tǒng)計(jì),引入圖表可視化展示,更加直觀地追蹤

          image.png

          待完善的點(diǎn)

          1. 應(yīng)該做錯(cuò)誤類型區(qū)分,如業(yè)務(wù)錯(cuò)誤與接口錯(cuò)誤等
          2. 過多的日志在業(yè)務(wù)服務(wù)器堆積,造成業(yè)務(wù)服務(wù)器的存儲(chǔ)空間不夠的情況,在遷到mongodb后在考慮不要日志??
          3. 上報(bào)頻率做限制。如類似mouseover事件中的報(bào)錯(cuò)應(yīng)該考慮防抖般的處理

          后記

          至此,我們總結(jié)了幾種異常捕獲的做法,并完成了對(duì)前端程序異常的上報(bào)功能,這對(duì)開發(fā)和測試人員都有較大的意義,用一句或說便是,要對(duì)產(chǎn)品保持敬畏之心,時(shí)刻關(guān)注存在的缺陷問題。代碼中有疑問或者不對(duì)的地方歡迎各位批評(píng)指正,共同進(jìn)步。求點(diǎn)贊三連QAQ????

          參考鏈接:

          從0到1,Vue大牛的前端搭建——異常監(jiān)控系統(tǒng)

          • 本文作者:violetrosez

          • 本文鏈接:https://juejin.cn/post/6965022635470110733

          The End

          歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),記得點(diǎn)個(gè) 「在看」

          點(diǎn)個(gè)『在看』支持下 

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色一级电影视频 | 成人精品视频在线观看 | 538精品在线视频 | 美女日B视频 | 亚州视频在线观看 |