使用Sentry做监控和性能分析


使用 sentry 做异常监控

sentry 异常监控原理

异常详情获取

sentry通过覆写 window.onerrorwindow.unhandlerejection这两个api来实现自动捕获异常

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
//覆写window.onerror
oldErrorHandler = window.onerror;
window.onerror = function (msg, url, line, colum, error) {
//收集异常信息并上报
triggerHandlers("error", {
column,
error,
line,
msg,
url,
});
if (oldErrorHandler) {
return oldErrorHandler.apply(this, arguments);
}
return false;
};
//覆写window.unhandledrejection
oldOnUnhandleRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function (e) {
//收集异常信息并上报
triggerHandler("unhandledrejection", e);
if (oldOnUnhandledRejectionHandler) {
return oldUnhandleRejectionHandler.apply(this, arguments);
}
return true;
};

为了在捕获到异常时能获取更详尽的信息 被捕获的异常需要带上一些标记(event name | event target等)

这里我们先看下一个使用使用频繁的函数 wrap sentry就是通过它对callback进行trycatch

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
//https://github1s.com/getsentry/sentry-javascript/blob/master/packages/browser/src/helpers.ts
export function wrap(
fn: WrappedFunction,
options: {
mechainism?: Mechanism;
} = {},
before?: WrappedFunction
) {
if(typeof fn !== 'function') {
return fn; //如果传入的不是函数 则直接返回
}
//对传入的callback进行trycatch处理
try{
//判断callback是否之前就被处理过了 如果已经处理过就直接返回结束流程
const wrapper = fn._sentry_wrapped_;
if(wrapper) {
return wrapper
}
//如果我们不想对该函数进行二次包装
if(getOriginalFunction(fn)) {
return fn;
}
} catch(e) {
return fn;
}
//注意 这里不能用箭头函数 因为需要保存this的上下文环境
const sentryWrapped: WrappedFunction = function (this: unknow): void {
const args = Array.prototype.slice.call(arguments)
try { //判断是否有before预处理函数
if(before && typeof before === 'function') {
before.apply(this, arguments);
}
const wrappedArguments = args.map(arg => wrap(arg, options))
}
}
//......
}
  • 标记xhr接口回调

为了标记xhr接口回调 需要先对XMLHttpRequest.prototype.send方法劫持覆写 等到实例是用了被覆写的方法后 就会对xhr对象的 onload | onerror | onprogress | onreadystateChange等进行覆写 使用 try..catch..传入捕获异常的callback

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
 //先对send进行覆写
if(this._options.XMLHttpRequest && 'XMLHttpRequest' in WINDOW) {
fill(XMLHttpRequest.propotype, 'send', _wraqXHR);
}
//使用到了这个实例的send 就会对后面的onlaod onerror onprogress onreadystateChange进行覆写
function _warpXHR(originalSend: () => void): () => void{
return function (this: XMLHttpRequest, ...args: any[]):void {
const xhr = this;//保存this
const xmlHttpRequestProps: XMLHttpRequestProp[] = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
//分别对 onload onerror onprogress onreadystatechange四个事件进行覆写
xmlHttpRequestProps.forEach(prop => {
if(prop in xhr && typeop xhr == 'function') {
fill(xhr, prop, function(original: WrappedFunction): () => any {
const warpOptions = {
mechanism: {
data: {
function: prop,
handler: getFunctionName(original)
},
handled: true,
type: 'instrument'
}
};
//如果这个方法在之前已经被trycatch调用过了 则直接获取原始函数的名称
const originalFunction = getOriginalFunction(original);
if(originalFunction) {
wrapOptions.mechanism.data.handler = getFucntionName(originalFunction)
}
return wrap(original, wrapOptions)

})
}
})
return oriinalSend.apply(this, args)
}
}
  • 实现 setTimeout | setInterval | requestAnimationFrame

通过覆写这些原生方法 在调用时会触发 try...catchcallback

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
//实现setTimeout的覆写
if(this._options.setTimeout) {
fill(WINDOW, 'setTimeout', _warpTimeFunction)
};
function _wrapTimeFunction(original: () => void): () => number {
return function(this: any, ...args: any: []): number {
const originalCallback = args[0]; //读取callback
//wrap会对传入的函数进行trrycatch的处理
args[0] = wrap(originalCallback, {
mechanism: {
data: {function: getFunctionName(original)},
handled: true,
type: 'increment'
}
});
return original.apply(this, args)

}
}
//实现setInterval的覆写 setInterval是和setTimeout一样的逻辑
if (this._options.setInterval) {
fill(WINDOW, 'setInterval', _wrapTimeFunction);
}
//实现requestAnimationFrame的覆写
if (this._options.requestAnimationFrame) {
fill(WINDOW, 'requestAnimationFrame', _wrapRAF);
}
function _wrapRAF(original: any): (callback: () => void) => any {
return function (this: any, callback: () => void): () => void {
return original.apply(this, [
wrap(callback, {
mechanism: {
data: {
function: 'requestAnimationFrame',
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
}),
]);
};
}
  • 标记dom事件
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
function _warpEventTarget(target: string): void {
const globalObject = WINDOW as {[key: string]: any}'
const proto = globalObject[target] && globalObject[target].prototype;
if(!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
//如果是null或者是createObject(null)或者是没有addEventListener的属性 则直接返回
return;
}
//对adEventListener进行覆写
fill(proto, 'addEventListener', function(original) {
try {
if(typeof fn.handleEvent === 'function') {
//使用wrap处理handle
fn.handleEvent = wrap$1(fn.handleEvent.bind(fn), {
mechanism: {
data: {
function: 'handleEvent',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
});

}
} catch (e) {

}
return original.apply(this, {
eventName,
wrap(fn,
mechanism: {
data: {
function: 'addEventListener',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
}),
options,
]);
)
})
})
}

行为获取

sentry 接入应用以后 收集用户的行为(页面跳转、click、keypress、fetch/xhr、console等行为)然后和异常信息一起上报 而sentry通过覆写对应的api来实现效果

页面跳转

通过覆写 window.onpopstate \ history.pushstate / history.replaceState

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
//window.onpopstate
const oldOnPopState = WINDOW.onpopstate;
WINDOW.onpopstate = function(this, ...args) {
const to = WINDOW.href;
const form = lastHref;
lastHref = to;
//收集页面跳转行为
triggerHandlers('history', {
from,
to
})
if(oldOnPropState) {
try {
//使用原生的popstate
return oldOnPopState.apply(this, args);
} catch (_oO) {
// no-empty
}
}
}
//覆写 pushState replaceState
// 保存原生的 pushState 方法
var originPushState = window.history.pushState;
// 保存原生的 replaceState 方法
var originReplaceState = window.history.replaceState;

// 劫持覆写 pushState
window.history.pushState = function() {
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
var url = args.length > 2 ? args[2] : undefined;
if (url) {
var from = lastHref;
var to = String(url);
lastHref = to;
// 将页面跳转行为收集起来
triggerHandlers('history', {
from: from,
to: to,
});
}
// 使用原生的 pushState 做页面跳转
return originPushState.apply(this, args);
}

// 劫持覆写 replaceState
window.history.replaceState = function() {
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
var url = args.length > 2 ? args[2] : undefined;
if (url) {
var from = lastHref;
var to = String(url);
lastHref = to;
// 将页面跳转行为收集起来
triggerHandlers('history', {
from: from,
to: to,
});
}
// 使用原生的 replaceState 做页面跳转
return originReplaceState.apply(this, args);
}

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对原生的fetchxhr做了劫持覆写

    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
    13
    var 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来接收异常了

    image-20221104200524905

    4.接入飞书平台 使用飞书群聊功能自动通知人员

    1. 创建异常上报群并设置群聊机器人

      image-20221104200704236

    2. 生成机器人的webhook 记住保存后面用

      image-20221104200751332

    3. sentry 平台设置对应的webhook

      image-20221104200908164

    4. 创建飞书捷径并设置操作

      image-20221104200958281

    5. 接下来 只要项目发生异常 就会通知sentry sentry就会将异常信息发布到飞书通知群通知对应人员去修复

使用 sentry 做性能分析

常见的性能优化指标及获取方式

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const timing = window.performance.timing;
{
navigationStart, //同一个浏览器上下文中 上一个文档结束时的时间戳 如果没有上一个文档 这个值会和fetchStart相同
unloadEventStart,//上一个文档unload事件触发的时间戳 如果没有上一个文档 为0
unloadEventEnd,//上一个unload事件结束时的时间戳 如果没有上一个文档 为0
redirectStart,//表示最后一个http重定向开始时的时间戳 如果没有重定向或者有一个非同源的重定向 为0
redirectEnd,//最后一个重定向结束时的时间戳 如果没有重定向或者有一个非同源的重定向 为 0
fetchStart, //浏览器准备好使用http请求获取文档的时间戳 这个时间会在检查任何缓存之前
domainLookupEnd/Start//域名查询开始/结束时的时间戳 如果用了持久连接或者本地有缓存 则和fetchStart相同
connectStart, //http请求向服务器发送链接请求的时间戳 如果是用了持久连接 这个值和fechStart相同
connectEnd, //浏览器和服务器之间建立起链接的时间 所有握手和认证过程全部结束 如果使用了持久链接 这个值和fetchStart相同
secureConnectionStart, // 浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,返回 0。
requestStart, // 浏览器向服务器发起 http 请求(或者读取本地缓存)时的时间戳,即获取 html 文档。
responseStart, // 浏览器从服务器接收到第一个字节时的时间戳。
responseEnd, // 浏览器从服务器接受到最后一个字节时的时间戳。
domLoading, // dom 结构开始解析的时间戳,document.readyState 的值为 loading。
domInteractive, // dom 结构解析结束,开始加载内嵌资源的时间戳,document.readyState 的状态为 interactive。
domContentLoadedEventStart, // DOMContentLoaded 事件触发时的时间戳,所有需要执行的脚本执行完毕。
domContentLoadedEventEnd, // DOMContentLoaded 事件结束时的时间戳
domComplete, // dom 文档完成解析的时间戳, document.readyState 的值为 complete。
loadEventStart, // load 事件触发的时间。
loadEventEnd // load 时间结束时的时间。

}

页面何时开始渲染 FP & FCP

  • first paint表示页面开始首次绘制的时间戳 值越小越好 在FP事件点之前 用户看到的是导航之前的页面
  • first contentful paint表示首次绘制任何文本、图像、非空白canvas或者SVG的时间点 值越小越好

可以通过 perfermance.getEntry | performance.getEntriesByName | performanceObserver来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
performance.getEntries().filter(item => item.name === 'first-paint')[0];  // 获取 FP 时间

performance.getEntries().filter(item => item.name === 'first-contentful-paint')[0]; // 获取 FCP 时间

performance.getEntriesByName('first-paint'); // 获取 FP 时间

performance.getEntriesByName('first-contentful-paint'); // 获取 FCP 时间

// 也可以通过 performanceObserver 的方式获取
var observer = new PerformanceObserver(function(list, obj) {
var entries = list.getEntries();
entries.forEach(item => {
if (item.name === 'first-paint') {
...
}
if (item.name === 'first-contentful-paint') {
...
}
})
});
observer.observe({type: 'paint'});

页面何时渲染主要内容 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});

性能分析关键指标

  • lighthouseTCP \ LCP \SI \TTI \ TBT \ CLS
  • SentryFCP \ LCP \ FID \ CLS

Sentry 性能监控的原理

通过 window.performance.getEntries \ performanceObserver 获取用户在使用应用过程中涉及的 load相关 FCP / LCP / FID / CLS等指标数据 然后上报 监控平台拿到数据后 通过可视化的方式展示指标数据 帮助我们分析

sentry将性能指标数据分为两部分 首屏加载相关和页面切换相关

首屏加载(pageload)

1.应用加载时使用 sentry.init方法进行初始化

在初始化时 通过setTimeout实现首屏完成后再上报首屏性能指标数据 (默认为1000ms 如果我们的首屏时间超过1000ms 则需要手动设置timeout

1
2
3
4
5
6
7
8
9
10
11
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [
new BrowserTracing({
idleTimeout: 3000,
...
}),
],
tracesSampleRate: 1.0,

})

2.在setTimeoutcallback中通过 window.performance.getEntries | performanceObserver获取性能指标数据 然后通过接口上报

页面切换(navigation)

1.在 Sentry.init 初始化过程中对 history.pushState / history.replaceState / window.onpopState 进行覆写 拦截路由切换操作

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
 function historyReplacementFunction(originalHistoryFunction: () => void): () => void {
return function (this: History, ...args: any[]): void {
const url = args.length > 2 ? args[2] : undefined;
if (url) {
// coerce to string (this is what pushState does)
const from = lastHref;
const to = String(url);
// keep track of the current URL state, as we always receive only the updated state
lastHref = to;
triggerHandlers('history', {
from,
to,
});
}
return originalHistoryFunction.apply(this, args);
};
}
//对pushState replaceState进行覆写
fill(WINDOW.history, 'pushState', historyReplacementFunction);
fill(WINDOW.history, 'replaceState', historyReplacementFunction);
}
function instrumentHistory(): void {
if (!supportsHistory()) {
return;
}

const oldOnPopState = WINDOW.onpopstate;
WINDOW.onpopstate = function (this: WindowEventHandlers, ...args: any[]): any {
const to = WINDOW.location.href;
// keep track of the current URL state, as we always receive only the updated state
const from = lastHref;
lastHref = to;
triggerHandlers('history', {
from,
to,
});
if (oldOnPopState) {
// Apparently this can throw in Firefox when incorrectly implemented plugin is installed.
// https://github.com/getsentry/sentry-javascript/issues/3344
// https://github.com/bugsnag/bugsnag-js/issues/469
try {
return oldOnPopState.apply(this, args);
} catch (_oO) {
// no-empty
}
}
};

2.页面切换以后 window.performance.genEntires获取性能指标数据 然后通过接口上报 通过performance。getEntries获取性能指标数据时 sentry 会记录上次上报时的oldIndex 等到下次上报 oldIndex + 1开始获取指标性能数据

使用 sentry 进行性能分析

1
2
3
4
5
6
7
8
9
//以react为例
import * as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";

Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [new BrowserTracing()],
tracesSampleRate: 0.2, //采样率 决定了性能指标数据上报的频率 最大值为1
});

image.png

Oct-04-2022 21-51-03.gif

image.png

怎么做性能优化

优化性能 分为让用户更快地看到页面内容(FCP/ LCP)、更早、更流畅地操作页面(FID/TTI/TTB)以及更好的视觉体验(CLS

LCP &FCP

优化 FCP

  • 减少服务器响应时间-:避免多次重定向 提前建立连接 preconnectdns预解析、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动画 而不是触发布局偏移(引发回流重绘)的属性动画

文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录