import { Runtime } from "#Source/environment/base.index.ts" import { normalizeBrowserExceptionRecord } from "./normalize.ts" import type { BrowserExceptionRecord, ExceptionListenerCleanup, ObserveExceptionsOptions, } from "./types.ts" /** * @description 表示浏览器侧异常监听回调。 */ export type BrowserExceptionListener = (record: BrowserExceptionRecord) => void /** * @description 监听浏览器侧通过 `error` 事件暴露的全局异常。 */ export const onBrowserGlobalError = ( listener: BrowserExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useBrowser( ({ window }) => { const internalListener = (event: ErrorEvent): void => { listener( normalizeBrowserExceptionRecord({ source: "browser.global-error", exception: event.error ?? event.message, message: event.message, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: event, filename: event.filename, lineno: event.lineno, colno: event.colno, }), ) } window.addEventListener("error", internalListener) return () => { window.removeEventListener("error", internalListener) } }, () => { return (): void => { return undefined } }, ) } type InternalBrowserOnErrorListener = ( message: string | Event, source?: string | undefined, lineno?: number | undefined, colno?: number | undefined, error?: Error | undefined, ) => boolean interface InternalBrowserOnErrorState { previousOnError: OnErrorEventHandler | null currentOnError: OnErrorEventHandler listeners: Set } const internalBrowserOnErrorStateMap = new WeakMap() const internalGetBrowserOnErrorState = (window: Window): InternalBrowserOnErrorState => { const existingState = internalBrowserOnErrorStateMap.get(window) if (existingState !== undefined) { return existingState } const internalListeners = new Set() const internalPreviousOnError = window.onerror const internalCurrentOnError: OnErrorEventHandler = ( message, source, lineno, colno, error, ): boolean => { let internalHandled = false for (const internalListener of internalListeners) { internalHandled = internalListener(message, source, lineno, colno, error) || internalHandled } if (typeof internalPreviousOnError === "function") { internalHandled = internalPreviousOnError(message, source, lineno, colno, error) === true || internalHandled } return internalHandled } const internalState: InternalBrowserOnErrorState = { previousOnError: internalPreviousOnError, currentOnError: internalCurrentOnError, listeners: internalListeners, } internalBrowserOnErrorStateMap.set(window, internalState) // oxlint-disable-next-line prefer-add-event-listener window.onerror = internalCurrentOnError return internalState } /** * @description 监听浏览器侧通过 `window.onerror` 暴露的全局异常。 */ export const onBrowserWindowOnError = ( listener: BrowserExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useBrowser( ({ window }) => { const internalOnErrorState = internalGetBrowserOnErrorState(window) const internalListener: InternalBrowserOnErrorListener = ( message, source, lineno, colno, error, ): boolean => { listener( normalizeBrowserExceptionRecord({ source: "browser.window-onerror", exception: error ?? message, message: typeof message === "string" ? message : undefined, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: { message, source, lineno, colno, error }, filename: source, lineno, colno, }), ) return false } internalOnErrorState.listeners.add(internalListener) return () => { internalOnErrorState.listeners.delete(internalListener) if (internalOnErrorState.listeners.size !== 0) { return } internalBrowserOnErrorStateMap.delete(window) if (window.onerror === internalOnErrorState.currentOnError) { // oxlint-disable-next-line prefer-add-event-listener window.onerror = internalOnErrorState.previousOnError } } }, () => { return (): void => { return undefined } }, ) } /** * @description 监听浏览器侧未处理 Promise 拒绝。 */ export const onBrowserUnhandledRejection = ( listener: BrowserExceptionListener, options?: ObserveExceptionsOptions | undefined, ): ExceptionListenerCleanup => { return Runtime.useBrowser( ({ window }) => { const internalListener = (event: PromiseRejectionEvent): void => { listener( normalizeBrowserExceptionRecord({ source: "browser.unhandled-rejection", exception: event.reason, timestamp: options?.captureTimestamp?.() ?? Date.now(), originalEvent: event, }), ) } window.addEventListener("unhandledrejection", internalListener) return () => { window.removeEventListener("unhandledrejection", internalListener) } }, () => { return (): void => { return undefined } }, ) } /** * @description 表示批量监听浏览器异常时可选的配置。 * * 未显式传入时,三个浏览器入口默认都会启用;仅当对应选项为 `false` 时才跳过该入口。 */ export interface ObserveBrowserExceptionsOptions extends ObserveExceptionsOptions { includeGlobalError?: boolean | undefined includeWindowOnError?: boolean | undefined includeUnhandledRejection?: boolean | undefined } /** * @description 批量监听浏览器侧全局异常入口。 * * 默认会同时监听 `error`、`window.onerror` 和 `unhandledrejection`。 * 如需关闭某个入口,请将对应的 `include*` 选项显式设为 `false`。 */ export const observeBrowserExceptions = ( listener: BrowserExceptionListener, options?: ObserveBrowserExceptionsOptions | undefined, ): ExceptionListenerCleanup => { const { includeGlobalError, includeWindowOnError, includeUnhandledRejection } = options ?? {} const cleanups: ExceptionListenerCleanup[] = [] if (includeGlobalError !== false) { cleanups.push(onBrowserGlobalError(listener, options)) } if (includeWindowOnError !== false) { cleanups.push(onBrowserWindowOnError(listener, options)) } if (includeUnhandledRejection !== false) { cleanups.push(onBrowserUnhandledRejection(listener, options)) } return () => { for (const cleanup of cleanups) { cleanup() } } }