使用 sentry 做异常监控
sentry 异常监控原理
异常详情获取
sentry通过覆写 window.onerror
和 window.unhandlerejection
这两个api
来实现自动捕获异常
1 | //覆写window.onerror |
为了在捕获到异常时能获取更详尽的信息 被捕获的异常需要带上一些标记(event name | event target
等)
这里我们先看下一个使用使用频繁的函数 wrap
sentry
就是通过它对callback
进行trycatch
的
1 | //https://github1s.com/getsentry/sentry-javascript/blob/master/packages/browser/src/helpers.ts |
- 标记
xhr
接口回调
为了标记xhr
接口回调 需要先对XMLHttpRequest.prototype.send
方法劫持覆写 等到实例是用了被覆写的方法后 就会对xhr
对象的 onload | onerror | onprogress | onreadystateChange
等进行覆写 使用 try..catch..
传入捕获异常的callback
1 | //先对send进行覆写 |
- 实现
setTimeout | setInterval | requestAnimationFrame
通过覆写这些原生方法 在调用时会触发 try...catch
的 callback
1 | //实现setTimeout的覆写 |
- 标记
dom
事件
1 | function _warpEventTarget(target: string): void { |
行为获取
sentry 接入应用以后 收集用户的行为(页面跳转、click、keypress、fetch/xhr、console等行为
)然后和异常信息一起上报 而sentry
通过覆写对应的api
来实现效果
页面跳转
通过覆写 window.onpopstate \ history.pushstate / history.replaceState
1 | //window.onpopstate |
click | keypress
通过
document
代理click、keypress
事件来收集click、keypress
通过劫持
addEventListener
来收集click、keypress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51//代理DOM
function instructDOM();void {
if( !('document' in WINDOW)) { //如果不支持document
return;
}
//triggerDOMHandler用来收集用户click/keypress行为
var triggerDOMHandler = triggerHandlers.bind(null, 'dom');
var globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true);
// 通过 document 代理 click、keypress 事件的方式收集 click、keypress 行为
document.addEventListener('click', globalDOMEventHandler, false);
document.addEventListener('keypress', globalDOMEventHandler, false);
['EventTarget', 'Node'].forEach(function (target) {
var proto = window[target] && window[target].prototype;
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
return;
}
// 劫持覆写 Node.prototype.addEventListener 和 EventTarget.prototype.addEventListener
fill(proto, 'addEventListener', function (originalAddEventListener) {
// 返回新的 addEventListener 覆写原生的 addEventListener
return function (type, listener, options) {
// click、keypress 事件,要做特殊处理,
if (type === 'click' || type == 'keypress') {
try {
var el = this;
var handlers_1 = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {});
var handlerForType = (handlers_1[type] = handlers_1[type] || { refCount: 0 });
// 如果没有收集过 click、keypress 行为
if (!handlerForType.handler) {
var handler = makeDOMEventHandler(triggerDOMHandler);
handlerForType.handler = handler;
originalAddEventListener.call(this, type, handler, options);
}
handlerForType.refCount += 1;
}
catch (e) {
// Accessing dom properties is always fragile.
// Also allows us to skip `addEventListenrs` calls with no proper `this` context.
}
}
// 使用原生的 addEventListener 方法注册事件
return originalAddEventListener.call(this, type, listener, options);
};
});
...
});
}收集 fetch/xhr 接口行为
sentry
对原生的fetch
和xhr
做了劫持覆写1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87//劫持fetch
var originFetch = window.fetch;
window.fetch = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// 获取接口 url、method 类型、参数、接口调用时间信息
var handlerData = {
args: args,
fetchData: {
method: getFetchMethod(args),
url: getFetchUrl(args),
},
startTimestamp: Date.now(),
};
// 收集接口调用信息
triggerHandlers('fetch', __assign({}, handlerData));
return originalFetch.apply(window, args).then(function (response) {
// 接口请求成功,收集返回数据
triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response }));
return response;
}, function (error) {
// 接口请求失败,收集接口异常数据
triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: error }));
throw error;
});
}
//劫持xhr 主要通过劫持覆写open | send 方法实现接收集接口请求的行为
//当调用open 实际调用的是覆写的open 而覆写的open内部又覆写了onreadystatechange 这样就可以收集到接口请求返回的结果
function instrumentXHR() {
...
var xhrproto = XMLHttpRequest.prototype;
// 覆写 XMLHttpRequest.prototype.open
fill(xhrproto, 'open', function (originalOpen) {
return function () {
...
var onreadystatechangeHandler = function () {
if (xhr.readyState === 4) {
...
// 收集接口调用结果
triggerHandlers('xhr', {
args: args,
endTimestamp: Date.now(),
startTimestamp: Date.now(),
xhr: xhr,
});
}
};
// 覆写 onreadystatechange
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
fill(xhr, 'onreadystatechange', function (original) {
return function () {
var readyStateArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
readyStateArgs[_i] = arguments[_i];
}
onreadystatechangeHandler();
return original.apply(xhr, readyStateArgs);
};
});
}
else {
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
return originalOpen.apply(xhr, args);
};
});
// 覆写 XMLHttpRequest.prototype.send
fill(xhrproto, 'send', function (originalSend) {
return function () {
...
// 收集接口调用行为
triggerHandlers('xhr', {
args: args,
startTimestamp: Date.now(),
xhr: this,
});
return originalSend.apply(this, args);
};
});
}收集 console
实际上就是对
debug/info/warn/error/log/assert
这些api
进行覆写1
2
3
4
5
6
7
8
9
10
11
12
13var originConsoleLog = console.log;
console.log = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// 收集 console.log 行为
triggerHandlers("console", { args: args, level: "log" });
if (originConsoleLog) {
originConsoleLog.apply(console, args);
}
};使用 Sentry 上报异常
1.在 sentry 构建一个项目 项目会自动生成一个
dns
这个dns
在项目接入 Sentry 使用2.使用
Sentry
提供的init
接入就行1
2
3
4
5
6//以react为例
(Sentry as any).init({
dsn: "https://83c5abfb9bc54d708ce01bd9993eeddf@o1424804.ingest.sentry.io/4504083781582848",
integrations: [new BrowserTracing()],
tracesSampleRate: 1.0
});3.接下来就能使用
sentry
来接收异常了4.接入飞书平台 使用飞书群聊功能自动通知人员
创建异常上报群并设置群聊机器人
生成机器人的
webhook
记住保存后面用sentry 平台设置对应的
webhook
创建飞书捷径并设置操作
接下来 只要项目发生异常 就会通知
sentry
sentry
就会将异常信息发布到飞书通知群通知对应人员去修复
使用 sentry 做性能分析
常见的性能优化指标及获取方式
1 | const timing = window.performance.timing; |
页面何时开始渲染 FP & FCP
first paint
表示页面开始首次绘制的时间戳 值越小越好 在FP
事件点之前 用户看到的是导航之前的页面first contentful paint
表示首次绘制任何文本、图像、非空白canvas
或者SVG
的时间点 值越小越好
可以通过 perfermance.getEntry | performance.getEntriesByName | performanceObserver
来获取
1 | performance.getEntries().filter(item => item.name === 'first-paint')[0]; // 获取 FP 时间 |
页面何时渲染主要内容 SI & LCP
LCP
页面首次加载时最大元素的绘制事件点 可以通过performanceObserver
获取```js
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {console.log("LCP candidate", entry.startTime, entry);
}
}).observe({ type: “largest-contentful-paint”, buffered: true });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#### 页面可以交互 TTI & TBT
- `time to ineractive`可交互时间 用于测量页面从开始加载到主要资源加载完成渲染 并能快速、可靠地响应用户输入所需的时间
- `total blocking time` 总的阻塞时间
#### 交互是否有延迟 FID & LONG TASK
- `FID` `first input delay` 衡量从用户第一次与页面交互(比如点击链接 按钮 自定义控件)直到浏览器对交互做出响应 并实际能够开始处理事件处理程序的事件 通过 `performanceObserver`获取
```js
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log("FID candidate", delay, entry);
}
}).observer({ type: "first-input", buffered: true });Long Task
衡量用户在使用过程中遇到的交互延迟 阻塞情况 可以告诉我们那些任务耗时过久(一般超过50ms
就是长任务)```js
new PerformanceObserver(function(list) {var perfEntries = list.getEntries(); for (var i = 0; i < perfEntries.length; i++) { ... }
})observe({ type: ‘longtask’});
1
2
3
4
5
6
7
8
9
10
11
12
#### 页面是否稳定 CLS
- `CLS Cumulative Layout Shift` 用于测量整个页面生命周期内发生的意外布局偏移中最大一连串的布局偏移情况
```js
new PerformanceObserver(function(list) {
var perfEntries = list.getEntries();
for (var i = 0; i < perfEntries.length; i++) {
...
}
})observe({type: 'layout-shift', buffered: true});
性能分析关键指标
lighthouse
:TCP \ LCP \SI \TTI \ TBT \ CLS
Sentry
:FCP \ LCP \ FID \ CLS
Sentry 性能监控的原理
通过 window.performance.getEntries \ performanceObserver
获取用户在使用应用过程中涉及的 load
相关 FCP / LCP / FID / CLS
等指标数据 然后上报 监控平台拿到数据后 通过可视化的方式展示指标数据 帮助我们分析
sentry
将性能指标数据分为两部分 首屏加载相关和页面切换相关
首屏加载(pageload)
1.应用加载时使用 sentry.init
方法进行初始化
在初始化时 通过setTimeout
实现首屏完成后再上报首屏性能指标数据 (默认为1000ms
如果我们的首屏时间超过1000ms
则需要手动设置timeout
)
1 | Sentry.init({ |
2.在setTimeout
的callback
中通过 window.performance.getEntries | performanceObserver
获取性能指标数据 然后通过接口上报
页面切换(navigation)
1.在 Sentry.init
初始化过程中对 history.pushState / history.replaceState / window.onpopState
进行覆写 拦截路由切换操作
1 | function historyReplacementFunction(originalHistoryFunction: () => void): () => void { |
2.页面切换以后 window.performance.genEntires
获取性能指标数据 然后通过接口上报 通过performance。getEntries
获取性能指标数据时 sentry 会记录上次上报时的oldIndex
等到下次上报 oldIndex + 1
开始获取指标性能数据
使用 sentry 进行性能分析
1 | //以react为例 |
怎么做性能优化
优化性能 分为让用户更快地看到页面内容(FCP/ LCP
)、更早、更流畅地操作页面(FID/TTI/TTB
)以及更好的视觉体验(CLS
)
LCP &FCP
优化 FCP
- 减少服务器响应时间-:避免多次重定向 提前建立连接
preconnect
、dns
预解析、http2
、 使用高效的缓存策略、使用CDN
使用SSG
代替SSR
- 优化加载速度:预加载关键资源 压缩
js、css、图片等静态资源
、移除未使用的资源 - 延迟加载未使用的资源:
defer/async
、懒加载 - 减少
js
的阻塞渲染: 尽快尽早地加载需要的资源、使用worker
- 在请求数和请求文件之间寻找最佳的平衡点
- 避免
DOM
过大 - 减少关键请求的深度
优化 LCP
优化LCP
除了上面的手段 还可以将客户端渲染改成服务端渲染 提前将页面主体渲染出来
优化 FID / TTI / TBT
更关键的是 js 的阻塞时间
- 优化资源加载速度:预加载
js
资源 压缩js
大小 使用CDN
使用缓存 - 减少
js
执行时间: 延迟加载未使用的js
使用worker
- 减少关键请求的深度
CLS
- 提前确定好
img、视频
等媒体节点的尺寸 - 首选
transform
动画 而不是触发布局偏移(引发回流重绘)的属性动画