某⼀天產品:xxx⼴告主反饋咱們的⻚⾯註冊不了! ⼜⼀天運營:這個活動在xxx媒體上掛掉了!javascript
在我司線上運行的是近億級別的廣告頁面,這樣線上若是裸奔,出現了什麼問題不知道,後置在業務端發現,被業務方詢問,這種場景很尷尬。css
公司存在四個事業部,而每一個事業部不下於3個項目,這裏至少12個項目,這裏做爲伏筆,業務線多。html
咱們是選擇本身作呢,仍是選第三方的呢。咱們比較一項幾款常見第三方。前端
以Sentry爲計費,對這12個項目計算一下。12個項目一年將近10萬。而大體估算過須要2人1.5月即90人日,能完成MVP版本,按每人1.5萬工資/月計算,總共花費4.5萬,並且是一勞永逸的。java
所以從成本角度咱們會選擇自研,但除了成本外,還有其餘緣由。例如咱們會基於這套系統作一些自定義功能,與公司權限用戶系統打通,再針對用戶進行Todo管理,對用戶進行錯誤排行等。node
還有基於業務數據的安全,咱們但願自我搭建一個系統。react
因此從成本、安全、擴展性角度,咱們選擇了本身研發。webpack
咱們要什麼樣的一個產品呢,根據第一性原理,解決關鍵問題「怎麼定位問題」。 經過5W1H法咱們來分析,咱們想要知道些什麼信息呢?git
其實錯誤監控說簡單就一句話能夠描述,蒐集頁面錯誤,進行上報,而後對症分析。web
按照5W1H法則進行分析這句話,能夠發現有幾項須要咱們關注。
首先咱們須要梳理下,咱們須要一些哪些功能。
那咱們怎麼獲得上面的信息進行最終錯誤的定位呢。
首先咱們確定須要對錯誤進行蒐集,而後用戶設備頁面端的錯誤咱們怎麼才能感知到呢,這就須要進行上報。那麼第一層就展示出來了,咱們須要一個蒐集上報端。
那怎麼才能進行上報呢,和後端協做那麼久,確定知道的吧🙃 ,你須要一個接口。那就須要一個服務器來進行對於上報的錯誤進行採集,對於錯誤進行篩選聚合。那麼第二層也知道了啊,咱們須要一個採集聚合端。
咱們蒐集到了咱們足夠的物料信息了,那接下來要怎麼用起來呢,咱們須要把它們按照咱們的規則進行整理。若是每次又是經過寫類SQL進行整理查詢效率會很低,所以咱們須要一個可視化的平臺進行展現。所以有了第三層,可視化分析端。
感受好像作完啦,想必你們都這麼想,一個錯誤監控平臺作完了,🙅 。若是是這樣你會發現一個現象,每次上線和上線後一段時間,開發同窗都一直盯着屏幕看,這是在幹嗎,人形眼動觀察者模式嗎。所以咱們須要經過代碼去解決,天然而然,第四層,監控告警端應運而生。
因此請大聲說出來咱們須要什麼🙈 ,蒐集上報端,採集聚合端,可視分析端,監控告警端。
如函數同樣,定義好每一個環節的輸入和輸出,且核心須要處理的功能。
下面咱們看看上述所說的四個端怎麼去實現呢。
這個環節主要輸入是全部錯誤,輸出是捕獲上報錯誤。核心是處理不一樣類型錯誤的蒐集工做。其餘是一些非核心但必要的工做。
先看看咱們須要處理哪些錯誤類型。
常見JS執行錯誤
解析時發生語法錯誤
// 控制檯運行
const xx,
複製代碼
window.onerror捕獲不到SyntxError,通常SyntaxError在構建階段,甚至本地開發階段就會被發現。
值不是所期待的類型
// 控制檯運行
const person = void 0
person.name
複製代碼
引用未聲明的變量
// 控制檯運行
nodefined
複製代碼
當一個值不在其所容許的範圍或者集合中
(function fn ( ) { fn() })()
複製代碼
網絡錯誤
資源加載錯誤
new Image().src = '/remote/image/notdeinfed.png'
複製代碼
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錯誤
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)
})
複製代碼
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)
}
複製代碼
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。經過如下兩種方法能給予解決。
<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);
複製代碼
<!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,有下列三種常見狀況。
調用方法場景
能夠經過封裝一個函數,能裝飾原方法,使得其能被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纔是最佳選擇。
儘可能避免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>
複製代碼
這個環節,輸入是藉口接收到的錯誤記錄,輸出是有效的數據入庫。核心功能須要對數據進行清洗,順帶解決了過多的服務壓力。另外一個核心功能是對數據進行入庫。
整體流程能夠看爲錯誤標識 -> 錯誤過濾 -> 錯誤接收 -> 錯誤存儲。
聚合以前,咱們須要有不一樣維度標識錯誤的能力,能夠理解爲定位單個錯誤條目,單個錯誤事件的能力。
單個錯誤條目
經過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;
}
複製代碼
以下圖,一個錯誤事件(事件列表),下屬每條即爲實際的錯誤條目。
域名過濾
過濾本頁面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就能夠。
剛開始作了待處理錯誤列表、個人錯誤列表、已解決列表,錯誤與人沒有綁定關係,過於依賴人爲主動,須要每一個人主動到平臺上處理,效果不佳。
後面經過錯誤做者排行榜,經過釘釘日報來提醒對應人員處理。緊急錯誤,經過實時告警來責任到人,後面告警會說。
具體原理:
利用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
}
複製代碼
感興趣SourceMap原理的,能夠繼續深刻,SourceMap 與前端異常監控。
經過蒐集用戶的操做,能夠明顯發現錯誤爲何產生。
使用addEventListener監聽全局上的click事件,將事件和DOM元素名字收集。與錯誤信息一塊兒上報。
監聽XMLHttpRequest的onreadystatechange回調函數
監聽window.onpopstate,頁面進行跳轉時會觸發。
重寫console對象的info等方法。
有興趣能夠參考行爲監控。
因爲涉及到一些隱私,下述會作脫敏處理。
上線灰度運行後,咱們發現SLS日誌存在一些空日誌😢 ,🦢,這是發生了啥?
首先咱們回憶下這個鏈路上有哪些環節可能存在問題。
排查鏈路,SLS採集環節以前有磁盤日誌收集,服務端接收,SDK上報,那咱們依次排查。
往前一步,發現磁盤日誌就已經存在空日誌,那剩下就得看一下接收端、SDK端。
開始利用控制變量法,先在SDK端進行空判斷,防止空日誌上報。結果:發現無效😅。
再繼續對Node接收端處理,對接收到的數據進行判空,若是爲空不進行日誌打印,結果:依然無效😳。
因此開始定位是否是日誌打印自己出了什麼問題?研究了下日誌第三方日誌庫的API,進行了各類嘗試,發現依舊沒用,我臉黑了🌚。
什麼狀況,「遇事不決」看源碼。排查下日誌庫源碼存在什麼問題。對於源碼的主調用流程走了一遍,並無發現什麼問題,一頭霧水🙃。
整個代碼邏輯很正常,這讓咱們開始懷疑難道是數據的問題,因而開始縮減上報的字段,最終定義爲了一個字段。發現上線後沒有問題了😢。
難道是有些字段存儲的數據過長致使的?但從代碼邏輯、流程日誌中並無反應這個錯誤的可能性。
所以咱們利用二分法,二分地增長字段,最終定位到了某個字段。若是存在某個字段上報就會出現問題。這很出乎人的意料。
咱們再想了下鏈路,除了日誌庫,其餘代碼基本都是咱們本身的邏輯,因此對日誌庫進行了排查,懷疑其對某個字段作了什麼處理。
因而經過搜索,定位到了日誌庫在僕從模式(能夠了解下Node的主從模式)下會使用某個字段來表意,致使和咱們上報的字段衝突,所以丟失了🤪。
解決了上個問題,開心了,一股成就感涌上心頭。但立刻就被當頭一棒,我發現我高興的太早了🤮。
團隊的某同窗在本地測試的時候,因爲玩的很開心,一直去刷新頁面去上報當前頁面的錯誤。但他發現本地上報的條數和實際日誌服務裏的條數對不上,日誌服務裏的少了不少。
因爲以前自身剛畢業時候作過2年多後端開發,對於IO操做丟失數據仍是有點敏感。直覺上就感受多是多進程方向的問題。懷疑是多進程致使的文件死鎖問題。
那咱們去掉多線程,經過單線程,咱們去重複原先復現問題的步驟。發現沒有遺漏🤭。
咱們發現能進行配置Cluster(主從模式)的地方有兩處,日誌庫和部署工具。
觀察日誌庫默認使用的主從進程模式,而部署工具沒有主從模式的概念,勢必會致使寫入IO的死鎖問題,致使日誌丟失。因而在想社區有沒有能夠有解決此問題的第三方支持。
而後經過谷歌搜索,很快就找到了對應的第三方庫,它能提供主人進程和僕從進程之間的消息溝通。原理是主人進程負責全部消息寫入log,而僕從進程經過消息傳遞給主人進程。
處理異常
source-map
React錯誤
Script Error
Capture and report JavaScript errors with window.onerror | Product Blog • Sentry
總體
以前開放日本身演講的