XuqmGroup-RNSDK/packages/common/src/api/diagnostics.ts

111 行
3.1 KiB
TypeScript

import type { AxiosError, AxiosResponse } from 'axios'
import { isAxiosError } from 'axios'
import type { z } from 'zod'
import type { RequestError } from './errors'
type SafeIssue = {
code: string
message: string
path: string
}
export type SafeApiDiagnostic = {
code?: string
httpStatus?: number
issues?: SafeIssue[]
method?: string
path?: string
type: 'AxiosError' | 'Response' | 'ValidationError'
}
export type SafeApiErrorReport = {
diagnostic?: SafeApiDiagnostic
requestType: RequestError['type']
}
/**
* 网络诊断只能描述请求形态,不得复制请求/响应 body、headers 或完整 URL。
* 这些字段可能包含手机号、userId、sessionId、签名和其它业务数据。
*/
export function safeRequestPath(input?: string): string | undefined {
if (!input) return undefined
try {
return new URL(input, 'https://xuqm.invalid').pathname
} catch {
return input.split(/[?#]/, 1)[0]
}
}
function requestShape(response: AxiosResponse<unknown>) {
return {
httpStatus: response.status,
method: response.config.method?.toUpperCase(),
path: safeRequestPath(response.config.url),
}
}
export function createResponseDiagnostic(response: AxiosResponse<unknown>): SafeApiDiagnostic {
return { type: 'Response', ...requestShape(response) }
}
export function createValidationDiagnostic(
response: AxiosResponse<unknown>,
issues: z.ZodIssue[],
): SafeApiDiagnostic {
return {
type: 'ValidationError',
...requestShape(response),
issues: issues.map(issue => ({
code: issue.code,
message: issue.message,
path: issue.path.join('.'),
})),
}
}
export function createAxiosDiagnostic(error: AxiosError): SafeApiDiagnostic {
return {
type: 'AxiosError',
code: error.code,
httpStatus: error.response?.status,
method: error.config?.method?.toUpperCase(),
path: safeRequestPath(error.config?.url),
}
}
/**
* 非 axios 异常只允许记录构造名与截断消息。
* 不接触 cause、stack、config 或任何可能持有业务数据的对象。
*/
export function createUnexpectedDiagnostic(error: unknown): string {
if (error instanceof Error) {
return `${error.name}: ${error.message.slice(0, 200)}`
}
return typeof error
}
type ValidationCause = {
issues: z.ZodIssue[]
response: AxiosResponse<unknown>
}
function isValidationCause(value: unknown): value is ValidationCause {
if (!value || typeof value !== 'object') return false
const candidate = value as Partial<ValidationCause>
return Array.isArray(candidate.issues) && Boolean(candidate.response?.config)
}
/**
* 全局错误采集只接收不可逆的安全摘要,绝不暴露 RequestError.cause。
* cause 可能持有 Axios config、headers、请求体和完整响应,只能留在调用现场。
*/
export function createSafeApiErrorReport(error: RequestError): SafeApiErrorReport {
const cause = error.cause
const diagnostic = isValidationCause(cause)
? createValidationDiagnostic(cause.response, cause.issues)
: isAxiosError(cause)
? createAxiosDiagnostic(cause)
: undefined
return { diagnostic, requestType: error.type }
}