<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>

          前端異常埋點系統(tǒng)初探

          共 18933字,需瀏覽 38分鐘

           ·

          2021-06-24 16:55

          微信搜索逆鋒起筆關注后回復編程pdf
          領取編程大佬們所推薦的 23 種編程資料!

          前言
          開發(fā)者有時會面臨上線的生產(chǎn)環(huán)境包出現(xiàn)了異常?? ,在長期生產(chǎn)bug并修復bug的循環(huán)中總結出一下幾個痛點:
          1. 無法快速定位到發(fā)生錯誤的代碼位置,因為腳手架構建時會用webapck自動幫我們壓縮代碼,而上線版本又通常不會保留 source map(開源貢獻者除外)
          2. 無法第一時間通知開發(fā)人員異常發(fā)生
          3. 不知道用戶OS與瀏覽器版本、請求參數(shù)(如頁面ID);而對于頁面邏輯是否錯誤問題,通常除了用戶OS與瀏覽器版本外,需要的是報錯的堆棧信息及具體報錯位置。
          錯誤埋點追蹤系統(tǒng)的出現(xiàn)就是為了應對上述問題的解決方案,筆者正好最近接觸了不少前端埋點與錯誤處理的博客內容,按例階段性產(chǎn)出博客總結一下。

          什么是埋點

          還不了解的同學可以閱讀以下文章:
          前端-埋點-理念-通識-淺談
          大數(shù)據(jù)時代數(shù)據(jù)的重要性不言而喻,而其中最重要的就是用戶信息的采集。埋點,無論是項目后期的復盤,還是明確業(yè)務價值,還是產(chǎn)品價值的挖掘,都具備很重要的意義。

          前端異常捕獲

          在ES3之前js代碼執(zhí)行的過程中,一旦出現(xiàn)錯誤,整個js代碼都會停止執(zhí)行,這樣就顯的代碼非常的不健壯。從ES3開始,js也提供了類似的異常處理機制,從而讓js代碼變的更健壯,程序執(zhí)行的過程中出現(xiàn)了異常,也可以讓程序具有了一部分的異?;謴湍芰Α?strong style="color: black;">js異常的特點是,出現(xiàn)不會導致JS引擎崩潰,最多只會終止當前執(zhí)行的任務。
          回歸正題,我們該如何在程序異常發(fā)生時捕捉并進行對應的處理呢?在Javascript中,我們通常有以下兩種異常捕獲機制。

          基本的try…catch語句

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

            function catchError() {
                try {
                    this.errFunc();
                } catch (error) {
                    console.log(error);
                }
            }
            catchError()
          復制代碼
          能捕捉到的異常,必須是線程執(zhí)行已經(jīng)進入 try catch 但 try catch 未執(zhí)行完的時候拋出來的,以下都是無法被捕獲到的情形。
          1. 異步任務拋出的異常(執(zhí)行時try catch已經(jīng)從執(zhí)行完了)
          2. promise(異常內部捕獲到了,并未往上拋異常,使用catch處理)
          3. 語法錯誤(代碼運行前,在編譯時就檢查出來了的錯誤)
          • 優(yōu)點:能夠較好地進行異常捕獲,不至于使得頁面由于一處錯誤掛掉
          • 缺點:顯得過于臃腫,大多代碼使用try ... catch包裹,影響代碼可讀性。
          面試官:請用一句話描述 try catch 能捕獲到哪些 JS 異常

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

          window.onerror 最大的好處就是同步任務、異步任務都可捕獲,可以得到具體的異常信息、異常文件的URL、異常的行號與列號及異常的堆棧信息,再捕獲異常后,統(tǒng)一上報至我們的日志服務器,而且可以全局監(jiān)聽,代碼看起來也簡潔很多。
          • 缺點:
          1. 此方法有一定的瀏覽器兼容性
          2. 跨域腳本無法準確捕獲異常,跨域之后window.onerror捕獲不到正確的異常信息,而是統(tǒng)一返回一個Script error,可通過在<script>使用crossorigin屬性來規(guī)避這個問題
          image.png
          window.addEventListener('error'function() {
            console.log(error);
            // ...
            // 異常上報
          });
          throw new Error('這是一個錯誤');
          復制代碼

          Promise內部異常

          前文已經(jīng)提到,onerror 以及 try-catch 也無法捕獲Promise實例拋出的異常,只能最后在 catch 函數(shù)上處理,但是代碼寫多了就容易糊涂,忘記寫 catch。
          如果你的應用用到很多的 Promise 實例的話,特別是在一些基于 promise 的異步庫比如 axios 等一定要小心,因為你不知道什么時候這些異步請求會拋出異常而你并沒有處理它,所以最好添加一個 Promise 全局異常捕獲事件 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腳本錯誤捕獲
          綜上,可以將幾種方式有效結合起來,筆者這里是在vue-cli框架中做的處理,其余類似:
          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

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

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

          webpack自定義插件實現(xiàn)sourcemap自動上傳

          為了我們每一次構建服務端能拿到最新的map文件,我們編寫一個插件讓webpack在打包完成后觸發(fā)一個鉤子實現(xiàn)文件上傳,在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, { endfalse });
                fileStream.on("end"function() {
                  req.end();
                  resolve();
                });
              });
            }
          }
          module.exports = SourceMapUploader;

          復制代碼

          錯誤上報

          兩種方式:
          1. img標簽 這種方式無需加載任何通訊庫,而且頁面是無需刷新的,相當于get請求,沒有跨域問題。缺點是有url長度限制,但一般來講足夠使用了。
          2. ajax 與正常的接口請求無異,可以用post
          這里采用第一種,通過動態(tài)創(chuàng)建一個img,瀏覽器就會向服務器發(fā)送get請求。將需要上報的錯誤數(shù)據(jù)放在url中,利用這種方式就可以將錯誤上報到服務器了。
          確定上報的內容,應該包含異常位置(行號,列號),異常信息,在錯誤堆棧中包含了絕大多數(shù)調試有關的信息,我們通訊的時候只能以字符串方式傳輸,我們需要將對象進行序列化處理。
          1. 將異常數(shù)據(jù)從屬性中解構出來,存入一個JSON對象
          2. 將JSON對象轉換為字符串
          3. 將字符串轉換為Base64
          后端接收到信息后進行對應的反向操作,就可以在日志中記錄。
          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}`;
          }
          復制代碼

          后端服務

          用koa搭一個簡單后臺服務,代碼比較簡單,按功能拆開來講

          上傳文件接口

          文件流寫入:
          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'

                alwaysIncludePatterntrue
              },
              error: {// 錯誤日志
                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' }
            }
          });


          /**
           * 錯誤日志記錄方式
           * @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
          }
          復制代碼

          錯誤解析

          這個接口就是對上報的錯誤信息進行解析,得到錯誤堆棧對象

          上面我們已經(jīng)拿到colno為2319,lineno為1,接下來需要安裝一個插件幫助我們找到對應壓縮前的代碼位置。
          npm install source-map -S
          復制代碼
          先讀取對應的map文件(按filename對應),然后只需傳入壓縮后的報錯行號列號即可,就會返回壓縮前的錯誤信息。打個比喻:簡單地說相當于一本書的目錄,我們根據(jù)目錄可以快速找到某一部分內容的頁數(shù)
          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 = "";
          });
          復制代碼
          image.png

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

          ELK前端日志分析
          www.cnblogs.com/xiao9873341…
          看了一下許多平臺對錯誤日志的分析和可視化都使用了ELK,ELK在服務器運維界應該是運用的非常成熟了,很多成熟的大型項目都使用ELK來作為前端日志監(jiān)控、分析的工具。我對運維這一塊興趣不大,有興趣的可以自行搭建,整出來界面還是挺炫酷的。
          而我又不想每一次都跑去服務器查看日志,于是想到了可以建個表來把錯誤信息給存起來。用起老三樣koa+mongodb+vue,我們這項目就算是齊活了。(mongodb,yyds??,省去了建表許多功夫)
          npm install mongodb --save
          復制代碼
          新建一個文件db.js封裝一下mongo連接,方便復用:
          // 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,
                    { 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();
          復制代碼
          然后就可以在項目中愉快使用
           
            let db = require("./db");
            ...
            log4js.logError(JSON.stringify(obj));
            //插入數(shù)據(jù)
            await db.insert(obj);
            ctx.body = "";
          復制代碼
          數(shù)據(jù)插入成功??

          增加一個查詢接口:
          router.get("/errlist"async (ctx) => {
            let res = await db.find({});
            ctx.body = {
              data: res,
            };
          });
          復制代碼
          為了豐富錯誤信息,我們還可以在上報的時候增加報錯時間,用戶瀏覽器信息,自定義錯誤類型統(tǒng)計,引入圖表可視化展示,更加直觀地追蹤
          image.png

          待完善的點

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

          后記

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

          逆鋒起筆是一個專注于程序員圈子的技術平臺,你可以收獲最新技術動態(tài)、最新內測資格、BAT等大廠大佬的經(jīng)驗、增長自身、學習資料職業(yè)路線、賺錢思維,微信搜索逆鋒起筆關注!


          參考鏈接:
          從0到1,Vue大牛的前端搭建——異常監(jiān)控系統(tǒng)
          關于本文

          作者:violetrosez

          https://juejin.cn/post/6965022635470110733


          畢業(yè)不到 1年 的前端開發(fā)同學的焦慮

          從面試官角度看一次前端面試經(jīng)歷

          這 30 個頂級工具!前端開發(fā)很喜歡

          這些鮮為人知的前端冷知識,你都GET了嗎?

          一行 JS 實現(xiàn)的功能,看起來像一個前端專家


          支持下 
          瀏覽 31
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  啪啪免费视频网站 | 日本岛国视频在线观看一区二区三区 | 资源在线官网8 | 麻豆三级片电影 | 国产传媒一区 |