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,163 @@
/*
* 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 { isNumber, isString, mapValues, omitBy, isNil } from 'lodash-es';
import type { SlardarInstance } from '@coze-studio/slardar-interface';
import {
type CommonLogOptions,
type LoggerReportClient,
LogAction,
LogLevel,
} from '../types';
import {
toFlatPropertyMap,
safeJson,
getErrorRecord,
getLogLevel,
getSlardarLevel,
} from './utils';
/**
* 将 meta 根据类型转换成
* - 指标 metrics可以被度量的值也就是数值
* - 维度 categories分类维度用来做筛选分组
*
* @param meta
* @returns
*/
function metaToMetricsCategories(meta?: Record<string, unknown>) {
const metrics: Record<string, number> = {};
const categories: Record<string, string> = {};
for (const k in meta) {
const val = meta[k];
if (isNumber(val)) {
metrics[k] = val;
} else {
categories[k] = isString(val) ? val : safeJson.stringify(val);
}
}
return {
metrics,
categories,
};
}
/**
* Record<string, unknown> => Record<string, string | number>
*/
function normalizeExtra(record: Record<string, unknown>) {
const result: Record<string, string | number> = {};
for (const k in record) {
const val = record[k];
if (isNumber(val) || isString(val)) {
result[k] = val;
} else {
result[k] = safeJson.stringify(val);
}
}
return result;
}
export class SlardarReportClient implements LoggerReportClient {
slardarInstance: SlardarInstance;
constructor(slardarInstance: SlardarInstance) {
// 业务项目里可能有多个 slardar 版本多个版本的类型不兼容constructor 里约束版本会存在问题 => 放开。
this.slardarInstance = slardarInstance;
if (!this.slardarInstance) {
console.warn('expected slardarInstance but get undefined/null');
}
}
send(options: CommonLogOptions) {
if (!options.action?.includes(LogAction.PERSIST)) {
// 非持久化日志,不上报
return;
}
const { level, message, action, eventName, meta, error, ...rest } = options;
// Slardar API
const resolveMeta = (inputs: Record<string, unknown>) =>
toFlatPropertyMap(
{
...rest,
...inputs,
error: getErrorRecord(error),
level: getLogLevel(level),
},
{
maxDepth: 4,
},
);
if (level === LogLevel.ERROR && meta?.reportJsError === true) {
const { reportJsError, reactInfo, ...restMeta } = meta || {};
const resolvedMeta = resolveMeta({
...restMeta,
message,
eventName,
});
// 上报 JS 异常
this.slardarInstance?.(
'captureException',
error,
omitBy(
mapValues(resolvedMeta, (v: unknown) =>
isString(v) ? v : safeJson.stringify(v),
),
isNil,
),
reactInfo as {
version: string;
componentStack: string;
},
);
} else if (eventName) {
const resolvedMeta = resolveMeta({
...meta,
});
const { metrics, categories } = metaToMetricsCategories(resolvedMeta);
// 上报独立的事件
this.slardarInstance?.('sendEvent', {
name: eventName,
metrics,
categories,
});
} else if (message) {
const resolvedMeta = resolveMeta({
...meta,
});
// 上报日志
this.slardarInstance?.('sendLog', {
level: getSlardarLevel(level),
content: message,
// slardar 内部会对 extra 处理分类number 类型的字段放入 metrics其他放入 categories
extra: normalizeExtra(resolvedMeta),
});
}
}
}
export type { SlardarInstance };

View 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 { reporter } from '../reporter';
export const getSlardarInstance = () => reporter.slardarInstance;
// 异步设置 coze 的 uid 信息
export const setUserInfoContext = (userInfo: DataItem.UserInfo) => {
const slardarInstance = getSlardarInstance();
if (slardarInstance) {
slardarInstance?.('context.set', 'coze_uid', userInfo?.user_id_str);
}
};

View File

@@ -0,0 +1,175 @@
/*
* 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 { LogLevel, type ApiErrorOption, ErrorType } from '../types';
function isObject<T>(it: T): it is object extends T
? // Narrow the `{}` type to an unspecified object
T & Record<string | number | symbol, unknown>
: unknown extends T
? // treat unknown like `{}`
T & Record<string | number | symbol, unknown>
: T extends object // function, array or actual object
? T extends readonly unknown[]
? never // not an array
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends (...args: any[]) => any
? never // not a function
: T // no, an actual object
: never {
// This is necessary because:
// typeof null === 'object'
// typeof [] === 'object'
// [] instanceof Object === true
return Object.prototype.toString.call(it) === '[object Object]';
}
export function toFlatPropertyMap(
inputObj: Record<string, unknown>,
options?: {
keySeparator?: string;
maxDepth?: number;
},
) {
const { keySeparator = '.', maxDepth } = options || {};
const flattenRecursive = (
obj: Record<string, unknown>,
propertyMap: Record<string, unknown>,
depth = 1,
parentKey?: string,
// eslint-disable-next-line max-params
) => {
for (const [key, value] of Object.entries(obj)) {
const path = parentKey ? `${parentKey}${keySeparator}${key}` : key;
const currentDepth = depth + 1;
if (value && isObject(value) && (!maxDepth || currentDepth <= maxDepth)) {
flattenRecursive(value, propertyMap, currentDepth, path);
} else {
propertyMap[path] = value;
}
}
return propertyMap;
};
const result: Record<string, unknown> = {};
return flattenRecursive(inputObj, result);
}
export const safeJson = (() => {
const stringify = (sth: unknown): string => {
try {
return JSON.stringify(sth);
} catch (e) {
console.error(e);
return `JSON stringify Error: ${(e as Error).message}`;
}
};
const parse = (sth?: string) => {
try {
return JSON.parse(sth || '');
} catch (e) {
console.error(e);
return null;
}
};
return {
stringify,
parse,
};
})();
export class ApiError extends Error {
errorOption: ApiErrorOption;
constructor(option: ApiErrorOption) {
super(
`httpStatus=${option.httpStatus}, code=${option.code}, message=${option.message}, logId=${option.logId}`,
);
this.name = 'ApiError';
this.errorOption = option;
}
}
export const getErrorType = (error?: ApiError | Error): string => {
if (!error) {
return ErrorType.Unknown;
}
if (error instanceof ApiError) {
// 优先用业务给的 api error type
if (error.errorOption?.errorType) {
return error.errorOption.errorType;
}
return ErrorType.ApiError;
}
return ErrorType.Unknown;
};
export const getApiErrorRecord = (
error: ApiError | Error,
): Record<string, unknown> => {
if (error instanceof ApiError && error.errorOption) {
const { errorOption } = error;
return {
httpStatus: errorOption.httpStatus,
code: errorOption.code,
logId: errorOption.logId,
response: safeJson.stringify(errorOption.response),
requestConfig: safeJson.stringify(errorOption.requestConfig),
};
}
return {};
};
export const getErrorRecord = (
error?: ApiError | Error,
): Record<string, string | number | undefined> => {
if (!error) {
return {};
}
return {
...getApiErrorRecord(error),
message: error.message,
stack: error.stack,
type: getErrorType(error),
};
};
const levelMap = {
[LogLevel.INFO]: 'info',
[LogLevel.SUCCESS]: 'success',
[LogLevel.WARNING]: 'warn',
[LogLevel.ERROR]: 'error',
[LogLevel.FATAL]: 'fatal',
};
export function getLogLevel(level = LogLevel.INFO) {
return levelMap[level];
}
/** Slardar 自定义事件级别,默认是 info, 可枚举项 debug | info | warn | error */
const slardarLevelMap = {
[LogLevel.INFO]: 'info',
[LogLevel.SUCCESS]: 'info',
[LogLevel.WARNING]: 'warn',
[LogLevel.ERROR]: 'error',
[LogLevel.FATAL]: 'error',
};
export function getSlardarLevel(level = LogLevel.INFO) {
return slardarLevelMap[level];
}