前言
上篇文章我们分享了性能监控相关的内容,这次我们来讲讲错误监控的内容
系列文章
- 前端监控SDK:从基础到实践 (1. 性能监控)
- 前端监控SDK:从基础到实践 (2. 错误监控)
- 前端监控SDK:从基础到实践 (3. 行为监控)
- 前端监控SDK:从基础到实践 (4. 异常监控)
在文章中,我会结合 monitor-sdk 的实现细节,和给大家学习搭建的demo来逐步拆解前端监控的各个模块,带领大家从 0 到 1 搭建属于自己的监控平台。
错误监控(js、resource、promise)
我将代码错误、资源加载错误、异步错误放在一起讲。
代码错误
我们先来看看js有哪些常见错误
1、SyntaxError(语法错误)
const 11a; // Uncaught SyntaxError: Invalid or unexpected token
2、ReferenceError(引用错误)
console.log(a); // ReferenceError: a is not defined
3、RangeError(范围错误)
const arr = new Array(-10) // RangeError: Invalid array length
4、TypeError(类型错误)
const arr = new '123'; // TypeError: "123" is not a constructor
const obj = {}
obj.fn() // TypeError: obj.fn is not a function
5、跨域脚本错误
当一个页面加载了跨域脚本并发生运行时错误时,如果跨域资源的服务器没有设置适当的 CORS(跨域资源共享)头,浏览器会屏蔽具体的错误信息(出于安全考虑),然后它的message会是Script error.
解决办法:
// 1.在跨域脚本资源的响应头中添加:
Access-Control-Allow-Origin: *
// 2.为 <script> 标签设置 crossorigin 属性:
<script src="https://example.com/script.js" crossorigin="anonymous"></script>
调研错误捕获的方法
1. 监听error事件
现代化的全局错误监听机制,用于捕获页面中发生的 JavaScript 错误和静态资源加载错误。当页面中的 JavaScript 执行出现未捕获的异常,或者资源(如图片、脚本、样式表等)加载失败时,该事件会被触发,并提供详细的错误信息
window.addEventListener('error', (event: ErrorEvent | Event) => {
console.error('Error captured by event listener:', event);
// 上报错误信息到后台
}, true);
2. window.onerror
window.onerror 是一个全局的错误捕获事件,用于捕获页面中未被捕获的 JavaScript 错误。当页面中的 JavaScript 发生错误时,如果没有被 try-catch 捕获,这个错误会被 window.onerror 捕获并处理。
window.onerror = function (message, source, lineno, colno, error) {
console.error('Error captured:', { message, source, lineno, colno, error });
// 上报错误信息到后台
};
让我们对比一下
特性 | window.addEventListener('error') | window.onerror |
---|---|---|
运行时 JavaScript 错误 | 支持 | 支持 |
资源加载错误 | 支持(如图片、CSS 文件、脚本加载失败) | 不支持 |
错误对象的访问 | 支持 | 可能为null |
window.onerror还有一个问题在于它只能设置一个,多次设置会覆盖之前的,而window.addEventListener('error')就不会有这样的问题
基于以上我们采用window.addEventListener('error')来做代码错误监控
js错误捕获
我在这分了三种错误类型,一种是资源加载错误、js执行错误、跨域错误
window.addEventListener(
'error',
(e: ErrorEvent | Event) => {
const errorType = getErrorType(e)
switch (errorType) {
case TraceSubTypeEnum.resource:
initResourceError(e)
break
case TraceSubTypeEnum.js:
initJsError(e as ErrorEvent)
break
case TraceSubTypeEnum.cors:
initCorsError(e as ErrorEvent)
break
default:
break
}
},
true
)
先来看看getErrorType的实现
const getErrorType = (event: ErrorEvent | Event) => {
const isJsError = event instanceof ErrorEvent
if (!isJsError) {
return TraceSubTypeEnum.resource
}
return event.message === 'Script error.'
? TraceSubTypeEnum.cors
: TraceSubTypeEnum.js
}
其实就是先根据入参event判断它的类型,就可以知道是js错误还是资源错误。然后如果是js错误再根据message判断是跨域错误还是js代码错误
js错误上报数据类型
export type ErrorCommonType = {
errId: string
state: any[]
timestamp: number
}
export type JsErrorType = commonType &
ErrorCommonType & {
message: string | Event // 错误信息
src?: string // 资源路径,打包后到路径
lineNo?: number // 错误行号
columnNo?: number // 错误列号
stack: any[] // 错误堆栈
pageUrl: string // 页面路径
eventData: string // 录屏数据
}
js错误上报具体代码
const initJsError = (e: ErrorEvent) => {
const {
colno: columnNo,
lineno: lineNo,
type,
message,
filename: src,
error
} = e
const subType = TraceSubTypeEnum.js
const stack = parseStackFrames(error)
const behavior = getBehaviour()
const state = behavior?.breadcrumbs?.state || []
const eventData = getRecordScreenData()
const reportData: JsErrorType = {
columnNo,
lineNo,
type,
message,
src,
subType,
pageUrl: window.location.href,
stack,
errId: getErrorUid(`${subType}-${message}-${src}`),
state,
timestamp: new Date().getTime(),
eventData
}
lazyReportBatch(reportData)
}
我们需要根据堆栈信息提取出报错行列数、报错方法等信息
// 解析错误堆栈
export function parseStackFrames(error: Error) {
const { stack } = error
// 正则表达式,用以解析堆栈split后得到的字符串
const FULL_MATCH =
/^\s*at (?:(.*?) ?()?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|/).*?)(?::(\d+))?(?::(\d+))?)?\s*$/i
// 限制只追溯5个
const STACKTRACE_LIMIT = 5
// 解析每一行
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
}
}
// 无 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)
}
可自行去demo网站测试一下哦
然后是跨域上报的具体实现代码
const initCorsError = (e: ErrorEvent) => {
const { message } = e
const type = TraceTypeEnum.error
const subType = TraceSubTypeEnum.cors
const reportData = {
type,
subType,
message
}
lazyReportBatch(reportData)
}
资源加载错误捕获
收集 JavaScript、CSS 和图片等加载错误
const initResourceError = (e: Event) => {
// 通过 e.target 确定错误是发生在哪个资源上
const target = e.target as ResourceErrorTarget
// img是src,link就是href
const src = target.src || target.href
const type = e.type
const subType = TraceSubTypeEnum.resource
const tagName = target.tagName
const message = ''
const html = target.outerHTML
// 获取dom加载位置
const path = getPathToElement(target)
const behavior = getBehaviour()
const state = behavior?.breadcrumbs?.state || []
const reportData: ResourceErrorType = {
type,
subType,
tagName,
message,
html,
src,
pageUrl: window.location.href,
path,
errId: getErrorUid(`${subType}-${message}-${src}`),
state,
timestamp: new Date().getTime()
}
lazyReportBatch(reportData)
}
获取dom的路径方法
export function getPathToElement(element: any) {
const path = []
let currentElement = element
try {
while (currentElement?.tagName?.toLowerCase() !== 'body') {
const parentNode = currentElement.parentNode
const children = Array.from(parentNode?.children)
const nodeIndex = children.indexOf(currentElement) + 1
const name = `${currentElement.tagName.toLowerCase()}:nth-child(${nodeIndex})`
// 将当前元素的标签和其兄弟索引添加到路径数组中
path.unshift(name)
// 移动到父元素
currentElement = parentNode
}
} catch (error) {
console.log(error)
}
// 最后添加 body 标签
path.unshift('body')
return path.join(' > ')
}
然后我们可以在demo网站上测试一下
上报的数据
promise错误捕获
promise的错误是通过监听unhandledrejection事件捕获的,但捕获到的错误是无法获取到错误文件还是错误行列数的
window.addEventListener(
'unhandledrejection',
(e: PromiseRejectionEvent) => {
const stack = parseStackFrames(e.reason)
const behavior = getBehaviour()
const state = behavior?.breadcrumbs?.state || []
const eventData = getRecordScreenData()
const reportData: PromiseErrorType = {
type: TraceTypeEnum.error,
subType: TraceSubTypeEnum.promise,
message: e.reason.message || e.reason,
stack,
pageUrl: window.location.href,
errId: getErrorUid(`'promise'-${e.reason.message}`),
state,
timestamp: new Date().getTime(),
eventData
}
// todo 发送错误信息
lazyReportBatch(reportData)
},
true
)
随便写一个promise错误
new Promise((resolve, reject) => {
reject('promise错误')
})
上报的数据
react错误捕获
React16开始,官方提供ErrorBoundary错误边界,被该组件包裹的子组件render函数报错时会触发离当前组件最近的父组件ErrorBoundary
这种情况下,可以通过componentDidCatch将捕获的错误上报
import React, { ReactNode } from 'react'
import { lazyReportBatch } from '../common/report'
import {
getErrorUid,
getReactComponentInfo,
parseStackFrames
} from '../common/utils'
import { ReactErrorType } from '../types'
import { TraceSubTypeEnum, TraceTypeEnum } from '../common/enum'
import { getBehaviour, getRecordScreenData } from '../behavior'
interface ErrorBoundaryProps {
Fallback: ReactNode // ReactNode 表示任意有效的 React 内容
children: ReactNode
}
interface ErrorBoundaryState {
hasError: boolean
}
let err = {}
class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { hasError: false }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ hasError: true })
const { componentName, url: src } = getReactComponentInfo(errorInfo)
const type = TraceTypeEnum.error
const subType = TraceSubTypeEnum.react
const message = error.message
const stack = parseStackFrames(error)
const pageUrl = window.location.href
const errId = getErrorUid(`${subType}-${message}-${src}`)
const info = error.message
const behavior = getBehaviour()
const state = behavior?.breadcrumbs?.state || []
const eventData = getRecordScreenData()
const reportData: ReactErrorType = {
type,
subType,
stack,
pageUrl,
message,
errId,
componentName,
info,
src,
state,
timestamp: new Date().getTime(),
eventData
}
err = reportData
lazyReportBatch(reportData)
}
render() {
const { Fallback } = this.props
if (this.state.hasError) {
// @ts-ignore
return <Fallback error={err}/>
}
return this.props.children
}
}
export default ErrorBoundary
上报的数据
在框架中报错,会有这样的错误栈
我们需要根据这样的错误栈获取react错误组件的信息
export const getReactComponentInfo = (errorInfo: React.ErrorInfo) => {
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>'
const ROOT_COMPONENT_NAME = '<Root>'
// 获取 React 组件调用栈
const componentStack = errorInfo.componentStack || ''
/**
* 提取组件调用栈中最后一个出错的组件
* React 的 componentStack 通常包含如下格式:
* at ComponentName (filePath:line:column)
* at AnotherComponent (filePath:line:column)
*/
const extractComponentName = (stack: string) => {
const match = stack.match(/at\s+([^\s]+)\s+(/)
if (match && match[1]) {
return `<${match[1]}>`
}
return ANONYMOUS_COMPONENT_NAME
}
const extractComponentFile = (stack: string) => {
const match = stack.match(/(([^)]+))/)
if (match && match[1]) {
return match[1] // 返回文件路径及位置
}
return ''
}
// 提取信息
const componentName =
extractComponentName(componentStack) || ROOT_COMPONENT_NAME
const componentFile = extractComponentFile(componentStack)
return {
componentName,
url: componentFile
}
}
然后在我们项目中这样使用就好
补充
因为ErrorBoundary是类组件它需要extends React.Component,而我们是做一个sdk,总不能其他项目用我们的库还得还得安装react的依赖吧?所以就需要在package.json配置
{
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
}
然后在rollup.config.js配置
module.exports = [
{
plugins: [...],
external: ['react', 'react-dom'], // 声明外部依赖
}
]
vue错误捕获
errorHandler
是 Vue 提供的全局错误处理方法,用于捕获组件渲染、事件处理、生命周期钩子和子组件中的未捕获错误。开发者可以通过 errorHandler
实现错误的统一处理和上报。
import { getBehaviour, getRecordScreenData } from '../behavior'
import { TraceSubTypeEnum, TraceTypeEnum } from '../common/enum'
import { lazyReportBatch } from '../common/report'
import {
getErrorUid,
getVueComponentInfo,
parseStackFrames
} from '../common/utils'
import { VueErrorType } from '../types'
// 初始化 Vue异常 的数据获取和上报
export interface Vue {
config: {
errorHandler?: any
warnHandler?: any
}
}
const initVueError = (app: Vue) => {
app.config.errorHandler = (err: Error, vm: any, info: string) => {
console.error(err)
const { componentName, url: src } = getVueComponentInfo(vm)
const type = TraceTypeEnum.error
const subType = TraceSubTypeEnum.vue
const message = err.message
const stack = parseStackFrames(err)
const pageUrl = window.location.href
const behavior = getBehaviour()
const state = behavior?.breadcrumbs?.state || []
const eventData = getRecordScreenData()
const reportData: VueErrorType = {
type,
subType,
message,
stack,
pageUrl,
info,
componentName,
src,
errId: getErrorUid(`${subType}-${message}-${src}`),
state,
timestamp: new Date().getTime(),
eventData
}
lazyReportBatch(reportData)
}
}
export default initVueError
获取vue错误组件
// 获取vue报错组件信息
export const getVueComponentInfo = (vm: any) => {
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>'
if (!vm) {
return {
componentName: ANONYMOUS_COMPONENT_NAME,
url: ''
}
}
if (vm.$root === vm) {
return {
componentName: ROOT_COMPONENT_NAME,
url: ''
}
}
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 {
componentName: name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME,
url: file
}
}
vue3使用方法
const app = createApp(App)
initVueError(app)
vue2使用方法
import Vue from 'vue'
initVueError(Vue)
vue错误上报信息
尾言
以上就是错误监控的全部内容了,笔者不一定正确,如果大家有发现有错误或有优化的地方也可以在评论区指出。下一篇会写用户行为栈的内容。
最后如果文章对你有帮助也希望能给我点个赞,给我的开源项目点个star,monitor-sdk 你的支持是我学习的最大动力。