1. 講透前端錯(cuò)誤監(jiān)控,看這篇文章就夠了

        共 30304字,需瀏覽 61分鐘

         ·

        2022-01-22 09:29

        點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

        回復(fù)算法,加入前端編程面試算法每日一題群


        ○ 一、背景

        痛點(diǎn)

        某?天產(chǎn)品:xxx?告主反饋我們的??注冊(cè)不了!??天運(yùn)營(yíng):這個(gè)活動(dòng)在xxx媒體上掛掉了!

        在我司線(xiàn)上運(yùn)行的是近億級(jí)別的廣告頁(yè)面,這樣線(xiàn)上如果裸奔,出現(xiàn)了什么問(wèn)題不知道,后置在業(yè)務(wù)端發(fā)現(xiàn),被業(yè)務(wù)方詢(xún)問(wèn),這種場(chǎng)景很尷尬。

        選擇

        公司存在四個(gè)事業(yè)部,而每個(gè)事業(yè)部不下于3個(gè)項(xiàng)目,這里至少12個(gè)項(xiàng)目,這里作為伏筆,業(yè)務(wù)線(xiàn)多。

        我們是選擇自己做呢,還是選第三方的呢。我們比較一項(xiàng)幾款常見(jiàn)第三方。

        • Fundebug:付費(fèi)版 159元/月起,數(shù)據(jù)存在第三方,而數(shù)據(jù)自我保存需要 30 萬(wàn)/年。還是很貴的。
        • FrontJS,F(xiàn)rontJS 高級(jí)版 899/月,專(zhuān)業(yè)版是 2999/月。
        • Sentry,80 美金/月。

        以Sentry為計(jì)費(fèi),對(duì)這12個(gè)項(xiàng)目計(jì)算一下。12個(gè)項(xiàng)目一年將近10萬(wàn)。而大致估算過(guò)需要2人1.5月即90人日,能完成MVP版本,按每人1.5萬(wàn)工資/月計(jì)算,總共花費(fèi)4.5萬(wàn),而且是一勞永逸的。

        因此從成本角度我們會(huì)選擇自研,但除了成本外,還有其他原因。例如我們會(huì)基于這套系統(tǒng)做一些自定義功能,與公司權(quán)限用戶(hù)系統(tǒng)打通,再針對(duì)用戶(hù)進(jìn)行Todo管理,對(duì)用戶(hù)進(jìn)行錯(cuò)誤排行等。

        還有基于業(yè)務(wù)數(shù)據(jù)的安全,我們希望自我搭建一個(gè)系統(tǒng)。

        所以從成本、安全、擴(kuò)展性角度,我們選擇了自己研發(fā)。

        ○ 二、產(chǎn)品設(shè)計(jì)

        我們要什么樣的一個(gè)產(chǎn)品呢,根據(jù)第一性原理,解決關(guān)鍵問(wèn)題“怎么定位問(wèn)題”。通過(guò)5W1H法我們來(lái)分析,我們想要知道些什么信息呢?

        錯(cuò)誤信息

        其實(shí)錯(cuò)誤監(jiān)控說(shuō)簡(jiǎn)單就一句話(huà)可以描述,搜集頁(yè)面錯(cuò)誤,進(jìn)行上報(bào),然后對(duì)癥分析。

        按照5W1H法則進(jìn)行分析這句話(huà),可以發(fā)現(xiàn)有幾項(xiàng)需要我們關(guān)注。

        1. What,發(fā)?了什么錯(cuò)誤:邏輯錯(cuò)誤、數(shù)據(jù)錯(cuò)誤、?絡(luò)錯(cuò)誤、語(yǔ)法錯(cuò)誤等。
        2. When,出現(xiàn)的時(shí)間段,如時(shí)間戳。
        3. Who,影響了多少用戶(hù),包括報(bào)錯(cuò)事件數(shù)、IP、設(shè)備信息。
        4. Where,出現(xiàn)的頁(yè)面是哪些,包括頁(yè)面、廣告位(我司)、媒體(我司)。
        5. Why,錯(cuò)誤的原因是為什么,包括錯(cuò)誤堆棧、?列、SourceMap。
        6. How,怎么定位解決問(wèn)題,我們還需要收集系統(tǒng)等信息。

        架構(gòu)層次

        首先我們需要梳理下,我們需要一些哪些功能。

        那我們?cè)趺吹玫缴厦娴男畔⑦M(jìn)行最終錯(cuò)誤的定位呢。

        首先我們肯定需要對(duì)錯(cuò)誤進(jìn)行搜集,然后用戶(hù)設(shè)備頁(yè)面端的錯(cuò)誤我們?cè)趺床拍芨兄侥兀@就需要進(jìn)行上報(bào)。那么第一層就展現(xiàn)出來(lái)了,我們需要一個(gè)搜集上報(bào)端。

        那怎么才能進(jìn)行上報(bào)呢,和后端協(xié)作那么久,肯定知道的吧?? ,你需要一個(gè)接口。那就需要一個(gè)服務(wù)器來(lái)進(jìn)行對(duì)于上報(bào)的錯(cuò)誤進(jìn)行采集,對(duì)于錯(cuò)誤進(jìn)行篩選聚合。那么第二層也知道了啊,我們需要一個(gè)采集聚合端。

        我們搜集到了我們足夠的物料信息了,那接下來(lái)要怎么用起來(lái)呢,我們需要把它們按照我們的規(guī)則進(jìn)行整理。如果每次又是通過(guò)寫(xiě)類(lèi)SQL進(jìn)行整理查詢(xún)效率會(huì)很低,因此我們需要一個(gè)可視化的平臺(tái)進(jìn)行展示。因此有了第三層,可視化分析端。

        感覺(jué)好像做完啦,想必大家都這么想,一個(gè)錯(cuò)誤監(jiān)控平臺(tái)做完了,?? 。如果是這樣你會(huì)發(fā)現(xiàn)一個(gè)現(xiàn)象,每次上線(xiàn)和上線(xiàn)后一段時(shí)間,開(kāi)發(fā)同學(xué)都一直盯著屏幕看,這是在干嘛,人形眼動(dòng)觀(guān)察者模式嗎。因此我們需要通過(guò)代碼去解決,自然而然,第四層,監(jiān)控告警端應(yīng)運(yùn)而生。

        所以請(qǐng)大聲說(shuō)出來(lái)我們需要什么?? ,搜集上報(bào)端,采集聚合端,可視分析端,監(jiān)控告警端。

        ○ 三、系統(tǒng)設(shè)計(jì)

        如函數(shù)一樣,定義好每個(gè)環(huán)節(jié)的輸入和輸出,且核心需要處理的功能。

        下面我們看看上述所說(shuō)的四個(gè)端怎么去實(shí)現(xiàn)呢。

        搜集上報(bào)端(SDK)

        這個(gè)環(huán)節(jié)主要輸入是所有錯(cuò)誤,輸出是捕獲上報(bào)錯(cuò)誤。核心是處理不同類(lèi)型錯(cuò)誤的搜集工作。其他是一些非核心但必要的工作。

        錯(cuò)誤類(lèi)型

        先看看我們需要處理哪些錯(cuò)誤類(lèi)型。

        常見(jiàn)JS執(zhí)行錯(cuò)誤

        1. SyntaxError

        解析時(shí)發(fā)生語(yǔ)法錯(cuò)誤

        // 控制臺(tái)運(yùn)行
        const xx,
        復(fù)制代碼

        window.onerror捕獲不到SyntxError,一般SyntaxError在構(gòu)建階段,甚至本地開(kāi)發(fā)階段就會(huì)被發(fā)現(xiàn)。

        1. TypeError

        值不是所期待的類(lèi)型

        // 控制臺(tái)運(yùn)行
        const person = void 0
        person.name
        復(fù)制代碼
        1. ReferenceError

        引用未聲明的變量

        // 控制臺(tái)運(yùn)行
        nodefined
        復(fù)制代碼
        1. RangeError

        當(dāng)一個(gè)值不在其所允許的范圍或者集合中

        (function fn ( { fn() })()
        復(fù)制代碼

        網(wǎng)絡(luò)錯(cuò)誤

        1. ResourceError

        資源加載錯(cuò)誤

        new Image().src = '/remote/image/notdeinfed.png'
        復(fù)制代碼
        1. HttpError

        Http請(qǐng)求錯(cuò)誤

        // 控制臺(tái)運(yùn)行
        fetch('/remote/notdefined', {})
        復(fù)制代碼

        搜集錯(cuò)誤

        所有起因來(lái)源于錯(cuò)誤,那我們?nèi)绾芜M(jìn)行錯(cuò)誤捕獲。

        try/catch

        能捕獲常規(guī)運(yùn)行時(shí)錯(cuò)誤,語(yǔ)法錯(cuò)誤和異步錯(cuò)誤不行

        // 常規(guī)運(yùn)行時(shí)錯(cuò)誤,可以捕獲 ?
        try {
          console.log(notdefined);
        catch(e) {
          console.log('捕獲到異常:', e);
        }

        // 語(yǔ)法錯(cuò)誤,不能捕獲 ?
        try {
          const notdefined,
        catch(e) {
          console.log('捕獲到異常:', e);
        }

        // 異步錯(cuò)誤,不能捕獲 ?
        try {
          setTimeout(() => {
            console.log(notdefined);
          }, 0)
        catch(e) {
          console.log('捕獲到異常:',e);
        }
        復(fù)制代碼

        try/catch有它細(xì)致處理的優(yōu)勢(shì),但缺點(diǎn)也比較明顯。

        window.onerror

        pure js錯(cuò)誤收集,window.onerror,當(dāng) JS 運(yùn)行時(shí)錯(cuò)誤發(fā)生時(shí),window 會(huì)觸發(fā)一個(gè) ErrorEvent 接口的 error 事件。

        /**
        @param {String}  message    錯(cuò)誤信息
        @param {String}  source    出錯(cuò)文件
        @param {Number}  lineno    行號(hào)
        @param {Number}  colno    列號(hào)
        @param {Object}  error  Error對(duì)象
        */


        window.onerror = function(message, source, lineno, colno, error{
           console.log('捕獲到異常:', {message, source, lineno, colno, error});
        }
        復(fù)制代碼

        先驗(yàn)證下幾個(gè)錯(cuò)誤是否可以捕獲。

        // 常規(guī)運(yùn)行時(shí)錯(cuò)誤,可以捕獲 ?

        window.onerror = function(message, source, lineno, colno, error{
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        console.log(notdefined);

        // 語(yǔ)法錯(cuò)誤,不能捕獲 ?
        window.onerror = function(message, source, lineno, colno, error{
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        const notdefined,
              
        // 異步錯(cuò)誤,可以捕獲 ?
        window.onerror = function(message, source, lineno, colno, error{
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        setTimeout(() => {
          console.log(notdefined);
        }, 0)

        // 資源錯(cuò)誤,不能捕獲 ?
        <script>
          window.onerror = function(message, source, lineno, colno, error{
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
          return true;
        }
        </script>
        <img src="https://yun.tuia.cn/im
        age/kkk.png">
        復(fù)制代碼

        window.onerror 不能捕獲資源錯(cuò)誤怎么辦?

        window.addEventListener

        當(dāng)一項(xiàng)資源(如圖片或腳本)加載失敗,加載資源的元素會(huì)觸發(fā)一個(gè) Event 接口的 error 事件,這些 error 事件不會(huì)向上冒泡到 window,但能被捕獲。而window.onerror不能監(jiān)測(cè)捕獲。

        // 圖片、script、css加載錯(cuò)誤,都能被捕獲 ?
        <script>
          window.addEventListener('error', (error) => {
             console.log('捕獲到異常:', error);
          }, true)
        </script>

        <img src="https://yun.tuia.cn/image/kkk.png">
        <script src="https://yun.tuia.cn/foundnull.js"></script>
        <link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
          
        // new Image錯(cuò)誤,不能捕獲 ?
        <script>
          window.addEventListener('error', (error) => {
            console.log('捕獲到異常:', error);
          }, true)
        </script>

        <script>
          new Image().src = 'https://yun.tuia.cn/image/lll.png'
        </script>


        // fetch錯(cuò)誤,不能捕獲 ?
        <script>
          window.addEventListener('error', (error) => {
            console.log('捕獲到異常:', error);
          }, true)
        </script>

        <script>
          fetch('https://tuia.cn/test')
        </script>

        復(fù)制代碼

        new Image運(yùn)用的比較少,可以單獨(dú)自己處理自己的錯(cuò)誤。

        但通用的fetch怎么辦呢,fetch返回Promise,但Promise的錯(cuò)誤不能被捕獲,怎么辦呢?

        Promise錯(cuò)誤

        1. 普通Promise錯(cuò)誤

        try/catch不能捕獲Promise中的錯(cuò)誤

        // try/catch 不能處理 JSON.parse 的錯(cuò)誤,因?yàn)樗?nbsp;Promise 中
        try {
          new Promise((resolve,reject) => { 
            JSON.parse('')
            resolve();
          })
        catch(err) {
          console.error('in try catch', err)
        }

        // 需要使用catch方法
        new Promise((resolve,reject) => { 
          JSON.parse('')
          resolve();
        }).catch(err => {
          console.log('in catch fn', err)
        })
        復(fù)制代碼
        1. async錯(cuò)誤

        try/catch不能捕獲async包裹的錯(cuò)誤

        const getJSON = async () => {
          throw new Error('inner error')
        }

        // 通過(guò)try/catch處理
        const makeRequest = async () => {
            try {
                // 捕獲不到
                JSON.parse(getJSON());
            } catch (err) {
                console.log('outer', err);
            }
        };

        try {
            // try/catch不到
            makeRequest()
        catch(err) {
            console.error('in try catch', err)
        }

        try {
            // 需要await,才能捕獲到
            await makeRequest()
        catch(err) {
            console.error('in try catch', err)
        }
        復(fù)制代碼
        1. import chunk錯(cuò)誤

        import其實(shí)返回的也是一個(gè)promise,因此使用如下兩種方式捕獲錯(cuò)誤

        // Promise catch方法
        import(/* webpackChunkName: "incentive" */'./index').then(module => {
            module.default()
        }).catch((err) => {
            console.error('in catch fn', err)
        })

        // await 方法,try catch
        try {
            const module = await import(/* webpackChunkName: "incentive" */'./index');
            module.default()
        catch(err) {
            console.error('in try catch', err)
        }
        復(fù)制代碼

        小結(jié):全局捕獲Promise中的錯(cuò)誤

        以上三種其實(shí)歸結(jié)為Promise類(lèi)型錯(cuò)誤,可以通過(guò)unhandledrejection捕獲

        // 全局統(tǒng)一處理Promise
        window.addEventListener("unhandledrejection"function(e){
          console.log('捕獲到異常:', e);
        });
        fetch('https://tuia.cn/test')
        復(fù)制代碼

        為了防止有漏掉的 Promise 異常,可通過(guò)unhandledrejection用來(lái)全局監(jiān)聽(tīng)Uncaught Promise Error。

        Vue錯(cuò)誤

        由于Vue會(huì)捕獲所有Vue單文件組件或者Vue.extend繼承的代碼,所以在Vue里面出現(xiàn)的錯(cuò)誤,并不會(huì)直接被window.onerror捕獲,而是會(huì)拋給Vue.config.errorHandler。

        /**
         * 全局捕獲Vue錯(cuò)誤,直接扔出給onerror處理
         */

        Vue.config.errorHandler = function (err{
          setTimeout(() => {
            throw err
          })
        }
        復(fù)制代碼

        React錯(cuò)誤

        react 通過(guò)componentDidCatch,聲明一個(gè)錯(cuò)誤邊界的組件

        class ErrorBoundary extends React.Component {
          constructor(props) {
            super(props);
            this.state = { hasErrorfalse };
          }

          static getDerivedStateFromError(error) {
            // 更新 state 使下一次渲染能夠顯示降級(jí)后的 UI
            return { hasErrortrue };
          }

          componentDidCatch(error, errorInfo) {
            // 你同樣可以將錯(cuò)誤日志上報(bào)給服務(wù)器
            logErrorToMyService(error, errorInfo);
          }

          render() {
            if (this.state.hasError) {
              // 你可以自定義降級(jí)后的 UI 并渲染
              return <h1>Something went wrong.</h1>;
            }

            return this.props.children; 
          }
        }

        class App extends React.Component {
           
          render() {
            return (
            <ErrorBoundary>
              <MyWidget />
            </ErrorBoundary>
          
            )
          }
        }
        復(fù)制代碼

        但error boundaries并不會(huì)捕捉以下錯(cuò)誤:React事件處理,異步代碼,error boundaries自己拋出的錯(cuò)誤。

        跨域問(wèn)題

        一般情況,如果出現(xiàn) Script error 這樣的錯(cuò)誤,基本上可以確定是出現(xiàn)了跨域問(wèn)題。

        如果當(dāng)前投放頁(yè)面和云端JS所在不同域名,如果云端JS出現(xiàn)錯(cuò)誤,window.onerror會(huì)出現(xiàn)Script Error。通過(guò)以下兩種方法能給予解決。

        • 后端配置Access-Control-Allow-Origin、前端script加crossorigin。
        <script src="http://yun.tuia.cn/test.js" crossorigin></script>

        const script = document.createElement('script');
        script.crossOrigin = 'anonymous';
        script.src = 'http://yun.tuia.cn/
        test.js';
        document.body.appendChild(script);
        復(fù)制代碼
        • 如果不能修改服務(wù)端的請(qǐng)求頭,可以考慮通過(guò)使用 try/catch 繞過(guò),將錯(cuò)誤拋出。
        <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
          <script>
          window.onerror = function (message, url, line, column, error{
            console.log(message, url, line, column, error);
          }

          try {
            foo(); // 調(diào)用testerror.js中定義的foo方法
          } catch (e) {
            throw e;
          }
          </script>

        </body>
        </html>
        復(fù)制代碼

        會(huì)發(fā)現(xiàn)如果不加try catch,console.log就會(huì)打印script error。加上try catch就能捕獲到。

        我們捋一下場(chǎng)景,一般調(diào)用遠(yuǎn)端js,有下列三種常見(jiàn)情況。

        • 調(diào)用遠(yuǎn)端JS的方法出錯(cuò)
        • 遠(yuǎn)端JS內(nèi)部的事件出問(wèn)題
        • 要么在setTimeout等回調(diào)內(nèi)出錯(cuò)

        調(diào)用方法場(chǎng)景

        可以通過(guò)封裝一個(gè)函數(shù),能裝飾原方法,使得其能被try/catch。


        <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
          <script>
          window.onerror = function (message, url, line, column, error{
            console.log(message, url, line, column, error);
          }

          function wrapErrors(fn{
            // don't wrap function more than once
            if (!fn.__wrapped__) {
              fn.__wrapped__ = function () {
                try {
                  return fn.apply(thisarguments);
                } catch (e) {
                  throw e; // re-throw the error
                }
              };
            }

            return fn.__wrapped__;
          }

          wrapErrors(foo)()
          </script>

        </body>
        </html>

        復(fù)制代碼

        大家可以嘗試去掉wrapErrors感受下。

        事件場(chǎng)景

        可以劫持原生方法。


        <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script>
            const originAddEventListener = EventTarget.prototype.addEventListener;
            EventTarget.prototype.addEventListener = function (type, listener, options{
              const wrappedListener = function (...args{
                try {
                  return listener.apply(this, args);
                }
                catch (err) {
                  throw err;
                }
              }
              return originAddEventListener.call(this, type, wrappedListener, options);
            }
          </script>

          <div style="height: 9999px;">http://test.com</div>
          <script src="https://yun.dui88.com/tuia/cdn/remote/error_scroll.js"></script>
          <script>
          window.onerror = function (message, url, line, column, error{
            console.log(message, url, line, column, error);
          }
          </script>

        </body>
        </html>
        復(fù)制代碼

        大家可以嘗試去掉封裝EventTarget.prototype.addEventListener的那段代碼,感受下。

        上報(bào)接口

        為什么不能直接用GET/POST/HEAD請(qǐng)求接口進(jìn)行上報(bào)?

        這個(gè)比較容易想到原因。一般而言,打點(diǎn)域名都不是當(dāng)前域名,所以所有的接口請(qǐng)求都會(huì)構(gòu)成跨域。

        為什么不能用請(qǐng)求其他的文件資源(js/css/ttf)的方式進(jìn)行上報(bào)?

        創(chuàng)建資源節(jié)點(diǎn)后只有將對(duì)象注入到瀏覽器DOM樹(shù)后,瀏覽器才會(huì)實(shí)際發(fā)送資源請(qǐng)求。而且載入js/css資源還會(huì)阻塞頁(yè)面渲染,影響用戶(hù)體驗(yàn)。

        構(gòu)造圖片打點(diǎn)不僅不用插入DOM,只要在js中new出Image對(duì)象就能發(fā)起請(qǐng)求,而且還沒(méi)有阻塞問(wèn)題,在沒(méi)有js的瀏覽器環(huán)境中也能通過(guò)img標(biāo)簽正常打點(diǎn)。

        使用new Image進(jìn)行接口上報(bào)。最后一個(gè)問(wèn)題,同樣都是圖片,上報(bào)時(shí)選用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。

        首先,1x1像素是最小的合法圖片。而且,因?yàn)槭峭ㄟ^(guò)圖片打點(diǎn),所以圖片最好是透明的,這樣一來(lái)不會(huì)影響頁(yè)面本身展示效果,二者表示圖片透明只要使用一個(gè)二進(jìn)制位標(biāo)記圖片是透明色即可,不用存儲(chǔ)色彩空間數(shù)據(jù),可以節(jié)約體積。因?yàn)樾枰该魃钥梢灾苯优懦齁EPG。

        同樣的響應(yīng),GIF可以比BMP節(jié)約41%的流量,比PNG節(jié)約35%的流量。GIF才是最佳選擇。

        • 可以進(jìn)行跨域
        • 不會(huì)攜帶cookie
        • 不需要等待服務(wù)器返回?cái)?shù)據(jù)

        使用1\*1的gif[1]

        非阻塞加載

        盡量避免SDK的js資源加載影響。

        通過(guò)先把window.onerror的錯(cuò)誤記錄進(jìn)行緩存,然后異步進(jìn)行SDK的加載,再在SDK里面處理錯(cuò)誤上報(bào)。

        <!DOCTYPE html>
        <html lang="en">
        <head>
            <script>
                (function(w{
                    w._error_storage_ = [];
                    function errorhandler(){
                        // 用于記錄當(dāng)前的錯(cuò)誤            
                        w._error_storage_&&w._error_storage_.push([].slice.call(arguments));
                    } 
                    w.addEventListener && w.addEventListener("error", errorhandler, true);
                    var times = 3,
                    appendScript = function appendScript() {
                        var sc = document.createElement("script");
                        sc.async = !0,
                        sc.src = './build/skyeye.js',  // 取決于你存放的位置
                        sc.crossOrigin = "anonymous",
                        sc.onerror = function() {
                            times--,
                            times > 0 && setTimeout(appendScript, 1500)
                        },
                        document.head && document.head.appendChild(sc);
                    };
                    setTimeout(appendScript, 1500);
                })(window);
            </script>

        </head>
        <body>
            <h1>這是一個(gè)測(cè)試頁(yè)面(new)</h1>
        </body>
        </html>

        復(fù)制代碼

        采集聚合端(日志服務(wù)器)

        這個(gè)環(huán)節(jié),輸入是借口接收到的錯(cuò)誤記錄,輸出是有效的數(shù)據(jù)入庫(kù)。核心功能需要對(duì)數(shù)據(jù)進(jìn)行清洗,順帶解決了過(guò)多的服務(wù)壓力。另一個(gè)核心功能是對(duì)數(shù)據(jù)進(jìn)行入庫(kù)。

        總體流程可以看為錯(cuò)誤標(biāo)識(shí) -> 錯(cuò)誤過(guò)濾 -> 錯(cuò)誤接收 -> 錯(cuò)誤存儲(chǔ)。

        錯(cuò)誤標(biāo)識(shí)(SDK配合)

        聚合之前,我們需要有不同維度標(biāo)識(shí)錯(cuò)誤的能力,可以理解為定位單個(gè)錯(cuò)誤條目,單個(gè)錯(cuò)誤事件的能力。

        單個(gè)錯(cuò)誤條目

        通過(guò)date和隨機(jī)值生成一條對(duì)應(yīng)的錯(cuò)誤條目id。

        const errorKey = `${+new Date()}@${randomString(8)}`

        function randomString(len{  
            len = len || 32;
            let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
            let maxPos = chars.length;
            let pwd = '';  
            for (let i = 0; i < len; i++) {    
                pwd += chars.charAt(Math.floor(Math.random() * maxPos));  
            }  
            return pwd;
        }
        復(fù)制代碼

        單個(gè)錯(cuò)誤事件

        首先需要有定位同個(gè)錯(cuò)誤事件(不同用戶(hù),發(fā)生相同錯(cuò)誤類(lèi)型、錯(cuò)誤信息)的能力。

        通過(guò)message、colno與lineno進(jìn)行相加計(jì)算阿斯克碼值,可以生成錯(cuò)誤的errorKey。

        const eventKey = compressString(String(e.message), String(e.colno) + String(e.lineno))

        function compressString(str, key{
            let chars = 'ABCDEFGHJKMNPQRSTWXYZ';
            if (!str || !key) {
                return 'null';
            }
            let n = 0,
                m = 0;
            for (let i = 0; i < str.length; i++) {
                n += str[i].charCodeAt();
            }
            for (let j = 0; j < key.length; j++) {
                m += key[j].charCodeAt();
            }
            let num = n + '' + key[key.length - 1].charCodeAt() + m + str[str.length - 1].charCodeAt();
            if(num) {
                num = num + chars[num[num.length - 1]];
            }
            return num;
        }
        復(fù)制代碼

        如下圖,一個(gè)錯(cuò)誤事件(事件列表),下屬每條即為實(shí)際的錯(cuò)誤條目。

        錯(cuò)誤過(guò)濾(SDK配合)

        域名過(guò)濾

        過(guò)濾本頁(yè)面script error,可能被webview插入其他js。

        我們只關(guān)心自己的遠(yuǎn)端JS問(wèn)題,因此做了根據(jù)本公司域名進(jìn)行過(guò)濾。

        // 偽代碼
        if(!e.filename || !e.filename.match(/^(http|https):\/\/yun./)) return true
        復(fù)制代碼

        重復(fù)上報(bào)

        怎么避免重復(fù)的數(shù)據(jù)上報(bào)?根據(jù)errorKey來(lái)進(jìn)行緩存,重復(fù)的錯(cuò)誤避免上報(bào)的次數(shù)超過(guò)閾值。

        // 偽代碼

        const localStorage = window.localStorage;
        const TIMES = 6// 緩存條數(shù)

        export function setItem(key, repeat{
            if(!key) {
                key = 'unknow';
            }
          
            if (has(key)) {
                const value = getItem(key);
                
               // 核心代碼,超過(guò)條數(shù),跳出
                if (value >= repeat) {
                    return true;
                }
                storeStorage[key] = {
                    value: value + 1,
                    timeDate.now()
                }
            } else {
                storeStorage[key] = {
                    value1,
                    timeDate.now()
                }
            }
            return false;
        }
        復(fù)制代碼

        錯(cuò)誤接收

        在處理接收接口的時(shí)候,注意流量的控制,這也是后端開(kāi)發(fā)需要投入最多精力的地方,處理高并發(fā)的流量。

        錯(cuò)誤記錄

        接收端使用Koa,簡(jiǎn)單的實(shí)現(xiàn)了接收及打印到磁盤(pán)。

        // 偽代碼

        module.exports = async ctx => {
          const { query } = ctx.request;
          
          // 對(duì)于字段進(jìn)行簡(jiǎn)單check 
          check([ 'mobile''network''ip''system''ua', ......], query);

          ctx.type = 'application/json';
          ctx.body = { code'1'msg'數(shù)據(jù)上報(bào)成功' };

          // 進(jìn)行日志記錄到磁盤(pán)的代碼,根據(jù)自己的日志庫(kù)選擇
        };
        復(fù)制代碼

        削峰機(jī)制

        比如每秒設(shè)置2000的閾值,然后根據(jù)請(qǐng)求量減少上限,定時(shí)重置上限。

        // 偽代碼

        // 1000ms
        const TICK = 1000;
        // 1秒上限為2000
        const MAX_LIMIT = 2000;
        // 每臺(tái)服務(wù)器請(qǐng)求上限值
        let maxLimit = MAX_LIMIT;

        /**
         * 啟動(dòng)重置函數(shù)
         */

        const task = () => {
          setTimeout(() => {
            maxLimit = MAX_LIMIT;
            task();
          }, TICK);
        };
        task();

        const check = () => {
          if (maxLimit <= 0) {
            throw new Error('超過(guò)上報(bào)次數(shù)');
          }
          maxLimit--;
          // 執(zhí)行業(yè)務(wù)代碼。。。
        };
        復(fù)制代碼

        采樣處理

        超過(guò)閾值,還可以進(jìn)行采樣收集。

        // 只采集 20%
        if(Math.random() < 0.2) {
          collect(data)      // 記錄錯(cuò)誤信息
        }
        復(fù)制代碼

        錯(cuò)誤存儲(chǔ)

        對(duì)于打印在了磁盤(pán)的日志,我們?cè)趺礃硬拍軐?duì)于其進(jìn)行聚合呢,這里得考慮使用存儲(chǔ)方案。

        一般選擇了存儲(chǔ)方案后,設(shè)置好配置,存儲(chǔ)方案就可以通過(guò)磁盤(pán)定時(shí)周期性的獲取數(shù)據(jù)。因此我們需要選擇一款存儲(chǔ)方案。

        對(duì)于存儲(chǔ)方案,我們對(duì)比了日常常見(jiàn)方案,阿里云日志服務(wù) - Log Service(SLS)、ELK(Elastic、Logstash、Kibana)、Hadoop/Hive(將數(shù)據(jù)存儲(chǔ)在 Hadoop,利用 Hive 進(jìn)行查詢(xún)) 類(lèi)方案的對(duì)比。

        從以下方面進(jìn)行了對(duì)比,最終選擇了Log Service,主要考慮為無(wú)需搭建,成本低,查詢(xún)功能滿(mǎn)足。

        功能項(xiàng) ELK 類(lèi)系統(tǒng) Hadoop + Hive 日志服務(wù)
        日志延時(shí) 1~60 秒 幾分鐘~數(shù)小時(shí) 實(shí)時(shí)
        查詢(xún)延時(shí) 小于 1 秒 分鐘級(jí) 小于 1 秒
        查詢(xún)能力
        擴(kuò)展性 提前預(yù)備機(jī)器 提前預(yù)備機(jī)器 秒級(jí) 10 倍擴(kuò)容
        成本 較高 較低 很低

        日志延時(shí):日志產(chǎn)生后,多久可查詢(xún)。查詢(xún)延時(shí):?jiǎn)挝粫r(shí)間掃描數(shù)據(jù)量。查詢(xún)能力:關(guān)鍵詞查詢(xún)、條件組合查詢(xún)、模糊查詢(xún)、數(shù)值比較、上下文查詢(xún)。擴(kuò)展性:快速應(yīng)對(duì)百倍流量上漲。成本:每 GB 費(fèi)用。

        具體API使用,可查看日志服務(wù)[2]。

        可視分析端(可視化平臺(tái))

        這個(gè)環(huán)節(jié),輸入是借口接收到的錯(cuò)誤記錄,輸出是有效的數(shù)據(jù)入庫(kù)。核心功能需要對(duì)數(shù)據(jù)進(jìn)行清洗,順帶解決了過(guò)多的服務(wù)壓力。另一個(gè)核心功能是對(duì)數(shù)據(jù)進(jìn)行入庫(kù)。

        主功能

        這部分主要是產(chǎn)品功能的合理設(shè)計(jì),做到小而美,具體的怎么聚合,參考阿里云SLS就可以。

        1. 首頁(yè)圖表,可選1天、4小時(shí)、1小時(shí)等等,聚合錯(cuò)誤數(shù),根據(jù)1天切分24份來(lái)聚合。
        2. 首頁(yè)列表,聚合選中時(shí)間內(nèi)的數(shù)據(jù),展示錯(cuò)誤文件、錯(cuò)誤key、事件數(shù)、錯(cuò)誤類(lèi)型、時(shí)間、錯(cuò)誤信息。
        3. 錯(cuò)誤詳情,事件列表、基本信息、設(shè)備信息、設(shè)備占比圖表(見(jiàn)上面事件列表的圖)。


        image.png

        排行榜

        剛開(kāi)始做了待處理錯(cuò)誤列表、我的錯(cuò)誤列表、已解決列表,錯(cuò)誤與人沒(méi)有綁定關(guān)系,過(guò)于依賴(lài)人為主動(dòng),需要每個(gè)人主動(dòng)到平臺(tái)上處理,效果不佳。

        后面通過(guò)錯(cuò)誤作者排行榜,通過(guò)釘釘日?qǐng)?bào)來(lái)提醒對(duì)應(yīng)人員處理。緊急錯(cuò)誤,通過(guò)實(shí)時(shí)告警來(lái)責(zé)任到人,后面告警會(huì)說(shuō)。

        具體原理:

        • webpack打包通過(guò)git命令把作者和作者郵箱、時(shí)間打包在頭部。
        • 在可視化服務(wù)中,去請(qǐng)求對(duì)應(yīng)的報(bào)錯(cuò)url匹配到對(duì)應(yīng)作者,返回給展示端。
        image.png

        SourceMap

        利用webpack的hidden-source-map構(gòu)建。與 source-map 相比少了末尾的注釋?zhuān)?output 目錄下的 index.js.map 沒(méi)有少。線(xiàn)上環(huán)境避免source-map泄露。

        webpackJsonp([1],[
          function(e,t,i){...},
          function(e,t,i){...},
          function(e,t,i){...},
          function(e,t,i){...},
          ...
        ])
        // 這里沒(méi)有生成source-map的鏈接地址
        復(fù)制代碼

        根據(jù)報(bào)錯(cuò)文件的url,根據(jù)團(tuán)隊(duì)內(nèi)部約定好的目錄和規(guī)則,定位之前打包上傳的sourceMap地址。

        const sourcemapUrl = ('xxxfolder/' + url + 'xxxHash' +'.map')
        復(fù)制代碼


        獲取上報(bào)的line、column、source,利用第三方庫(kù)sourceMap進(jìn)行定位。

        const sourceMap = require('source-map')

        // 根據(jù)行數(shù)獲取源文件行數(shù)
        const getPosition = async(map, rolno, colno) => {
          const consumer = await new sourceMap.SourceMapConsumer(map)

          const position = consumer.originalPositionFor({
            line: rolno,
            column: colno
          })

          position.content = consumer.sourceContentFor(position.source)

          return position
        }
        復(fù)制代碼

        感興趣SourceMap原理的,可以繼續(xù)深入,SourceMap 與前端異常監(jiān)控[3]。

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

        報(bào)警設(shè)置

        1. 每條業(yè)務(wù)線(xiàn)設(shè)置自己的閾值、錯(cuò)誤時(shí)間跨度,報(bào)警輪詢(xún)間隔
        2. 通過(guò)釘釘hook報(bào)警到對(duì)應(yīng)的群
        3. 通過(guò)日?qǐng)?bào)形式報(bào)出錯(cuò)誤作者排行榜
        image.png

        ○ 四、擴(kuò)展

        行為搜集

        通過(guò)搜集用戶(hù)的操作,可以明顯發(fā)現(xiàn)錯(cuò)誤為什么產(chǎn)生。

        分類(lèi)

        • UI行為:點(diǎn)擊、滾動(dòng)、聚焦/失焦、長(zhǎng)按
        • 瀏覽器行為:請(qǐng)求、前進(jìn)/后退、跳轉(zhuǎn)、新開(kāi)頁(yè)面、關(guān)閉
        • 控制臺(tái)行為:log、warn、error

        搜集方式

        1. 點(diǎn)擊行為

        使用addEventListener監(jiān)聽(tīng)全局上的click事件,將事件和DOM元素名字收集。與錯(cuò)誤信息一起上報(bào)。

        1. 發(fā)送請(qǐng)求

        監(jiān)聽(tīng)XMLHttpRequest的onreadystatechange回調(diào)函數(shù)

        1. 頁(yè)面跳轉(zhuǎn)

        監(jiān)聽(tīng)window.onpopstate,頁(yè)面進(jìn)行跳轉(zhuǎn)時(shí)會(huì)觸發(fā)。

        1. 控制臺(tái)行為

        重寫(xiě)console對(duì)象的info等方法。

        有興趣可以參考行為監(jiān)控[4]。

        遇到的問(wèn)題

        由于涉及到一些隱私,下述會(huì)做脫敏處理。

        空日志問(wèn)題

        上線(xiàn)灰度運(yùn)行后,我們發(fā)現(xiàn)SLS日志存在一些空日志?? ,??,這是發(fā)生了啥?

        首先我們回憶下這個(gè)鏈路上有哪些環(huán)節(jié)可能存在問(wèn)題。

        image.png

        排查鏈路,SLS采集環(huán)節(jié)之前有磁盤(pán)日志收集,服務(wù)端接收,SDK上報(bào),那我們依次排查。

        往前一步,發(fā)現(xiàn)磁盤(pán)日志就已經(jīng)存在空日志,那剩下就得看一下接收端、SDK端。

        開(kāi)始利用控制變量法,先在SDK端進(jìn)行空判斷,防止空日志上報(bào)。結(jié)果:發(fā)現(xiàn)無(wú)效??。

        再繼續(xù)對(duì)Node接收端處理,對(duì)接收到的數(shù)據(jù)進(jìn)行判空,如果為空不進(jìn)行日志打印,結(jié)果:依然無(wú)效??。

        所以開(kāi)始定位是不是日志打印本身出了什么問(wèn)題?研究了下日志第三方日志庫(kù)的API,進(jìn)行了各種嘗試,發(fā)現(xiàn)依舊沒(méi)用,我臉黑了??。

        什么情況,“遇事不決”看源碼。排查下日志庫(kù)源碼存在什么問(wèn)題。對(duì)于源碼的主調(diào)用流程走了一遍,并沒(méi)有發(fā)現(xiàn)什么問(wèn)題,一頭霧水??。

        整個(gè)代碼邏輯很正常,這讓我們開(kāi)始懷疑難道是數(shù)據(jù)的問(wèn)題,于是開(kāi)始縮減上報(bào)的字段,最終定義為了一個(gè)字段。發(fā)現(xiàn)上線(xiàn)后沒(méi)有問(wèn)題了??。

        難道是有些字段存儲(chǔ)的數(shù)據(jù)過(guò)長(zhǎng)導(dǎo)致的?但從代碼邏輯、流程日志中并沒(méi)有反應(yīng)這個(gè)錯(cuò)誤的可能性。

        因此我們利用二分法,二分地增加字段,最終定位到了某個(gè)字段。如果存在某個(gè)字段上報(bào)就會(huì)出現(xiàn)問(wèn)題。這很出乎人的意料。

        我們?cè)傧肓讼骆溌?,除了日志?kù),其他代碼基本都是我們自己的邏輯,所以對(duì)日志庫(kù)進(jìn)行了排查,懷疑其對(duì)某個(gè)字段做了什么處理。

        于是通過(guò)搜索,定位到了日志庫(kù)在仆從模式(可以了解下Node的主從模式)下會(huì)使用某個(gè)字段來(lái)表意,導(dǎo)致和我們上報(bào)的字段沖突,因此丟失了??。

        日志丟失問(wèn)題

        解決了上個(gè)問(wèn)題,開(kāi)心了,一股成就感涌上心頭。但馬上就被當(dāng)頭一棒,我發(fā)現(xiàn)我高興的太早了??。

        團(tuán)隊(duì)的某同學(xué)在本地測(cè)試的時(shí)候,由于玩的很開(kāi)心,一直去刷新頁(yè)面去上報(bào)當(dāng)前頁(yè)面的錯(cuò)誤。但他發(fā)現(xiàn)本地上報(bào)的條數(shù)和實(shí)際日志服務(wù)里的條數(shù)對(duì)不上,日志服務(wù)里的少了很多。

        由于之前自身剛畢業(yè)時(shí)候做過(guò)2年多后端開(kāi)發(fā),對(duì)于IO操作丟失數(shù)據(jù)還是有點(diǎn)敏感。直覺(jué)上就感覺(jué)可能是多進(jìn)程方向的問(wèn)題。懷疑是多進(jìn)程導(dǎo)致的文件死鎖問(wèn)題。

        那我們?nèi)サ舳嗑€(xiàn)程,通過(guò)單線(xiàn)程,我們?nèi)ブ貜?fù)原先復(fù)現(xiàn)問(wèn)題的步驟。發(fā)現(xiàn)沒(méi)有遺漏??。

        我們發(fā)現(xiàn)能進(jìn)行配置Cluster(主從模式)的地方有兩處,日志庫(kù)和部署工具。

        觀(guān)察日志庫(kù)默認(rèn)使用的主從進(jìn)程模式,而部署工具沒(méi)有主從模式的概念,勢(shì)必會(huì)導(dǎo)致寫(xiě)入IO的死鎖問(wèn)題,導(dǎo)致日志丟失。于是在想社區(qū)有沒(méi)有可以有解決此問(wèn)題的第三方支持。

        然后通過(guò)谷歌搜索,很快就找到了對(duì)應(yīng)的第三方庫(kù),它能提供主人進(jìn)程和仆從進(jìn)程之間的消息溝通。原理是主人進(jìn)程負(fù)責(zé)所有消息寫(xiě)入log,而仆從進(jìn)程通過(guò)消息傳遞給主人進(jìn)程。


        關(guān)于本文

        來(lái)源:羽飛

        https://juejin.cn/post/6987681953424080926

        最后

        歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
        回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
        回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
        回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
        如果這篇文章對(duì)你有幫助,在看」是最大的支持
         》》面試官也在看的算法資料《《
        “在看和轉(zhuǎn)發(fā)”就是最大的支持


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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. hugeboobs大胸大乳 | 99re视频这里只有在线精品8 | 电车痴汉系列h文 | 色哟哟 精品一区 | 人人操超碰在线观看 |