import { getErrorReportApi } from '@/api/common'
interface ErrorData {
type: 'script' | 'resource' | 'promise' | 'manual'
message?: string
source?: string
line?: number
column?: number
stack?: string
url?: string
tag?: string
reason?: string
[key: string]: any
}
interface ReportPayload extends ErrorData {
timestamp: number
url: string
userAgent: string
userId: string
}
interface ReporterOptions {
maxPerSecond?: number
sampleRate?: number
}
class ErrorReporter {
private queue: ReportPayload[] = []
private errorCache: Map<string, boolean> = new Map()
private maxPerSecond: number
private lastSent: number = 0
private sampleRate: number
constructor({ maxPerSecond = 5, sampleRate = 1 }: ReporterOptions = {}) {
this.maxPerSecond = maxPerSecond
this.sampleRate = sampleRate
this.init()
}
private init(): void {
window.onerror = (
message: string | Event,
source?: string,
lineno?: number,
colno?: number,
error?: Error
): boolean => {
this.report({
type: 'script',
message: typeof message === 'string' ? message : String(message),
source,
line: lineno,
column: colno,
stack: error?.stack,
})
return true
}
window.addEventListener(
'error',
(e: ErrorEvent) => {
if (e.target !== window) {
const target = e.target as any
this.report({
type: 'resource',
url: target.src || (target as HTMLLinkElement).href,
tag: target.tagName,
})
}
},
true
)
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
this.report({
type: 'promise',
reason: e.reason?.message || String(e.reason),
stack: e.reason?.stack,
})
})
window.addEventListener('unload', () => {
if (this.queue.length) {
navigator.sendBeacon('/api/error/report', JSON.stringify(this.queue))
}
})
setInterval(() => this.errorCache.clear(), 60 * 60 * 1000)
}
public reportError(error: Error, extra: Record<string, any> = {}): void {
this.report({
type: 'manual',
message: error.message,
stack: error.stack,
...extra,
})
}
public reportManual(data: Omit<ErrorData, 'type'>): void {
this.report({
type: 'manual',
...data,
})
}
private report(data: ErrorData): void {
if (Math.random() > this.sampleRate) return
const errorKey = `${data.type}-${data.message}-${data.stack?.substring(0, 50) || ''}`
if (this.errorCache.has(errorKey)) return
this.errorCache.set(errorKey, true)
const payload: ReportPayload = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
userId: localStorage.getItem('userId') || 'anonymous',
...data,
}
if (process.env.NODE_ENV === 'development') {
console.log('Error reported:', payload)
}
this.queue.push(payload)
this.throttleSend()
}
private throttleSend(): void {
const now = Date.now()
if (now - this.lastSent < 1000) {
setTimeout(() => this.throttleSend(), 1000 - (now - this.lastSent))
return
}
const batch = this.queue.splice(0, this.maxPerSecond)
if (batch.length) {
getErrorReportApi(batch)
.catch((err) => console.error('错误上报失败:', err))
this.lastSent = now
}
}
}
const reporter = new ErrorReporter()
export default reporter
import globalComponents from '@/components';
import ArcoVue from '@arco-design/web-vue';
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import { createApp } from 'vue';
import App from './App.vue';
import directive from './directive';
import i18n from './locale';
import './mock';
import router from './router';
import store from './store';
import errorReporter from './utils/errorReporter';
import '@/assets/style/global.less';
import 'virtual:uno.css';
const app = createApp(App);
app.use(ArcoVue, {});
app.use(ArcoVueIcon);
app.use(router);
app.use(store);
app.use(i18n);
app.use(globalComponents);
app.use(directive);
app.config.errorHandler = (err: unknown, instance, info: string) => {
if (err instanceof Error) {
errorReporter.reportError(err, { vueInfo: info });
} else {
errorReporter.reportManual({ message: String(err), vueInfo: info });
}
};
app.mount('#app');
组件中使用
<!-- src/components/TestComponent.vue -->
<template>
<button @click="testError">测试错误</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import errorReporter from '@/utils/errorReporter';
export default defineComponent({
name: 'TestComponent',
methods: {
testError(): void {
try {
throw new Error('手动测试错误');
} catch (e) {
errorReporter.reportError(e as Error, { context: '按钮点击' });
}
},
},
});
</script>
自定义配置
import errorReporter from './utils/errorReporter';
const customReporter = new ErrorReporter({
reportUrl: 'https://api.example.com/error',
maxPerSecond: 10,
sampleRate: 0.1,
});
测试 app.vue
onMounted(() => {
throw new Error('测试全局脚本错误')
})
上报结果
标准错误(reportError)
errorReporter.reportError(err, { vueInfo: info }, customData);
err:一个 Error 对象(如 new Error('测试错误'))。
{ vueInfo: info }:附加上下文。
customData:可选的自定义数据。
返回的数据结构(后端收到的)
假设 err = new Error('测试错误'),info = 'mounted',customData = { requestId: 'abc123' },后端收到的可能是:
[
{
"timestamp": 16987654321,
"url": "http://localhost:3000/test",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"userId": "anonymous",
"type": "manual",
"message": "测试错误",
"stack": "Error: 测试错误\n at testFunction (file:///src/main.ts:10:15)\n at ...",
"vueInfo": "mounted",
"customData": {
"requestId": "abc123"
}
}
]
说明:由于限流设计,后端收到的是批量数据(数组),这里示例中只有一个错误。
字段含义
timestamp:错误发生的时间戳(毫秒),如 16987654321,表示错误的具体时间。
url:当前页面地址,如 "http://localhost:3000/test",告诉后端错误发生在哪个页面。
userAgent:浏览器信息,标识客户端环境,便于排查浏览器相关问题。
userId:用户 ID,默认从 localStorage 获取,若无则为 "anonymous",用于关联用户。
type:错误类型,这里是 "manual",表示手动上报(区别于自动捕获的 script、resource 等)。
message:错误消息,从 err.message 提取,如 "测试错误",描述错误内容。
stack:错误堆栈,从 err.stack 提取,提供调用栈信息,帮助定位代码位置。
vueInfo(来自 extra):Vue 上下文,如 "mounted",表示错误发生在 mounted 生命周期。
customData:自定义数据,如 { "requestId": "abc123" },告诉后端前端传的具体业务数据。
自定义错误(report)
errorReporter.report({ type: 'manual', message: String(err), vueInfo: info, customData });
直接传入一个 ErrorData 对象,手动指定字段。
返回的数据结构(后端收到的)
假设 err = '自定义异常',info = 'fetchData',customData = { status: 404 },后端收到的可能是:
[
{
"timestamp": 16987654322,
"url": "http://localhost:3000/test",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"userId": "anonymous",
"type": "manual",
"message": "自定义异常",
"vueInfo": "fetchData",
"customData": {
"status": 404
}
}
]
字段含义
timestamp:错误时间戳,如 16987654322,同上。
url:当前页面 URL,同上。
userAgent:浏览器信息,同上。
userId:用户 ID,同上。
type:错误类型,手动指定为 "manual",表示自定义上报。
message:错误消息,手动转为字符串,如 "自定义异常",描述错误内容。
vueInfo:自定义附加信息,如 "fetchData",表示错误发生在数据获取逻辑中。
customData:自定义数据,如 { "status": 404 },告诉后端相关业务信息。
(注意):没有 stack,因为自定义错误不依赖 Error 对象,除非手动传入。
错误上报
错误传递的是两种
1 标准的错误(前端正常的err)
2 自定义的错误
“标准错误” vs “自定义错误”
标准错误(reportError):
指的是 JavaScript 的 Error 对象(或其子类,如 TypeError、ReferenceError)。
优点:自带 message 和 stack,提供详细的堆栈跟踪。
使用 reportError 方法,自动解析这些属性。
适用于:try-catch 捕获的异常、Vue 的 errorHandler 抛出的错误。
自定义错误(report):
指的是开发者手动构造的错误数据,不一定是 Error 对象。
优点:完全灵活,可以上报任何格式的数据,不依赖堆栈。
使用 report 方法,需手动指定字段。
适用于:非标准异常(如字符串提示)、业务逻辑错误、自定义日志。