feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
37
frontend/packages/arch/bot-error/README.md
Normal file
37
frontend/packages/arch/bot-error/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# @coze-arch/bot-error 使用文档
|
||||
## 安装
|
||||
```
|
||||
cd ${path/to/project}
|
||||
rush add @coze-arch/bot-error
|
||||
```
|
||||
## 使用
|
||||
提供了一些用于错误处理的工具。
|
||||
|
||||
### CustomError
|
||||
|
||||
CustomError 是一个自定义错误类,用来代替原生的Error,使用 throw new CustomerError抛出的错误会上报到slardar的自定义事件中,使用方可以根据需要进行监控处理,不会统计到jsError
|
||||
|
||||
```
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
throw new CustomError('parmasValidation', 'empty copy');
|
||||
|
||||
```
|
||||
|
||||
### isCustomError
|
||||
|
||||
isCustomError 是一个函数,它可以检查一个错误是否是 CustomError
|
||||
|
||||
```
|
||||
import { isCustomError, CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
const myError = new CustomError('parmasValidation', 'empty copy');
|
||||
|
||||
console.log(isCustomError(error)); // 输出:true
|
||||
```
|
||||
|
||||
### useErrorCatch
|
||||
|
||||
useErrorCatch 是一个 React Hook,它可以帮助你在组件中捕获和处理错误。
|
||||
1. 监听全局 unhandledrejection、error 事件,上报相关内容由业务侧自行补充
|
||||
2. 对于已知错误,上报自定义事件进行监控
|
||||
138
frontend/packages/arch/bot-error/__tests__/certain-error.test.ts
Normal file
138
frontend/packages/arch/bot-error/__tests__/certain-error.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mock } from 'vitest';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { isApiError } from '@coze-arch/bot-http';
|
||||
|
||||
import {
|
||||
getErrorName,
|
||||
isCertainError,
|
||||
sendCertainError,
|
||||
} from '../src/certain-error';
|
||||
import { isCustomError, isChunkError } from '../src';
|
||||
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
createLoggerWith: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
persist: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
})),
|
||||
},
|
||||
reporter: {
|
||||
errorEvent: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock('axios', async () => {
|
||||
const actual: Record<string, unknown> = await vi.importActual('axios');
|
||||
return {
|
||||
...actual,
|
||||
isAxiosError: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('@coze-arch/bot-http', () => ({
|
||||
isApiError: vi.fn(),
|
||||
}));
|
||||
vi.mock('../src/custom-error', () => ({
|
||||
isCustomError: vi.fn(),
|
||||
}));
|
||||
vi.mock('../src/source-error', () => ({
|
||||
isChunkError: vi.fn(),
|
||||
}));
|
||||
const isNoInstanceError = vi.fn();
|
||||
const errorFuncList = [
|
||||
{
|
||||
func: isCustomError,
|
||||
name: 'CustomError',
|
||||
},
|
||||
{
|
||||
func: isAxiosError,
|
||||
name: 'AxiosError',
|
||||
},
|
||||
{
|
||||
func: isApiError,
|
||||
name: 'ApiError',
|
||||
},
|
||||
{
|
||||
func: isChunkError,
|
||||
name: 'ChunkLoadError',
|
||||
},
|
||||
{
|
||||
func: isNoInstanceError,
|
||||
name: 'notInstanceError',
|
||||
},
|
||||
];
|
||||
|
||||
describe('bot-error-certain-error', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('getErrorName', () => {
|
||||
const resNull = getErrorName(null);
|
||||
expect(resNull).equal('unknown');
|
||||
errorFuncList.forEach(item => {
|
||||
const { func } = item;
|
||||
(func as Mock).mockReturnValueOnce(true);
|
||||
const error = item.name === 'notInstanceError' ? {} : new Error();
|
||||
const res = getErrorName(error as Error);
|
||||
expect(res).equal(item.name);
|
||||
});
|
||||
});
|
||||
|
||||
test('isCertainError', () => {
|
||||
errorFuncList.forEach(item => {
|
||||
const { func } = item;
|
||||
(func as Mock).mockReturnValueOnce(true);
|
||||
const error = item.name === 'notInstanceError' ? {} : new Error();
|
||||
const res = isCertainError(error as Error);
|
||||
expect(res).equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('sendCertainError', () => {
|
||||
errorFuncList.forEach(item => {
|
||||
const handle = vi.fn();
|
||||
(item.func as Mock).mockReturnValue(true);
|
||||
const error = item.name === 'notInstanceError' ? {} : new Error();
|
||||
sendCertainError(error as Error, handle);
|
||||
expect(handle).not.toHaveBeenCalled();
|
||||
(item.func as Mock).mockReturnValue(false);
|
||||
sendCertainError(new Error(), handle);
|
||||
expect(handle).toHaveBeenCalled();
|
||||
});
|
||||
// notInstanceError json stringify 失败的单测
|
||||
errorFuncList.forEach(item => {
|
||||
const handle = vi.fn();
|
||||
if (item.name !== 'notInstanceError') {
|
||||
return;
|
||||
}
|
||||
(item.func as Mock).mockReturnValue(true);
|
||||
// JSON stringify 会报错的 case
|
||||
const b = { a: {} };
|
||||
const a = { b: {}, name: 'notInstanceError' };
|
||||
b.a = a;
|
||||
a.b = b;
|
||||
const error = item.name === 'notInstanceError' ? a : new Error();
|
||||
sendCertainError(error as Error, handle);
|
||||
expect(handle).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomError, isCustomError } from '../src/custom-error';
|
||||
|
||||
describe('bot-error-custom-error', () => {
|
||||
test('should create custom-error correctly', () => {
|
||||
const eventName = 'custom_error';
|
||||
const eventMsg = 'err_msg';
|
||||
const customError = new CustomError(eventName, eventMsg);
|
||||
expect(customError).toBeInstanceOf(Error);
|
||||
expect(customError.name).equal('CustomError');
|
||||
expect(customError.eventName).equal(eventName);
|
||||
expect(customError.msg).equal(customError.message).equal(eventMsg);
|
||||
});
|
||||
|
||||
test('should judge custom-error correctly', () => {
|
||||
const nonCustomError = new Error();
|
||||
const customError = new CustomError('test', 'test');
|
||||
expect(isCustomError(nonCustomError)).toBeFalsy();
|
||||
expect(isCustomError(customError)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
isWebpackChunkError,
|
||||
isThirdPartyJsChunkError,
|
||||
isCssChunkError,
|
||||
isChunkError,
|
||||
} from '../src/source-error';
|
||||
|
||||
describe('bot-error-source-error', () => {
|
||||
test('isWebpackChunkError', () => {
|
||||
const chunkError = new Error();
|
||||
chunkError.name = 'ChunkLoadError';
|
||||
expect(isWebpackChunkError(chunkError)).toBeTruthy();
|
||||
expect(isChunkError(chunkError)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isThirdPartyJsChunkError', () => {
|
||||
const loadingChunkError = new Error();
|
||||
loadingChunkError.message = 'Loading chunk xxxx';
|
||||
expect(isThirdPartyJsChunkError(loadingChunkError)).toBeTruthy();
|
||||
expect(isChunkError(loadingChunkError)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isCssChunkError', () => {
|
||||
const cssLoadingChunkError = new Error();
|
||||
cssLoadingChunkError.message = 'Loading CSS chunk xxx';
|
||||
expect(isCssChunkError(cssLoadingChunkError)).toBeTruthy();
|
||||
expect(isChunkError(cssLoadingChunkError)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mock } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { type SlardarInstance } from '@coze-arch/logger';
|
||||
|
||||
import { useErrorCatch } from '../src/use-error-catch';
|
||||
import { isCertainError, sendCertainError } from '../src/certain-error';
|
||||
import { CustomError } from '../src';
|
||||
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
createLoggerWith: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
persist: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock window event listeners without overriding the entire window object
|
||||
const mockAddEventListener = vi.fn(
|
||||
(e: string, cb: (event: unknown) => void) => {
|
||||
if (e === 'unhandledrejection') {
|
||||
cb({
|
||||
promise: Promise.reject(new Error()),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
const mockRemoveEventListener = vi.fn();
|
||||
|
||||
// Spy on and mock the window methods instead of replacing the entire window
|
||||
Object.defineProperty(window, 'addEventListener', {
|
||||
value: mockAddEventListener,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'removeEventListener', {
|
||||
value: mockRemoveEventListener,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
vi.mock('../src/certain-error');
|
||||
|
||||
describe('use-error-catch', () => {
|
||||
test('Should handle promise rejection correctly', () => {
|
||||
const slardarInstance = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock normal error
|
||||
(isCertainError as Mock).mockReturnValue(false);
|
||||
slardarInstance.on.mockImplementationOnce(
|
||||
(_: string, cb: (e: { payload: { error: Error } }) => void) => {
|
||||
cb({ payload: { error: new Error() } });
|
||||
},
|
||||
);
|
||||
const { unmount } = renderHook(() =>
|
||||
useErrorCatch(slardarInstance as unknown as SlardarInstance),
|
||||
);
|
||||
unmount();
|
||||
expect(mockAddEventListener).toHaveBeenCalled();
|
||||
expect(mockRemoveEventListener).toHaveBeenCalled();
|
||||
expect(slardarInstance.on).toHaveBeenCalled();
|
||||
expect(slardarInstance.off).toHaveBeenCalled();
|
||||
expect(sendCertainError).not.toHaveBeenCalled();
|
||||
|
||||
// Mock certain error
|
||||
(isCertainError as Mock).mockReturnValue(true);
|
||||
slardarInstance.on.mockImplementationOnce(
|
||||
(_: string, cb: (e: { payload: { error: Error } }) => void) => {
|
||||
const error = new CustomError('test', 'test');
|
||||
error.name = 'CustomError';
|
||||
cb({ payload: { error } });
|
||||
},
|
||||
);
|
||||
renderHook(() =>
|
||||
useErrorCatch(slardarInstance as unknown as SlardarInstance),
|
||||
);
|
||||
unmount();
|
||||
expect(sendCertainError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { sendCertainError } from '../src/certain-error';
|
||||
import { CustomError, useRouteErrorCatch } from '../src';
|
||||
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
createLoggerWith: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
persist: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
})),
|
||||
},
|
||||
}));
|
||||
vi.mock('../src/custom-error');
|
||||
vi.mock('../src/certain-error');
|
||||
|
||||
describe('useRouteErrorCatch', () => {
|
||||
test('Should handle route error correctly', () => {
|
||||
// normal error
|
||||
renderHook(() => useRouteErrorCatch(new Error()));
|
||||
expect(sendCertainError).toHaveBeenCalled();
|
||||
|
||||
// custom error
|
||||
renderHook(() => useRouteErrorCatch(new CustomError('test', 'test')));
|
||||
expect(sendCertainError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
12
frontend/packages/arch/bot-error/config/rush-project.json
Normal file
12
frontend/packages/arch/bot-error/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/arch/bot-error/eslint.config.js
Normal file
7
frontend/packages/arch/bot-error/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
38
frontend/packages/arch/bot-error/package.json
Normal file
38
frontend/packages/arch/bot-error/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@coze-arch/bot-error",
|
||||
"version": "0.0.1",
|
||||
"description": "bot error",
|
||||
"license": "Apache-2.0",
|
||||
"author": "haozhenfei@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-http": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"axios": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"debug": "^4.3.4",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"// deps": "debug@^4.3.4 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
161
frontend/packages/arch/bot-error/src/certain-error.ts
Normal file
161
frontend/packages/arch/bot-error/src/certain-error.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isAxiosError } from 'axios';
|
||||
import { logger, reporter } from '@coze-arch/logger';
|
||||
import { isApiError } from '@coze-arch/bot-http';
|
||||
|
||||
import { isChunkError } from './source-error';
|
||||
import { isCustomError, type CustomError } from './custom-error';
|
||||
import { ReportEventNames, type CertainErrorName } from './const';
|
||||
const loggerWithScope = logger.createLoggerWith({
|
||||
ctx: {
|
||||
namespace: 'bot-error',
|
||||
scope: 'certain-error',
|
||||
},
|
||||
});
|
||||
const notInstanceError = (error: Error) => !(error instanceof Error);
|
||||
|
||||
const errorList: { func: (error: Error) => boolean; name: CertainErrorName }[] =
|
||||
[
|
||||
{
|
||||
func: isCustomError,
|
||||
name: 'CustomError',
|
||||
},
|
||||
{
|
||||
func: isAxiosError,
|
||||
name: 'AxiosError',
|
||||
},
|
||||
{
|
||||
func: isApiError,
|
||||
name: 'ApiError',
|
||||
},
|
||||
{
|
||||
func: isChunkError,
|
||||
name: 'ChunkLoadError',
|
||||
},
|
||||
{
|
||||
func: notInstanceError,
|
||||
name: 'notInstanceError',
|
||||
},
|
||||
];
|
||||
|
||||
const handleCertainError: (error: Error) => void = error => {
|
||||
const errorName = getErrorName(error);
|
||||
|
||||
loggerWithScope.info({
|
||||
message: 'handleCertainError',
|
||||
meta: {
|
||||
errorName,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
if (errorName === 'unknown') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 上报到自定义错误
|
||||
if (errorName === 'CustomError') {
|
||||
const { eventName, msg } = error as CustomError;
|
||||
// 补充统一上报custom error event_name 用于监控
|
||||
loggerWithScope.persist.error({
|
||||
eventName: ReportEventNames.CustomErrorReport,
|
||||
message: msg,
|
||||
error,
|
||||
meta: {
|
||||
name: error.name,
|
||||
originEventName: eventName, // 原始originEventName
|
||||
originErrorMessage: msg, // 原始 error msg
|
||||
},
|
||||
});
|
||||
loggerWithScope.persist.error({
|
||||
eventName,
|
||||
message: msg,
|
||||
error,
|
||||
meta: {
|
||||
name: error.name,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 滤除已经上报到自定义事件
|
||||
if (errorName === 'ApiError' || errorName === 'AxiosError') {
|
||||
return;
|
||||
}
|
||||
|
||||
// ChunkLoad 失败, 不上报,在slardar 静态资源异常统计
|
||||
if (errorName === 'ChunkLoadError') {
|
||||
reporter.info({
|
||||
message: 'chunkLoadError',
|
||||
meta: {
|
||||
error,
|
||||
errorName: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 不继承 Error 的错误,目前 case(semi 表单校验 )
|
||||
if (errorName === 'notInstanceError') {
|
||||
let errorInfo;
|
||||
try {
|
||||
errorInfo =
|
||||
typeof error === 'object' ? JSON.stringify(error) : String(error);
|
||||
} catch (e) {
|
||||
errorInfo = 'notInstanceError json is invalid';
|
||||
}
|
||||
loggerWithScope.persist.error({
|
||||
eventName: ReportEventNames.NotInstanceError,
|
||||
message: errorInfo,
|
||||
error,
|
||||
meta: {
|
||||
name: 'notInstanceError',
|
||||
errorInfo,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
export const getErrorName = (error: Error) => {
|
||||
if (!error) {
|
||||
return 'unknown';
|
||||
}
|
||||
const result = errorList.find(({ func }) => func(error));
|
||||
if (result && result.name) {
|
||||
return result.name;
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const isCertainError = (error: Error) => {
|
||||
const errorName = getErrorName(error);
|
||||
return errorName !== 'unknown';
|
||||
};
|
||||
export const sendCertainError = (
|
||||
error: Error,
|
||||
handle?: (reason: string) => void,
|
||||
) => {
|
||||
if (isCertainError(error)) {
|
||||
handleCertainError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
handle?.(error?.message);
|
||||
};
|
||||
42
frontend/packages/arch/bot-error/src/const.ts
Normal file
42
frontend/packages/arch/bot-error/src/const.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ReportEventNames {
|
||||
/**
|
||||
* 通用异常错误
|
||||
*/
|
||||
ChunkLoadError = 'chunk_load_error', // webpack chunk load 失败
|
||||
Unhandledrejection = 'unhandledrejection', // 异步错误兜底
|
||||
GlobalErrorBoundary = 'global_error_boundary', // 全局的errorBoundary 错误
|
||||
NotInstanceError = 'notInstanceError',
|
||||
CustomErrorReport = 'custom_error_report', // 统一上报的custom error
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已经明确的错误
|
||||
*
|
||||
* 1、CustomError: 业务方 throw new CustomError(ReportEventNames.xxx, 'xxx')
|
||||
* 2、AxiosError: 状态码非 2xx;
|
||||
* 3、ApiError: 状态码 2xx & 业务code !== 0
|
||||
* 4、ChunkLoadError: webpack chunk load 失败
|
||||
* 5、notInstanceError,不继承 Error 的错误,目前 case(semi 表单校验 )
|
||||
*/
|
||||
export type CertainErrorName =
|
||||
| 'CustomError'
|
||||
| 'AxiosError'
|
||||
| 'ApiError'
|
||||
| 'ChunkLoadError'
|
||||
| 'notInstanceError';
|
||||
36
frontend/packages/arch/bot-error/src/custom-error.ts
Normal file
36
frontend/packages/arch/bot-error/src/custom-error.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class CustomError extends Error {
|
||||
constructor(
|
||||
public eventName: string,
|
||||
public msg: string,
|
||||
public ext?: {
|
||||
customGlobalErrorConfig?: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
super(msg);
|
||||
this.name = 'CustomError';
|
||||
this.ext = ext;
|
||||
}
|
||||
}
|
||||
// sladar beforeSend捕获到的错误需要通过.name判断错误类型
|
||||
export const isCustomError = (error: unknown): error is CustomError =>
|
||||
error instanceof CustomError ||
|
||||
(error as CustomError)?.name === 'CustomError';
|
||||
17
frontend/packages/arch/bot-error/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/bot-error/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
23
frontend/packages/arch/bot-error/src/index.ts
Normal file
23
frontend/packages/arch/bot-error/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { CustomError, isCustomError } from './custom-error';
|
||||
|
||||
export { useErrorCatch } from './use-error-catch';
|
||||
|
||||
export { isChunkError } from './source-error';
|
||||
|
||||
export { useRouteErrorCatch } from './use-route-error-catch';
|
||||
31
frontend/packages/arch/bot-error/src/source-error.ts
Normal file
31
frontend/packages/arch/bot-error/src/source-error.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const isWebpackChunkError = (error: Error) =>
|
||||
error.name === 'ChunkLoadError';
|
||||
|
||||
// Loading chunk 3 failed. (error: )
|
||||
export const isThirdPartyJsChunkError = (error: Error & { type?: string }) =>
|
||||
error.message?.startsWith('Loading chunk');
|
||||
|
||||
// Loading CSS chunk 8153 failed. ()
|
||||
export const isCssChunkError = (error: Error) =>
|
||||
error.message?.startsWith('Loading CSS chunk');
|
||||
|
||||
export const isChunkError = (error: Error) =>
|
||||
isWebpackChunkError(error) ||
|
||||
isThirdPartyJsChunkError(error) ||
|
||||
isCssChunkError(error);
|
||||
84
frontend/packages/arch/bot-error/src/use-error-catch.ts
Normal file
84
frontend/packages/arch/bot-error/src/use-error-catch.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { logger, type SlardarInstance } from '@coze-arch/logger';
|
||||
|
||||
import { ReportEventNames } from './const';
|
||||
import {
|
||||
sendCertainError,
|
||||
isCertainError,
|
||||
getErrorName,
|
||||
} from './certain-error';
|
||||
|
||||
const loggerWithScope = logger.createLoggerWith({
|
||||
ctx: {
|
||||
namespace: 'bot-error',
|
||||
scope: 'use-error-catch',
|
||||
},
|
||||
});
|
||||
|
||||
export const useErrorCatch = (slardarInstance: SlardarInstance) => {
|
||||
// 1. promise rejection
|
||||
useEffect(() => {
|
||||
const handlePromiseRejection = (event: PromiseRejectionEvent) => {
|
||||
event.promise.catch(error => {
|
||||
loggerWithScope.info({
|
||||
message: 'handlePromiseRejection',
|
||||
meta: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
sendCertainError(error, reason => {
|
||||
loggerWithScope.persist.error({
|
||||
eventName: ReportEventNames.Unhandledrejection,
|
||||
message: reason || 'unhandledrejection',
|
||||
error,
|
||||
meta: {
|
||||
reportJsError: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
window.addEventListener('unhandledrejection', handlePromiseRejection);
|
||||
return () => {
|
||||
window.removeEventListener('unhandledrejection', handlePromiseRejection);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 3. 拦截 slardar 上报
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const beforeSlardarSend = (e: any) => {
|
||||
const error = e?.payload?.error;
|
||||
if (
|
||||
error &&
|
||||
isCertainError(error) &&
|
||||
getErrorName(error) !== 'notInstanceError'
|
||||
) {
|
||||
sendCertainError(error);
|
||||
return false;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
slardarInstance?.on('beforeSend', beforeSlardarSend);
|
||||
return () => {
|
||||
slardarInstance?.off('beforeSend', beforeSlardarSend);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { logger } from '@coze-arch/logger';
|
||||
|
||||
import { CustomError } from './custom-error';
|
||||
import { ReportEventNames } from './const';
|
||||
import { sendCertainError } from './certain-error';
|
||||
|
||||
const loggerWithScope = logger.createLoggerWith({
|
||||
ctx: {
|
||||
namespace: 'bot-global-error',
|
||||
},
|
||||
});
|
||||
|
||||
export const useRouteErrorCatch = (error: unknown) => {
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
// 处理不是error实例的情况
|
||||
const realError =
|
||||
error instanceof Error
|
||||
? error
|
||||
: new CustomError(
|
||||
ReportEventNames.GlobalErrorBoundary,
|
||||
`global error route catch error infos:${String(error)}`,
|
||||
);
|
||||
// 过滤 其他error
|
||||
sendCertainError(realError, () => {
|
||||
loggerWithScope.persist.error({
|
||||
eventName: ReportEventNames.GlobalErrorBoundary,
|
||||
message: realError.message || 'global error route catch error',
|
||||
error: realError,
|
||||
meta: {
|
||||
name: realError.name,
|
||||
reportJsError: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
};
|
||||
0
frontend/packages/arch/bot-error/stories/.gitkeep
Normal file
0
frontend/packages/arch/bot-error/stories/.gitkeep
Normal file
31
frontend/packages/arch/bot-error/tsconfig.build.json
Normal file
31
frontend/packages/arch/bot-error/tsconfig.build.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../bot-http/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../logger/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/arch/bot-error/tsconfig.json
Normal file
15
frontend/packages/arch/bot-error/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
16
frontend/packages/arch/bot-error/tsconfig.misc.json
Normal file
16
frontend/packages/arch/bot-error/tsconfig.misc.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "stories", "vitest.config.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"]
|
||||
}
|
||||
}
|
||||
27
frontend/packages/arch/bot-error/vitest.config.ts
Normal file
27
frontend/packages/arch/bot-error/vitest.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
test: {
|
||||
coverage: {
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user