前言
在上一篇文章------ 🚀前端监控全链路解析+全栈实现🚀 中,我们提到了日志+监控的全链路实现。这篇文章一起再来讨论一下异常监控的时间,这对于我们排查生产问题来说十分有效。
之前的文章已经介绍过如何上报以及如何查询上报后的异常,这里就不再赘述,主要讨论的是异常的采集方式。
监听异常信息
由于语法错误我们一般在编译的时候就能发现,所以我们收集以下几种异常并上报:
- 运行时异常
- 资源加载异常
- 未捕获的
Promise
- 接口异常
运行时异常+资源加载异常
先看代码:
js
handleResourceError = (event) => {
const errorInfo = {
tagName: event.target.tagName,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
targetUrl: event.target.src || event.target.href,
};
this.log(ERROR_TYPE.RESOURCE, errorInfo);
};
handleRuntimeError = (event) => {
const errorInfo = {
message: event.message,
source: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error ? event.error.stack : null,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
};
this.log(ERROR_TYPE.RUNTIME, errorInfo);
};
handleError = (event) => {
if (event.target && (event.target.src || event.target.href)) {
// 资源加载错误
this.handleResourceError(event);
} else {
// 运行时错误
this.handleRuntimeError(event);
}
};
handleError
函数用于捕获运行时异常,通过判断异常事件的 target
,来判断是资源加载异常还是运行时异常。
因为我们加载资源总是通过 img
、 link
、 script
、 a
标签等,而路径属性一般都是src
或者 href
。
所以可以通过这种方式来区分,或者也可以通过 traget
的 tagName
属性来区分。
如果是资源异常,则上报具体的资源 url
;如果是运行时异常,则上报文件名、行号、列号,以及堆栈信息,后续我们会用到这种信息配合 SourceMap
来模拟定位生产问题。
最后通过以下的方式监听异常事件:
js
window.addEventListener("error", this.handleError, true);
未捕获的promise
同样先看代码:
js
handlePromiseRejection = (event) => {
const errorInfo = {
message: event.reason,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
};
this.log(ERROR_TYPE.PROMISE, errorInfo);
};
当捕获到未处理的 Promise reject
时,会触发 unhandledrejection
事件,这个时候就可以捕获到这个异常。 event.reason
就是 reject()
中传入的参数,即错误原因。
通过以下的方式来监听异常事件:
js
window.addEventListener("unhandledrejection", this.handlePromiseRejection);
接口异常
接口异常我们可以通过拦截 AJAX
和 Fetch
来实现。具体的拦截方式如下:
AJAX
js
initpAjaxInterceptor = () => {
const open = XMLHttpRequest.prototype.open;
const send = XMLHttpRequest.prototype.send;
const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (method, url) {
this._method = method;
this._url = url;
this._requestHeaders = {};
open.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
this._requestHeaders[header] = value; //
setRequestHeader.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this.addEventListener("load", () => {
if (this.status >= 400) {
const errorData = {
method: this._method,
url: this._url,
requestHeaders: this._requestHeaders,
requestBody: body,
status: this.status,
statusText: this.statusText,
responseText: this.responseText,
};
console.log("errorData", errorData);
}
});
send.apply(this, arguments);
};
};
解释一下上面的代码:
- 拓展
open
方法,获取到请求url
和method
- 拓展
setRequestHeader
方法,获取到请求头 - 拓展
send
方法,获取到请求体
在请求结束后查看状态码,如果状态异常,则上报异常数据。
Fetch
js
initFetchInterceptor = () => {
const originalFetch = window.fetch;
window.fetch = async function (input, init) {
const method = (init && init.method) || "GET";
const url = typeof input === "string" ? input : input.url;
const headers = (init && init.headers) || {};
const body = (init && init.body) || null;
try {
const response = await originalFetch(input, init);
if (!response.ok) {
const errorData = {
method: method,
url: url,
requestHeaders: headers,
requestBody: body,
status: response.status,
statusText: response.statusText,
responseText: await response.text(),
};
console.log("errorData", errorData);
}
return response;
} catch (error) {
const errorData = {
method: method,
url: url,
requestHeaders: headers,
requestBody: body,
errorMessage: error.message,
};
console.log("errorData", errorData);
throw error; // 重新抛出错误
}
};
};
对于 Fetch
请求的拦截也是大同小异,甚至更好处理一些,因为它的请求方式、请求头、请求体都是放在一个对象里面,我们可以更轻易的获取。
SourceMap
收集到了异常,特别是运行时异常之后,如何快速定位?我们生产上的代码都是打包混淆过的,要怎么定位到源代码? 这个时候就需要用到 SourceMap
了。
它可以将编译后的、混淆或压缩的代码映射回原始的源代码,方便我们调试与定位问题。
在 vite
中,主要可以通过如下方式配置 sourcemap
:
build.sourcemap
:此选项用于配置在生产构建时是否生成 source map
。可以设置为以下几种值:
false
:不生成source map
。true
:生成source map
文件。'inline'
:生成内联的source map
,将source map
内容嵌入到生成的 JavaScript 文件中。'hidden'
:生成source map
文件,但不在生成的JavaScript
文件中引用source map
文件。
以上是更详细的解释:
-
true
:- 含义:生成单独的 source map 文件。
- 用途 :当你在生产环境中使用这个选项时,Vite 会为每个生成的 JavaScript 文件创建一个对应的
.map
文件,这样在调试时可以使用浏览器的开发者工具查看源代码的原始版本。对于需要进行错误监控和调试的生产应用,这是一个非常有用的设置。
-
'inline'
:- 含义 :将 source map 内容嵌入到生成的 JavaScript 文件中,而不是生成单独的
.map
文件。 - 用途:这种方式可以减少文件数量,使得在某些情况下(例如在开发时或小型项目中)更容易使用,因为不需要加载额外的 source map 文件。它会将所有的源代码信息直接放在生成的 JavaScript 文件中。缺点是文件大小会增加,因为 source map 数据会直接包含在 JavaScript 文件中。
- 含义 :将 source map 内容嵌入到生成的 JavaScript 文件中,而不是生成单独的
-
'hidden'
:- 含义:生成 source map 文件,但在生成的 JavaScript 文件中不引用这个文件。
- 用途:这种方式适用于你想要保留 source map 以便于调试或错误监控,但不希望在生产环境中暴露源代码信息。浏览器在加载生成的 JavaScript 文件时不会尝试加载对应的 source map 文件,但该文件仍然存在,可以在需要时手动使用。这在某些情况下能够保护源代码,同时保持调试的灵活性。
使用
首先我们这里有一个运行时报错
点堆栈进去如下
其实看的不是太清楚,不好定位,接入监控之后,有如下的信息:
首先我们已经捕捉到了一个报错,在这些错误信息上报以及sourceMap文件上报之后,我们开始利用这些信息来快速定位问题。
根据 source
字段可以确定,我们需要找的是 index-Z0uUDqvI.js.map
这个 soucemap
文件。
可以写一个这样的脚本:
js
const fs = require("fs");
const { SourceMapConsumer } = require("source-map");
const path = require("path");
// 读取 source map 文件
const rawSourceMap = fs.readFileSync(
path.resolve(__dirname, "./dist/assets/index-Z0uUDqvI.js.map"),
"utf8"
);
// 解析 source map 文件
SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
const generatedLine = 187;
const generatedColumn = 35258;
const pos = consumer.originalPositionFor({
line: generatedLine,
column: generatedColumn,
});
console.log("Source:", pos.source);
console.log("Line:", pos.line);
console.log("Column:", pos.column);
});
然后执行一下,看看结果:
可以快速的定位到出错的地方:
最后
以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~