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

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

          共 30304字,需瀏覽 61分鐘

           ·

          2022-01-22 09:29

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

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


          ○ 一、背景

          痛點(diǎn)

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

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

          選擇

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

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

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

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

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

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

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

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

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

          錯誤信息

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

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

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

          架構(gòu)層次

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

          那我們怎么得到上面的信息進(jìn)行最終錯誤的定位呢。

          首先我們肯定需要對錯誤進(jìn)行搜集,然后用戶設(shè)備頁面端的錯誤我們怎么才能感知到呢,這就需要進(jìn)行上報(bào)。那么第一層就展現(xiàn)出來了,我們需要一個搜集上報(bào)端。

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

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

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

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

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

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

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

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

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

          錯誤類型

          先看看我們需要處理哪些錯誤類型。

          常見JS執(zhí)行錯誤

          1. SyntaxError

          解析時(shí)發(fā)生語法錯誤

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

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

          1. TypeError

          值不是所期待的類型

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

          引用未聲明的變量

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

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

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

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

          1. ResourceError

          資源加載錯誤

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

          Http請求錯誤

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

          搜集錯誤

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

          try/catch

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

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

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

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

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

          window.onerror

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

          /**
          @param {String}  message    錯誤信息
          @param {String}  source    出錯文件
          @param {Number}  lineno    行號
          @param {Number}  colno    列號
          @param {Object}  error  Error對象
          */


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

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

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

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

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

          // 資源錯誤,不能捕獲 ?
          <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 不能捕獲資源錯誤怎么辦?

          window.addEventListener

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

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

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


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

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

          復(fù)制代碼

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

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

          Promise錯誤

          1. 普通Promise錯誤

          try/catch不能捕獲Promise中的錯誤

          // try/catch 不能處理 JSON.parse 的錯誤,因?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錯誤

          try/catch不能捕獲async包裹的錯誤

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

          // 通過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錯誤

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

          // 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中的錯誤

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

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

          為了防止有漏掉的 Promise 異常,可通過unhandledrejection用來全局監(jiān)聽Uncaught Promise Error。

          Vue錯誤

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

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

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

          React錯誤

          react 通過componentDidCatch,聲明一個錯誤邊界的組件

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

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

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

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

              return this.props.children; 
            }
          }

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

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

          跨域問題

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

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

          • 后端配置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ù)端的請求頭,可以考慮通過使用 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);
            }

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

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

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

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

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

          調(diào)用方法場景

          可以通過封裝一個函數(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感受下。

          事件場景

          可以劫持原生方法。


          <!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請求接口進(jìn)行上報(bào)?

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

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

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

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

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

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

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

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

          使用1\*1的gif[1]

          非阻塞加載

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

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

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <script>
                  (function(w{
                      w._error_storage_ = [];
                      function errorhandler(){
                          // 用于記錄當(dāng)前的錯誤            
                          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>這是一個測試頁面(new)</h1>
          </body>
          </html>

          復(fù)制代碼

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

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

          總體流程可以看為錯誤標(biāo)識 -> 錯誤過濾 -> 錯誤接收 -> 錯誤存儲。

          錯誤標(biāo)識(SDK配合)

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

          單個錯誤條目

          通過date和隨機(jī)值生成一條對應(yīng)的錯誤條目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ù)制代碼

          單個錯誤事件

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

          通過message、colno與lineno進(jìn)行相加計(jì)算阿斯克碼值,可以生成錯誤的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ù)制代碼

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

          錯誤過濾(SDK配合)

          域名過濾

          過濾本頁面script error,可能被webview插入其他js。

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

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

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

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

          // 偽代碼

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

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

          錯誤接收

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

          錯誤記錄

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

          // 偽代碼

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

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

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

          削峰機(jī)制

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

          // 偽代碼

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

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

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

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

          采樣處理

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

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

          錯誤存儲

          對于打印在了磁盤的日志,我們怎么樣才能對于其進(jìn)行聚合呢,這里得考慮使用存儲方案。

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

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

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

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

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

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

          可視分析端(可視化平臺)

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

          主功能

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

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


          image.png

          排行榜

          剛開始做了待處理錯誤列表、我的錯誤列表、已解決列表,錯誤與人沒有綁定關(guān)系,過于依賴人為主動,需要每個人主動到平臺上處理,效果不佳。

          后面通過錯誤作者排行榜,通過釘釘日報(bào)來提醒對應(yīng)人員處理。緊急錯誤,通過實(shí)時(shí)告警來責(zé)任到人,后面告警會說。

          具體原理:

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

          SourceMap

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

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

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

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


          獲取上報(bào)的line、column、source,利用第三方庫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]

          錯誤報(bào)警

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

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

          ○ 四、擴(kuò)展

          行為搜集

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

          分類

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

          搜集方式

          1. 點(diǎn)擊行為

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

          1. 發(fā)送請求

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

          1. 頁面跳轉(zhuǎn)

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

          1. 控制臺行為

          重寫console對象的info等方法。

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

          遇到的問題

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

          空日志問題

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

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

          image.png

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

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

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

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

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

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

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

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

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

          我們再想了下鏈路,除了日志庫,其他代碼基本都是我們自己的邏輯,所以對日志庫進(jìn)行了排查,懷疑其對某個字段做了什么處理。

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

          日志丟失問題

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

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

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

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

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

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

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


          關(guān)于本文

          來源:羽飛

          https://juejin.cn/post/6987681953424080926

          最后

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


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

          手機(jī)掃一掃分享

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

          手機(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>
                  黄色无码高清视频 | 天干夜天干夜天天免费视频 | 成人免费黄色片 | www.水蜜桃视频 | 伊人中文无码 |