feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View 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 的错误,目前 casesemi 表单校验
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);
};

View 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 的错误,目前 casesemi 表单校验
*/
export type CertainErrorName =
| 'CustomError'
| 'AxiosError'
| 'ApiError'
| 'ChunkLoadError'
| 'notInstanceError';

View 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';

View 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' />

View 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';

View 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);

View 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);
};
}, []);
};

View File

@@ -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]);
};