一文摸清前端监控自研实践(三)错误监控
前言
上篇文章我们分享了关于 用户行为监控
的内容,本文我们接着来看 错误异常监控
的方面
系列文章传送门
应用的稳定情况
众所周知,无论进行发布前的单元测试
、集成测试
、人工测试
进行再多轮,都会难免漏掉一些边缘的测试场景,甚至还有一些奇奇怪怪的玄学故障出现
;而出现报错后,轻则某些数据页面无法访问
,重则导致客户数据出错
;
这时,一个完善的错误监控体系就派上很大的用场,它可以帮助我们做以下的事情:
- 应用报错时,及时知晓线上应用出现了错误,及时安排修复止损;
- 应用报错后,根据上报的用户行为追踪记录数据,迅速进行bug复现;
- 应用报错后,通过上报的错误行列以及错误信息,找到报错源码并快速修正;
- 数据采集后,进行分析提供宏观的 错误数、错误率、影响用户数等关键指标;
整体封装
// 错误类型
export enum mechanismType {
JS = 'js',
RS = 'resource',
UJ = 'unhandledrejection',
HP = 'http',
CS = 'cors',
VUE = 'vue',
}
// 格式化后的 异常数据结构体
export interface ExceptionMetrics {
mechanism: Object;
value?: string;
type: string;
stackTrace?: Object;
pageInformation?: Object;
breadcrumbs?: Array<behaviorStack>;
errorUid: string;
meta?: any;
}
// 初始化用参
export interface ErrorVitalsInitOptions {
Vue: any;
}
// 判断是 JS异常、静态资源异常、还是跨域异常
export const getErrorKey = (event: ErrorEvent | Event) => {
const isJsError = event instanceof ErrorEvent;
if (!isJsError) return mechanismType.RS;
return event.message === 'Script error.' ? mechanismType.CS : mechanismType.JS;
};
// 初始化的类
export default class ErrorVitals {
private engineInstance: EngineInstance;
// 已上报的错误 uid
private submitErrorUids: Array<string>;
constructor(engineInstance: EngineInstance, options: ErrorVitalsInitOptions) {
const { Vue } = options;
this.engineInstance = engineInstance;
this.submitErrorUids = [];
// 初始化 js错误
this.initJsError();
// 初始化 静态资源加载错误
this.initResourceError();
// 初始化 Promise异常
this.initPromiseError();
// 初始化 HTTP请求异常
this.initHttpError();
// 初始化 跨域异常
this.initCorsError();
// 初始化 Vue异常
this.initVueError(Vue);
}
// 封装错误的上报入口,上报前,判断错误是否已经发生过
errorSendHandler = (data: ExceptionMetrics) => {
// 统一加上 用户行为追踪 和 页面基本信息
const submitParams = {
...data,
breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),
pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),
} as ExceptionMetrics;
// 判断同一个错误在本次页面访问中是否已经发生过;
const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);
// 检查一下错误在本次页面访问中,是否已经产生过
if (hasSubmitStatus) return;
this.submitErrorUids.push(submitParams.errorUid);
// 记录后清除 breadcrumbs
this.engineInstance.userInstance.breadcrumbs.clear();
// 一般来说,有报错就立刻上报;
this.engineInstance.transportInstance.kernelTransportHandler(
this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),
);
};
// 初始化 JS异常 的数据获取和上报
initJsError = (): void => {
//... 详情代码在下
};
// 初始化 静态资源异常 的数据获取和上报
initResourceError = (): void => {
//... 详情代码在下
};
// 初始化 Promise异常 的数据获取和上报
initPromiseError = (): void => {
//... 详情代码在下
};
// 初始化 HTTP请求异常 的数据获取和上报
initHttpError = (): void => {
//... 详情代码在下
};
// 初始化 跨域异常 的数据获取和上报
initCorsError = (): void => {
//... 详情代码在下
};
// 初始化 Vue异常 的数据获取和上报
initVueError = (app: Vue): void => {
//... 详情代码在下
};
}
生成错误 uid
首先,什么叫为每个错误生成 uid
,这里生成的 uid
有什么用呢?答案其实很简单:
- 一次用户访问(页签未关闭),上报过一次错误后,后续产生
重复错误不再上报
- 多个用户产生的同一个错误,在服务端可以归类,
分析影响用户数、错误数等指标
- 需要注意的是,对于同一个原因产生的同一个错误,生成的 uid 是相同的
// 对每一个错误详情,生成一串编码
export const getErrorUid = (input: string) => {
return window.btoa(unescape(encodeURIComponent(input)));
};
错误堆栈
在做错误监控之前,我们先来了解一下什么是错误堆栈;我们写代码经常报错的时候能够看到,下图这样子类似的错误
,一个错误加上很多条很多条的调用信息组成的错误;这就是抛出的 Error对象
里的 Stack错误堆栈
,里面包含了很多信息:包括调用链
、文件名
、调用地址行列信息
等等;而在下文的错误捕获中,我们也需要去对 Stack错误堆栈
进行解析;
当然,解析这一长串的东西还是比较痛苦的,我这边就给出我的解析方法以供参考
:
// 正则表达式,用以解析堆栈split后得到的字符串
const FULL_MATCH =
/^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
// 限制只追溯10个
const STACKTRACE_LIMIT = 10;
// 解析每一行
export function parseStackLine(line: string) {
const lineMatch = line.match(FULL_MATCH);
if (!lineMatch) return {};
const filename = lineMatch[2];
const functionName = lineMatch[1] || '';
const lineno = parseInt(lineMatch[3], 10) || undefined;
const colno = parseInt(lineMatch[4], 10) || undefined;
return {
filename,
functionName,
lineno,
colno,
};
}
// 解析错误堆栈
export function parseStackFrames(error: Error) {
const { stack } = error;
// 无 stack 时直接返回
if (!stack) return [];
const frames = [];
for (const line of stack.split('\n').slice(1)) {
const frame = parseStackLine(line);
if (frame) {
frames.push(frame);
}
}
return frames.slice(0, STACKTRACE_LIMIT);
}
调用 parseStackFrames()
方法将 error对象
传入后,我们可以看到解析的效果还是可以的:
JS运行异常
什么叫 JS运行异常
呢?其实很简单,当 JavaScript运行时产生的错误 就属于 JS运行异常
比如,我们未定义一个方法就直接调用它,它会报错:Uncaught ReferenceError: xxx is not defined
,这就属于 JS运行异常
noEmit(); // 没有定义,直接调用
// 会报错:Uncaught ReferenceError: noEmit is not defined
那么,既然发生了错误,我们就需要去捕获它;而捕获JS运行异常有两种方法:
方法一
我们可以使用 window.onerror
来捕获全局的 JS运行异常
,window.onerror
是一个全局变量,默认值为null。当有js运行时错误触发时,window会触发error事件,并执行 window.onerror()
,借助这个特性,我们对 window.onerror
进行重写就可以捕获到代码中的异常;
window.onerror = (msg, url, row, col, error) => {
const exception = {
// 上报错误归类
mechanism: {
type: 'js'
},
// 错误信息
value: msg,
// 错误类型
type: error.name || 'UnKnowun',
// 解析后的错误堆栈
stackTrace: {
frames: parseStackFrames(error),
},
meta: {
url, // 文件地址
row, // 行号
col, // 列号
}
};
// 获取了报错详情,就可以走上报方法上报错误信息
console.log('JS运行error', exception);
return true; // 返回 true,阻止了默认事件执行,也就是原本将要在控制台打印的错误信息
};
方法二
我们还可以使用 window.addEventListener('error')
来捕获 JS运行异常
;它会比 window.onerror
先触发;
我们简单封装一下:
// 初始化 JS异常 的数据获取和上报
initJsError = (): void => {
const handler = (event: ErrorEvent) => {
// 阻止向上抛出控制台报错
event.preventDefault();
// 如果不是 JS异常 就结束
if (getErrorKey(event) !== mechanismType.JS) return;
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.JS,
},
// 错误信息
value: event.message,
// 错误类型
type: (event.error && event.error.name) || 'UnKnowun',
// 解析后的错误堆栈
stackTrace: {
frames: parseStackFrames(event.error),
},
// 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
// 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.JS}-${event.message}-${event.filename}`),
// 附带信息
meta: {
// file 错误所处的文件地址
file: event.filename,
// col 错误列号
col: event.colno,
// row 错误行号
row: event.lineno,
},
} as ExceptionMetrics;
// 一般错误异常立刻上报,不用缓存在本地
this.errorSendHandler(exception);
};
window.addEventListener('error', (event) => handler(event), true);
};
两者的区别和选用
阅读了上文,我们了解到想要监控 JS运行异常
,我们有两种方法可以选用,那么我们应该选用哪一种呢?或者说它们两者方法之间有什么区别呢?
- 它们两者均可以捕获到
JS运行异常
,但是 方法二除了可以监听JS运行异常
之外,还可以同时捕获到静态资源加载异常
onerror
可以接受多个参数。而addEventListener('error')
只有一个保存所有错误信息的参数
我这边个人更加建议使用第二种 addEventListener('error')
的方式;原因很简单:不像方法一可以被 window.onerror 重新覆盖
;而且可以同时处理静态资源错误
错误类型
细心的同学应该看见了,上文的捕获中,有一个参数叫做 错误类型
,我们可以通过这个来快速判断错误是基于什么导致的,那么 JS运行时的错误类型常见的有哪些呢?
类型 | 含义 | 说明 |
---|---|---|
SyntaxError | 语法错误 | 语法错误 |
ReferenceError | 引用错误 | 常见于引用了一个不存在的变量: let a = undefinedVariable; |
RangeError | 有效范围错误 | 数值变量或参数超出了其有效范围。 常见于 1.创建一个负长度数组 2.Number对象的方法参数超出范围:let b = new Array(-1) |
TypeError | 类型错误 | 常见于变量或参数不属于有效类型 let foo = 3;foo(); |
URIError | URL处理函数错误 | 使用全局URL处理函数错误,比如 decodeURIComponent('%'); |
- 这里有一个点需要特别注意,我们主观感觉上的
SyntaxError 语法错误
,除了用eval()
执行的脚本以外,一般是不可以被捕获到的,比如我们编写一个正常的语法错误
:
const d d = 1;
// 控制台报错 :Uncaught SyntaxError: Missing initializer in const declaration
// 但是上述的捕获方法无法正常捕获错误;
- 这明显上是一个
语法上的错误
,但是我们上述的两个错误捕获方法
,都没办法捕获到错误; - 只有在代码中通过
eval()
执行的代码脚本才可以正常捕获到错误信息;
eval('ddd fff');
// 控制台报错 VM149:1 Uncaught SyntaxError: Unexpected identifier
// 上文的错误捕获方法可以正常捕获到错误;
- 那么,WHY?
其实原因很简单, const d d = 1
; 这种语法错误,在编译解析阶段
就已经报错了,而拥有语法错误的脚本不会放入任务队列
进行执行,自然也就不会有错误冒泡到我们的捕获代码;而我们使用 eval()
;在编译解析阶段一切正常,直到执行的时候才进行报错,自然我们就可以捕获到这段错误;
当然,现在代码检查这么好用,早在编写代码时这种语法错误就被避免掉了,一般我们碰不上语法错误的~
静态资源加载异常
有的时候,我们界面上的 img图片
、CDN资源
突然失效了、打不开了,就比如以下面这个为例子,我们往html中放进一个img,把它的路径设为请求不到的地址:
<img src="http://localhost:8888/nottrue.jpg">
// 会报错 GET http://localhost:8888/nottrue.jpg net::ERR_CONNECTION_REFUSED
那我们怎么去捕获到这种请求不到资源的、或者说静态资源失效的报错呢?很简单,只需要祭出 window.addEventListener('error')
就可以了
// 静态资源错误的 ErrorTarget
export interface ResourceErrorTarget {
src?: string;
tagName?: string;
outerHTML?: string;
}
// 初始化 静态资源异常 的数据获取和上报
initResourceError = (): void => {
const handler = (event: Event) => {
event.preventDefault(); // 阻止向上抛出控制台报错
// 如果不是跨域脚本异常,就结束
if (getErrorKey(event) !== mechanismType.RS) return;
const target = event.target as ResourceErrorTarget;
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.RS,
},
// 错误信息
value: '',
// 错误类型
type: 'ResourceError',
// 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
// 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.RS}-${target.src}-${target.tagName}`),
// 附带信息
meta: {
url: target.src,
html: target.outerHTML,
type: target.tagName,
},
} as ExceptionMetrics;
// 一般错误异常立刻上报,不用缓存在本地
this.errorSendHandler(exception);
};
window.addEventListener('error', (event) => handler(event), true);
};
使用
addEventListener
捕获资源错误时,一定要将 第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。同理,由于 window.onerror 是通过在冒泡阶段捕获错误,所以无法捕获资源错误。
Promise异常
什么叫 Promise异常
呢?其实就是我们使用 Promise
的过程中,当 Promise
被 reject
且没有被 catch
处理的时候,就会抛出 Promise异常
;同样的,如果我们在使用 Promise
的过程中,报了JS的错误,同样也被以 Promise异常
的形式抛出:
下面我举两个会产生 Promise异常
的例子
Promise.resolve().then(() => console.log(c));
// Uncaught (in promise) ReferenceError: c is not defined
Promise.reject('reject了但是没有处理!')
// Uncaught (in promise) reject了但是没有处理!
而当抛出 Promise异常
时,会触发 unhandledrejection
事件,所以我们只需要去监听它就可以进行 Promise 异常
的捕获了,不过值得注意的一点是:相比与上面所述的直接获取报错的行号、列号等信息,Promise异常
我们只能捕获到一个 报错原因
而已;
// 初始化 Promise异常 的数据获取和上报
initPromiseError = (): void => {
const handler = (event: PromiseRejectionEvent) => {
event.preventDefault(); // 阻止向上抛出控制台报错
const value = event.reason.message || event.reason;
const type = event.reason.name || 'UnKnowun';
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.UJ,
},
// 错误信息
value,
// 错误类型
type,
// 解析后的错误堆栈
stackTrace: {
frames: parseStackFrames(event.reason),
},
// 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
// 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.UJ}-${value}-${type}`),
// 附带信息
meta: {},
} as ExceptionMetrics;
// 一般错误异常立刻上报,不用缓存在本地
this.errorSendHandler(exception);
};
window.addEventListener('unhandledrejection', (event) => handler(event), true);
};
HTTP请求异常
HTTP请求的捕获,我在前文中已经写过代码,可以回翻: 一文摸清前端监控实践要点(二)行为监控 HTTP 请求捕获
所谓 Http请求异常
也就是异步请求 HTTP 接口时的异常罢了,比如我调用了一个登录接口,但是我的传参不对,登录接口给我返回了 500 错误码
,其实这个时候就已经产生了异常了;
是否属于 Promise异常
看到这里,其实有的同学可能会疑惑
,我们现在的调用 HTTP 接口,一般也就是通过 async/await
这种基于Promise的解决异步的最终方案;那么,假如说请求了一个接口地址报了500,因为是基于 Promise
调用的接口,我们能够在上文的 Promise异常
捕获中,获取到一个错误信息(如下图);
但是有一个问题别忘记了,Promise异常捕获没办法获取报错的行列
,我们只知道 Promise 报错了,报错的信息是 接口请求500
;但是我们根本不知道是哪个接口报错了;
所以说,我们对于 Http请求异常
的捕获需求就是:全局统一监控
、报错的具体接口
、请求状态码
、请求耗时
以及请求参数
等等;
而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的 XMLHttpRequest
或者 Fetch
,我们只需要对这两个方法都进行 劫持
,就可以往接口请求的过程中加入我们所需要的一些参数捕获;
代码实现
// 初始化 HTTP请求异常 的数据获取和上报
initHttpError = (): void => {
const loadHandler = (metrics: httpMetrics) => {
// 如果 status 状态码小于 400,说明没有 HTTP 请求错误
if (metrics.status < 400) return;
const value = metrics.response;
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.HP,
},
// 错误信息
value,
// 错误类型
type: 'HttpError',
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.HP}-${value}-${metrics.statusText}`),
// 附带信息
meta: {
metrics,
},
} as ExceptionMetrics;
// 一般错误异常立刻上报,不用缓存在本地
this.errorSendHandler(exception);
};
proxyXmlHttp(null, loadHandler);
proxyFetch(null, loadHandler);
};
跨域脚本错误
介绍
还有一种错误,平常我们较难遇到,那就是 跨域脚本错误
,什么叫 跨域脚本错误
呢?比如说我们新建一个texterror.js
文件到 项目B
的 public 目录下以供外部访问;
// 新建的 texterror.js 文件
const a = new Array(-1);
可以看到,我们在 texterror.js
文件中写了一行会报错的代码,认真看过上文的同学应该知道,它会被捕获在 JS运行异常
中,且错误类型为 RangeError
;而我们从 项目A
中引入它;
// 项目B的地址,和项目A端口不同;
<script async src="http://xxxxxx:8081/texterror.js"> </script>
加载后运行,我们自然能在控制台发现报错:而我们上文的代码捕获也有错误捕获到:
但是我们发现,这里的 msg
信息是 Script error
,也没有获取到行号
、列号
、文件名
等的信息,这是怎么回事呢?
其实这是浏览器的一个安全机制
:当跨域加载的脚本中发生语法错误时,浏览器出于安全考虑,不会报告错误的细节,而只报告简单的 Script error
。浏览器只允许同域下的脚本捕获具体错误信息,而其他脚本只知道发生了一个错误,但无法获知错误的具体内容(控制台仍然可以看到,JS脚本无法捕获),我们上文通过项目A去加载项目B的文件,自然产生了跨域;
处理
其实对于三方脚本的错误,我们是否捕获都可以,不过我们需要一点处理,如果不需要捕获的话,就不进行上报,如果需要捕获的话,只上报类型;我们甚至可以只关心自己的远端JS问题,去根据公司域名进行过滤 filename。
我们对上文的 window.addEventListener('error')
再加上对跨域资源的判断,以和正常的代码中错误区分开;
// 初始化 跨域异常 的数据获取和上报
initCorsError = (): void => {
const handler = (event: ErrorEvent) => {
// 阻止向上抛出控制台报错
event.preventDefault();
// 如果不是跨域脚本异常,就结束
if (getErrorKey(event) !== mechanismType.CS) return;
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.CS,
},
// 错误信息
value: event.message,
// 错误类型
type: 'CorsError',
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.JS}-${event.message}`),
// 附带信息
meta: {},
} as ExceptionMetrics;
// 自行上报异常,也可以跨域脚本的异常都不上报;
this.errorSendHandler(exception);
};
window.addEventListener('error', (event) => handler(event), true);
};
补充
看到了这里,可能还有的同学想了解:那么这种跨域的脚本错误我们就没有办法进行获取错误详情吗?答案还是有的:
我们只需要 开启跨域资源共享CORS(Cross Origin Resource Sharing)
,就可以捕获错误了~我们需要分两步来进行实现:
- 添加
crossorigin="anonymous"
属性。
<script src="http://xxxxxxxx/texterror.js" crossorigin="anonymous"></script>
- 添加
跨域HTTP响应头
。
Access-Control-Allow-Origin: *
这两步完成后,允许了跨域,我们就可以在错误捕获脚本中获取到具体的错误信息拉!
Vue2、Vue3 错误捕获
Vue2
如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数;Vue3
同Vue2
,如果在组件渲染时出现运行错误,错误将会被传递至全局的 app.config.errorHandler 配置函数;
我们可以利用这两个钩子函数来进行错误捕获,由于是依赖于 Vue配置函数
的错误捕获,所以我们在初始化
时,需要用户将 Vue实例
传进来;
获取报错组件名
export interface Vue {
config: {
errorHandler?: any;
warnHandler?: any;
};
}
export interface ViewModel {
_isVue?: boolean;
__isVue?: boolean;
$root: ViewModel;
$parent?: ViewModel;
$props: { [key: string]: any };
$options: {
name?: string;
propsData?: { [key: string]: any };
_componentTag?: string;
__file?: string;
};
}
// 获取报错组件名
const classifyRE = /(?:^|[-_])(\w)/g;
const classify = (str: string) => str.replace(classifyRE, (c) => c.toUpperCase()).replace(/[-_]/g, '');
const ROOT_COMPONENT_NAME = '<Root>';
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>';
export const formatComponentName = (vm: ViewModel, includeFile: Boolean) => {
if (!vm) {
return ANONYMOUS_COMPONENT_NAME;
}
if (vm.$root === vm) {
return ROOT_COMPONENT_NAME;
}
const options = vm.$options;
let name = options.name || options._componentTag;
const file = options.__file;
if (!name && file) {
const match = file.match(/([^/\\]+)\.vue$/);
if (match) {
name = match[1];
}
}
return (
(name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : '')
);
};
初始化封装
// 只需要在外部把初始化好的 Vue 对象传入即可~
// 初始化 Vue异常 的数据获取和上报
initVueError = (app: Vue): void => {
app.config.errorHandler = (err: Error, vm: ViewModel, info: string): void => {
const componentName = formatComponentName(vm, false);
const exception = {
// 上报错误归类
mechanism: {
type: mechanismType.VUE,
},
// 错误信息
value: err.message,
// 错误类型
type: err.name,
// 解析后的错误堆栈
stackTrace: {
frames: parseStackFrames(err),
},
// 错误的标识码
errorUid: getErrorUid(`${mechanismType.JS}-${err.message}-${componentName}-${info}`),
// 附带信息
meta: {
// 报错的Vue组件名
componentName,
// 报错的Vue阶段
hook: info,
},
} as ExceptionMetrics;
// 一般错误异常立刻上报,不用缓存在本地
this.errorSendHandler(exception);
};
};
React 错误捕获
React
一样也有官方提供的错误捕获,见文档:zh-hans.reactjs.org/docs/react-…
和 Vue
不同的是,我们需要自己定义一个高阶组件暴露给项目使用,我这里就不具体详写了,感兴趣的同学可以自己进行补全:
import * as React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
}
// ...
componentDidCatch(error, info) {
// "组件堆栈" 例子:
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
}
// ...
}
项目使用方只需要这样既可:
import React from "react";
<ErrorBoundary>
<Example />
</ErrorBoundary>;
Source Map
我们的项目想要部署上线,就需要将项目源码经过混淆
、压缩
、babel编译转化
等等的操作之后,生成最终的打包产物
,再进行线上部署;而这样混淆后的代码
,我们基本上无法阅读,即使在上文的错误监控里,我们获取了报错代码的行号、列号等关键信息,我们也无法找到具体的源码位置所在
;这个时候就需要请出我们的 Sourcemap
了
Sourcemap
本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。
我们通过种种打包工具打包后,如果开启了 Sourcemap
功能,就会在打包产物里发现后缀为 .map
的文件,通过对它的解析,我们就可以得到项目的源代码;
- 我这里举例一个通过
nodejs
进行SourceMap
解析的例子代码:
// 这里因为 npm 装了 babel,所以用的 import,正常 nodejs 下为 require
import sourceMap from 'source-map'; //source-map库
import fs from 'fs' //fs为nodejs读取文件的库
import rp from 'request-promise'
/**
* @description: 用来解析 sourcemap 的函数方法
* @param {*} sourceMapFile 传入的 .map 源文件
* @param {*} line 报错行数
* @param {*} column 报错列数
* @param {*} offset 需要截取几行的代码
* @return {*}
*/
export const sourceMapAnalysis = async (sourceMapFile, line, column, offset) => {
// 通过 sourceMap 库转换为sourceMapConsumer对象
const consumer = await new sourceMap.SourceMapConsumer(sourceMapFile);
// 传入要查找的行列数,查找到压缩前的源文件及行列数
const sm = consumer.originalPositionFor({
line, // 压缩后的行数
column, // 压缩后的列数
});
// 压缩前的所有源文件列表
const { sources } = consumer;
// 根据查到的source,到源文件列表中查找索引位置
const smIndex = sources.indexOf(sm.source);
// 到源码列表中查到源代码
const smContent = consumer.sourcesContent[smIndex];
// 将源代码串按"行结束标记"拆分为数组形式
const rawLines = smContent.split(/\r?\n/g);
let begin = sm.line - offset;
const end = sm.line + offset + 1;
begin = begin < 0 ? 0 : begin;
const context = rawLines.slice(begin, end);
// 可以根据自己的需要,在末尾处加上 \n
// const context = rawLines.slice(begin, end).join('\n');
// 销毁
consumer.destroy();
return {
// 报错的具体代码
context,
// 报错在文件的第几行
originLine: sm.line + 1, // line 是从 0 开始数,所以 +1
// source 报错的文件路径
source: sm.source,
}
};
// 请求线上的 .map 文件进行解析
export const loadMapFileByUrl = async (url)=>{
return await rp(url)
}
const line = 9;
const column = 492621;
const rawSourceMap = JSON.parse(
// 这里加载在本地的 .map 文件
fs.readFileSync('./xxxxxxxxxxxxxxx.map','utf-8').toString() // 路径自拟
);
const inlineSourceMap = JSON.parse(await loadMapFileByUrl('http://xxxxxxxxxxxx.map')) // 路径自换
// 从url获取 sourcemap 文件
// const res = await sourceMapAnalysis(inlineSourceMap,line,column,2)
// 从本地获取 sourcemap 文件
const res = await sourceMapAnalysis(rawSourceMap,line,column,2)
console.log(res);
效果如下:
注意:使用
Sourcemap
的同学注意在打包的时候,将.map
文件和部署产物
分离,不能部署到线上地址哦! 如果你将.map
部署上去了,那么你项目的代码也就是直接明文跑在网页上,谁都可以查看未混淆的源码拉!
参考阅读
![[呲牙]](http://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)
![[呲牙]](http://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)
![[灵光一现]](http://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_25.51e6984.png)
-
本文分享如何自研前端监控系统之(一)性能监控;我们都听说过性能的重要性。但谈起性能,以及让网站"速度提升"时,我们具体指的是什么?而本篇文章将分享:如何准确地测量用户的网页性能体验。
- 9850
-
- 48
-
-
本文分享如何自研前端监控系统之(二)行为监控;来谈谈用户行为监控的内容;来看看我们应该如何分析PV、UV、用户的行为追踪、用户的特征信息等;
- 5960
-
- 12
-
简述 Slardar 前端监控自18年底开始建设以来,从仅仅作为 Sentry 的替代,经历了一系列迭代和发展,目前做为一个监控解决方案,已经应用到抖音、西瓜、今日头条等众多业务线。 据21年下旬统计
- 4.1w
-
- 58
-
作为一个前端,在开发过程即便十分小心,自测充分,在不同用户复杂的操作下也难免会出现程序员意想不到的问题,给公司或个人带来巨大的损失。 这时一款能够及时上报错误和能够帮助程序员很好的解决错误的前端错误监控系统就必不可少了。 接下来我们就聊聊常见的错误发生与处理。 ... 可以阅读…
- 1.3w
-
- 7
-
作为程序员,每次开发完自测充分的时候,但还是会有线上异常情况。如何快速发现 或者提前监控到这些异常的出现呢,是不是需要一个错误监控系统? fundebug、sentry、bat的... 有免费版的付费版,免费版。付费版和免费版其实差不多,免费够我们用了,付费的就是他啥都帮你弄好…
- 1.2w
-
- 45
-
在线上项目中,需要统计产品中用户行为和使用情况,从而可以从用户和产品的角度去了解用户群体,从而升级和迭代产品,使其更加贴近用户。用户行为数据可以通过前端数据监控的方式获得,除此之外,前端还需要实现性能监控和异常监控。性能监控包括首屏加载时间、白屏时间、http请求时间和http…
- 7.1w
-
- 37
-
这次由我们团队的羽飞同学带来我们自研的错误监控平台,欢迎各位看官老爷指正和吐槽。在我司线上运行的是近亿级别的广告页面,这样线上如果裸奔,出现了什么问题不知道......
- 1223
-
- 5
-
写代码难免会碰到错误。因此,在项目上线后,我们还需要主动对项目的错误进行收集,不能等用户发现错误,再联系我们,我们再去处理。 本文章为前端进阶系列的一部分,更多内容可查看 https://github.com/hpoenixf/hpoenixf.github.io
- 8247
-
- 10
-
前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。 我今天分享的主题是“如何实现一套多端错误监控平台”。先来做一个简单的自我介绍,我是来自贝贝-大前端架构组的 Allan ,目前致利于集团错误监控系统维护以…
- 1.5w
-
- 40
-
-
一:及时代码运行错误:也称为代码错误。这个错误往往是程序员在代码书写时造成的,比如语法错误、逻辑错误,这样的错误通常在测试阶段就会被发现,但是也可能存在“漏网之鱼”。 二:资源加载错误:这个错误通常是找不到文件(404)或者是文件加载超时造成的。 浏览器获取网页时,会对网页中每…
- 5388
-
- 5
链接:https://juejin.cn/post/7100841779854835719/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。