一篇講透自研的前端錯誤監控

○ 1、背景

痛點

某⼀天產品:xxx⼴告主反饋咱們的⻚⾯註冊不了! ⼜⼀天運營:這個活動在xxx媒體上掛掉了!javascript

在我司線上運行的是近億級別的廣告頁面,這樣線上若是裸奔,出現了什麼問題不知道,後置在業務端發現,被業務方詢問,這種場景很尷尬。css

選擇

公司存在四個事業部,而每一個事業部不下於3個項目,這裏至少12個項目,這裏做爲伏筆,業務線多。html

咱們是選擇本身作呢,仍是選第三方的呢。咱們比較一項幾款常見第三方。前端

  • Fundebug:付費版 159元/月起,數據存在第三方,而數據自我保存須要 30 萬/年。仍是很貴的。
  • FrontJS,FrontJS 高級版 899/月,專業版是 2999/月。
  • Sentry,80 美金/月。

以Sentry爲計費,對這12個項目計算一下。12個項目一年將近10萬。而大體估算過須要2人1.5月即90人日,能完成MVP版本,按每人1.5萬工資/月計算,總共花費4.5萬,並且是一勞永逸的。java

所以從成本角度咱們會選擇自研,但除了成本外,還有其餘緣由。例如咱們會基於這套系統作一些自定義功能,與公司權限用戶系統打通,再針對用戶進行Todo管理,對用戶進行錯誤排行等。node

還有基於業務數據的安全,咱們但願自我搭建一個系統。react

因此從成本、安全、擴展性角度,咱們選擇了本身研發。webpack

○ 2、產品設計

咱們要什麼樣的一個產品呢,根據第一性原理,解決關鍵問題「怎麼定位問題」。 經過5W1H法咱們來分析,咱們想要知道些什麼信息呢?git

錯誤信息

其實錯誤監控說簡單就一句話能夠描述,蒐集頁面錯誤,進行上報,而後對症分析。web

按照5W1H法則進行分析這句話,能夠發現有幾項須要咱們關注。

  1. What,發⽣了什麼錯誤:邏輯錯誤、數據錯誤、⽹絡錯誤、語法錯誤等。
  2. When,出現的時間段,如時間戳。
  3. Who,影響了多少用戶,包括報錯事件數、IP、設備信息。
  4. Where,出現的頁面是哪些,包括頁面、廣告位(我司)、媒體(我司)。
  5. Why,錯誤的緣由是爲何,包括錯誤堆棧、⾏列、SourceMap。
  6. How,怎麼定位解決問題,咱們還須要收集系統等信息。

架構層次

首先咱們須要梳理下,咱們須要一些哪些功能。

那咱們怎麼獲得上面的信息進行最終錯誤的定位呢。

首先咱們確定須要對錯誤進行蒐集,而後用戶設備頁面端的錯誤咱們怎麼才能感知到呢,這就須要進行上報。那麼第一層就展示出來了,咱們須要一個蒐集上報端。

那怎麼才能進行上報呢,和後端協做那麼久,確定知道的吧🙃 ,你須要一個接口。那就須要一個服務器來進行對於上報的錯誤進行採集,對於錯誤進行篩選聚合。那麼第二層也知道了啊,咱們須要一個採集聚合端。

咱們蒐集到了咱們足夠的物料信息了,那接下來要怎麼用起來呢,咱們須要把它們按照咱們的規則進行整理。若是每次又是經過寫類SQL進行整理查詢效率會很低,所以咱們須要一個可視化的平臺進行展現。所以有了第三層,可視化分析端。

感受好像作完啦,想必你們都這麼想,一個錯誤監控平臺作完了,🙅 。若是是這樣你會發現一個現象,每次上線和上線後一段時間,開發同窗都一直盯着屏幕看,這是在幹嗎,人形眼動觀察者模式嗎。所以咱們須要經過代碼去解決,天然而然,第四層,監控告警端應運而生。

因此請大聲說出來咱們須要什麼🙈 ,蒐集上報端,採集聚合端,可視分析端,監控告警端。

○ 3、系統設計

如函數同樣,定義好每一個環節的輸入和輸出,且核心須要處理的功能。

下面咱們看看上述所說的四個端怎麼去實現呢。

蒐集上報端(SDK)

這個環節主要輸入是全部錯誤,輸出是捕獲上報錯誤。核心是處理不一樣類型錯誤的蒐集工做。其餘是一些非核心但必要的工做。

錯誤類型

先看看咱們須要處理哪些錯誤類型。

常見JS執行錯誤

  1. SyntaxError

解析時發生語法錯誤

// 控制檯運行
const xx,
複製代碼

window.onerror捕獲不到SyntxError,通常SyntaxError在構建階段,甚至本地開發階段就會被發現。

  1. TypeError

值不是所期待的類型

// 控制檯運行
const person = void 0
person.name
複製代碼
  1. ReferenceError

引用未聲明的變量

// 控制檯運行
nodefined
複製代碼
  1. RangeError

當一個值不在其所容許的範圍或者集合中

(function fn ( ) { fn() })()
複製代碼

網絡錯誤

  1. ResourceError

資源加載錯誤

new Image().src = '/remote/image/notdeinfed.png'
複製代碼
  1. HttpError

Http請求錯誤

// 控制檯運行
fetch('/remote/notdefined', {})
複製代碼

蒐集錯誤

全部原由來源於錯誤,那咱們如何進行錯誤捕獲。

try/catch

能捕獲常規運行時錯誤,語法錯誤和異步錯誤不行

// 常規運行時錯誤,能夠捕獲 ✅
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);
}
複製代碼

try/catch有它細緻處理的優點,但缺點也比較明顯。

window.onerror

pure js錯誤收集,window.onerror,當 JS 運行時錯誤發生時,window 會觸發一個 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});
}
複製代碼

先驗證下幾個錯誤是否能夠捕獲。 ​

// 常規運行時錯誤,能夠捕獲 ✅

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/image/kkk.png"> 複製代碼

window.onerror 不能捕獲資源錯誤怎麼辦? ​

window.addEventListener

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

// 圖片、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>
複製代碼

new Image運用的比較少,能夠單獨本身處理本身的錯誤。 ​

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

Promise錯誤

  1. 普通Promise錯誤

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

// try/catch 不能處理 JSON.parse 的錯誤,由於它在 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)
})
複製代碼
  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)
}
複製代碼
  1. import chunk錯誤

import其實返回的也是一個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)
}
複製代碼

小結:全局捕獲Promise中的錯誤

以上三種其實歸結爲Promise類型錯誤,能夠經過unhandledrejection捕獲

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

爲了防止有漏掉的 Promise 異常,可經過unhandledrejection用來全局監聽Uncaught Promise Error。 ​

Vue錯誤

因爲Vue會捕獲全部Vue單文件組件或者Vue.extend繼承的代碼,因此在Vue裏面出現的錯誤,並不會直接被window.onerror捕獲,而是會拋給Vue.config.errorHandler。

/** * 全局捕獲Vue錯誤,直接扔出給onerror處理 */
Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })
}
複製代碼

React錯誤

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

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

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以顯示降級後的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你一樣能夠將錯誤日誌上報給服務器
    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>  
    )
  }
}
複製代碼

但error boundaries並不會捕捉如下錯誤:React事件處理,異步代碼,error boundaries本身拋出的錯誤。

跨域問題

通常狀況,若是出現 Script error 這樣的錯誤,基本上能夠肯定是出現了跨域問題。

若是當前投放頁面和雲端JS所在不一樣域名,若是雲端JS出現錯誤,window.onerror會出現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);
複製代碼
  • 若是不能修改服務端的請求頭,能夠考慮經過使用 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(); // 調用testerror.js中定義的foo方法 } catch (e) { throw e; } </script>
</body>
</html>
複製代碼

會發現若是不加try catch,console.log就會打印script error。加上try catch就能捕獲到。 ​

咱們捋一下場景,通常調用遠端js,有下列三種常見狀況。

  • 調用遠端JS的方法出錯
  • 遠端JS內部的事件出問題
  • 要麼在setTimeout等回調內出錯

調用方法場景

能夠經過封裝一個函數,能裝飾原方法,使得其能被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(this, arguments); } catch (e) { throw e; // re-throw the error } }; } return fn.__wrapped__; } wrapErrors(foo)() </script>
</body>
</html>

複製代碼

你們能夠嘗試去掉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>
複製代碼

你們能夠嘗試去掉封裝EventTarget.prototype.addEventListener的那段代碼,感覺下。

上報接口

爲何不能直接用GET/POST/HEAD請求接口進行上報? ​

這個比較容易想到緣由。通常而言,打點域名都不是當前域名,因此全部的接口請求都會構成跨域。 ​

爲何不能用請求其餘的文件資源(js/css/ttf)的方式進行上報? ​

建立資源節點後只有將對象注入到瀏覽器DOM樹後,瀏覽器纔會實際發送資源請求。並且載入js/css資源還會阻塞頁面渲染,影響用戶體驗。 ​

構造圖片打點不只不用插入DOM,只要在js中new出Image對象就能發起請求,並且尚未阻塞問題,在沒有js的瀏覽器環境中也能經過img標籤正常打點。

使用new Image進行接口上報。最後一個問題,一樣都是圖片,上報時選用了1x1的透明GIF,而不是其餘的PNG/JEPG/BMP文件。 ​

首先,1x1像素是最小的合法圖片。並且,由於是經過圖片打點,因此圖片最好是透明的,這樣一來不會影響頁面自己展現效果,兩者表示圖片透明只要使用一個二進制位標記圖片是透明色便可,不用存儲色彩空間數據,能夠節約體積。由於須要透明色,因此能夠直接排除JEPG。 ​

一樣的響應,GIF能夠比BMP節約41%的流量,比PNG節約35%的流量。GIF纔是最佳選擇。

  • 能夠進行跨域
  • 不會攜帶cookie
  • 不須要等待服務器返回數據

使用1*1的gif

非阻塞加載

儘可能避免SDK的js資源加載影響。

經過先把window.onerror的錯誤記錄進行緩存,而後異步進行SDK的加載,再在SDK裏面處理錯誤上報。

<!DOCTYPE html>
<html lang="en"> <head> <script> (function(w) { w._error_storage_ = []; function errorhandler(){ // 用於記錄當前的錯誤  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>
複製代碼

採集聚合端(日誌服務器)

這個環節,輸入是藉口接收到的錯誤記錄,輸出是有效的數據入庫。核心功能須要對數據進行清洗,順帶解決了過多的服務壓力。另外一個核心功能是對數據進行入庫。

整體流程能夠看爲錯誤標識 -> 錯誤過濾 -> 錯誤接收 -> 錯誤存儲。

錯誤標識(SDK配合)

聚合以前,咱們須要有不一樣維度標識錯誤的能力,能夠理解爲定位單個錯誤條目,單個錯誤事件的能力。 ​

單個錯誤條目

經過date和隨機值生成一條對應的錯誤條目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;
}
複製代碼

單個錯誤事件

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

經過message、colno與lineno進行相加計算阿斯克碼值,能夠生成錯誤的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;
}
複製代碼

以下圖,一個錯誤事件(事件列表),下屬每條即爲實際的錯誤條目。 image.png

錯誤過濾(SDK配合)

域名過濾

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

咱們只關心本身的遠端JS問題,所以作了根據本公司域名進行過濾。

// 僞代碼
if(!e.filename || !e.filename.match(/^(http|https):\/\/yun./)) return true
複製代碼

重複上報

怎麼避免重複的數據上報?根據errorKey來進行緩存,重複的錯誤避免上報的次數超過閾值。

// 僞代碼

const localStorage = window.localStorage;
const TIMES = 6; // 緩存條數

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

錯誤接收

在處理接收接口的時候,注意流量的控制,這也是後端開發須要投入最多精力的地方,處理高併發的流量。

錯誤記錄

接收端使用Koa,簡單的實現了接收及打印到磁盤。

// 僞代碼

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

  ctx.type = 'application/json';
  ctx.body = { code: '1', msg: '數據上報成功' };

  // 進行日誌記錄到磁盤的代碼,根據本身的日誌庫選擇
};
複製代碼

削峯機制

好比每秒設置2000的閾值,而後根據請求量減小上限,定時重置上限。

// 僞代碼

// 1000ms
const TICK = 1000;
// 1秒上限爲2000
const MAX_LIMIT = 2000;
// 每臺服務器請求上限值
let maxLimit = MAX_LIMIT;

/** * 啓動重置函數 */
const task = () => {
  setTimeout(() => {
    maxLimit = MAX_LIMIT;
    task();
  }, TICK);
};
task();

const check = () => {
  if (maxLimit <= 0) {
    throw new Error('超過上報次數');
  }
  maxLimit--;
  // 執行業務代碼。。。
};
複製代碼

採樣處理

超過閾值,還能夠進行採樣收集。

// 只採集 20%
if(Math.random() < 0.2) {
  collect(data)      // 記錄錯誤信息
}
複製代碼

錯誤存儲

對於打印在了磁盤的日誌,咱們怎麼樣才能對於其進行聚合呢,這裏得考慮使用存儲方案。

通常選擇了存儲方案後,設置好配置,存儲方案就能夠經過磁盤定時週期性的獲取數據。所以咱們須要選擇一款存儲方案。

對於存儲方案,咱們對比了平常常見方案,阿里雲日誌服務 - Log Service(SLS)、ELK(Elastic、Logstash、Kibana)、Hadoop/Hive(將數據存儲在 Hadoop,利用 Hive 進行查詢) 類方案的對比。

從如下方面進行了對比,最終選擇了Log Service,主要考慮爲無需搭建,成本低,查詢功能知足。

功能項 ELK 類系統 Hadoop + Hive 日誌服務
日誌延時 1~60 秒 幾分鐘~數小時 實時
查詢延時 小於 1 秒 分鐘級 小於 1 秒
查詢能力
擴展性 提早預備機器 提早預備機器 秒級 10 倍擴容
成本 較高 較低 很低

日誌延時:日誌產生後,多久可查詢。 查詢延時:單位時間掃描數據量。 查詢能力:關鍵詞查詢、條件組合查詢、模糊查詢、數值比較、上下文查詢。 擴展性:快速應對百倍流量上漲。 成本:每 GB 費用。

具體API使用,可查看日誌服務

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

這個環節,輸入是藉口接收到的錯誤記錄,輸出是有效的數據入庫。核心功能須要對數據進行清洗,順帶解決了過多的服務壓力。另外一個核心功能是對數據進行入庫。

主功能

這部分主要是產品功能的合理設計,作到小而美,具體的怎麼聚合,參考阿里雲SLS就能夠。

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

image.png

排行榜

剛開始作了待處理錯誤列表、個人錯誤列表、已解決列表,錯誤與人沒有綁定關係,過於依賴人爲主動,須要每一個人主動到平臺上處理,效果不佳。 ​

後面經過錯誤做者排行榜,經過釘釘日報來提醒對應人員處理。緊急錯誤,經過實時告警來責任到人,後面告警會說。

具體原理:

  • webpack打包經過git命令把做者和做者郵箱、時間打包在頭部。
  • 在可視化服務中,去請求對應的報錯url匹配到對應做者,返回給展現端。

image.png

SourceMap

利用webpack的hidden-source-map構建。與 source-map 相比少了末尾的註釋,但 output 目錄下的 index.js.map 沒有少。線上環境避免source-map泄露。

webpackJsonp([1],[
  function(e,t,i){...},
  function(e,t,i){...},
  function(e,t,i){...},
  function(e,t,i){...},
  ...
])
// 這裏沒有生成source-map的連接地址
複製代碼

根據報錯文件的url,根據團隊內部約定好的目錄和規則,定位以前打包上傳的sourceMap地址。

const sourcemapUrl = ('xxxfolder/' + url + 'xxxHash' +'.map')
複製代碼

獲取上報的line、column、source,利用第三方庫sourceMap進行定位。

const sourceMap = require('source-map')

// 根據行數獲取源文件行數
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
}
複製代碼

image.png 感興趣SourceMap原理的,能夠繼續深刻,SourceMap 與前端異常監控

錯誤報警

報警設置

  1. 每條業務線設置本身的閾值、錯誤時間跨度,報警輪詢間隔
  2. 經過釘釘hook報警到對應的羣
  3. 經過日報形式報出錯誤做者排行榜

image.png

○ 4、擴展

行爲蒐集

經過蒐集用戶的操做,能夠明顯發現錯誤爲何產生。 image.png

分類

  • UI行爲: 點擊、滾動、聚焦/失焦、長按
  • 瀏覽器行爲:請求、前進/後退、跳轉、新開頁面、關閉
  • 控制檯行爲:log、warn、error

蒐集方式

  1. 點擊行爲

使用addEventListener監聽全局上的click事件,將事件和DOM元素名字收集。與錯誤信息一塊兒上報。

  1. 發送請求

監聽XMLHttpRequest的onreadystatechange回調函數

  1. 頁面跳轉

監聽window.onpopstate,頁面進行跳轉時會觸發。

  1. 控制檯行爲

重寫console對象的info等方法。 ​

有興趣能夠參考行爲監控

遇到的問題

因爲涉及到一些隱私,下述會作脫敏處理。

空日誌問題

上線灰度運行後,咱們發現SLS日誌存在一些空日誌😢 ,🦢,這是發生了啥?

首先咱們回憶下這個鏈路上有哪些環節可能存在問題。

image.png

排查鏈路,SLS採集環節以前有磁盤日誌收集,服務端接收,SDK上報,那咱們依次排查。 ​

image.png 往前一步,發現磁盤日誌就已經存在空日誌,那剩下就得看一下接收端、SDK端。

開始利用控制變量法,先在SDK端進行空判斷,防止空日誌上報。結果:發現無效😅。 ​

再繼續對Node接收端處理,對接收到的數據進行判空,若是爲空不進行日誌打印,結果:依然無效😳。 ​

因此開始定位是否是日誌打印自己出了什麼問題?研究了下日誌第三方日誌庫的API,進行了各類嘗試,發現依舊沒用,我臉黑了🌚。

什麼狀況,「遇事不決」看源碼。排查下日誌庫源碼存在什麼問題。對於源碼的主調用流程走了一遍,並無發現什麼問題,一頭霧水🙃。 ​

整個代碼邏輯很正常,這讓咱們開始懷疑難道是數據的問題,因而開始縮減上報的字段,最終定義爲了一個字段。發現上線後沒有問題了😢。 ​

難道是有些字段存儲的數據過長致使的?但從代碼邏輯、流程日誌中並無反應這個錯誤的可能性。 ​

所以咱們利用二分法,二分地增長字段,最終定位到了某個字段。若是存在某個字段上報就會出現問題。這很出乎人的意料。 ​

咱們再想了下鏈路,除了日誌庫,其餘代碼基本都是咱們本身的邏輯,因此對日誌庫進行了排查,懷疑其對某個字段作了什麼處理。 ​

因而經過搜索,定位到了日誌庫在僕從模式(能夠了解下Node的主從模式)下會使用某個字段來表意,致使和咱們上報的字段衝突,所以丟失了🤪。

日誌丟失問題

解決了上個問題,開心了,一股成就感涌上心頭。但立刻就被當頭一棒,我發現我高興的太早了🤮。

團隊的某同窗在本地測試的時候,因爲玩的很開心,一直去刷新頁面去上報當前頁面的錯誤。但他發現本地上報的條數和實際日誌服務裏的條數對不上,日誌服務裏的少了不少。

因爲以前自身剛畢業時候作過2年多後端開發,對於IO操做丟失數據仍是有點敏感。直覺上就感受多是多進程方向的問題。懷疑是多進程致使的文件死鎖問題。

那咱們去掉多線程,經過單線程,咱們去重複原先復現問題的步驟。發現沒有遺漏🤭。

咱們發現能進行配置Cluster(主從模式)的地方有兩處,日誌庫和部署工具。

觀察日誌庫默認使用的主從進程模式,而部署工具沒有主從模式的概念,勢必會致使寫入IO的死鎖問題,致使日誌丟失。因而在想社區有沒有能夠有解決此問題的第三方支持。

而後經過谷歌搜索,很快就找到了對應的第三方庫,它能提供主人進程和僕從進程之間的消息溝通。原理是主人進程負責全部消息寫入log,而僕從進程經過消息傳遞給主人進程。

○ 5、推薦閱讀及引用

處理異常

如何優雅處理前端異常?

source-map

SourceMap 與前端異常監控

React錯誤

React,優雅的捕獲異常

Script Error

Capture and report JavaScript errors with window.onerror | Product Blog • Sentry

What the heck is "Script error"? | Product Blog • Sentry

總體

前端搞監控|Allan - 如何實現一套多端錯誤監控平臺

一步一步搭建前端監控系統:JS錯誤監控篇

擼一個前端監控系統

以前開放日本身演講的

結束裸奔ppt

相關文章
相關標籤/搜索