XuqmGroup-RNSDK/packages/bugcollect/src/BugCollect.ts

228 行
7.5 KiB
TypeScript

import { Platform, Dimensions } from 'react-native'
import { getConfig, isInitialized, getUserId } from '@xuqm/rn-common'
import { LogQueue } from './queue/LogQueue'
import { ErrorCapture } from './capture/ErrorCapture'
import { computeFingerprint } from './fingerprint'
import { FunnelTracker } from './funnel/FunnelTracker'
import type {
LogLevel,
Environment,
IssueEvent,
LogEvent,
BugCollectEvent,
BreadcrumbItem,
DeviceInfo,
Level,
} from './types'
// ─── Internal state ───────────────────────────────────────────────────────────
const SDK_NAME = 'bugcollect.rn'
const SDK_VERSION = '0.2.0'
const MAX_BREADCRUMBS = 100
let _logLevel: LogLevel = 'warn'
let _environment: Environment = 'production'
let _queue: LogQueue | null = null
let _errorCaptureStarted = false
let _breadcrumbs: BreadcrumbItem[] = []
const _sessionId: string = _generateSessionId()
function _generateSessionId(): string {
return 'xxxx-xxxx-xxxx'.replace(/x/g, () => ((Math.random() * 16) | 0).toString(16))
}
function _levelNum(level: LogLevel): number {
return { debug: 0, info: 1, warn: 2, error: 3 }[level]
}
function _getPlatform(): 'ios' | 'android' | 'harmonyos' | 'web' | 'react-native' {
switch (Platform.OS) {
case 'ios': return 'ios'
case 'android': return 'android'
// @ts-expect-error HarmonyOS is not in the official RN types
case 'harmony': return 'harmonyos'
default: return 'react-native'
}
}
function _getAppVersion(): string {
const constants = Platform.constants as Record<string, unknown>
const v = constants['appVersion'] ?? constants['Version']
return typeof v === 'string' ? v : String(v ?? '0.0.0')
}
function _getDeviceInfo(): DeviceInfo {
const constants = Platform.constants as Record<string, unknown>
const { width, height } = Dimensions.get('screen')
return {
model: typeof constants['Model'] === 'string' ? constants['Model'] : undefined,
brand: typeof constants['Brand'] === 'string' ? constants['Brand'] : undefined,
manufacturer: typeof constants['Manufacturer'] === 'string' ? constants['Manufacturer'] : undefined,
osName: Platform.OS,
osVersion: typeof Platform.Version === 'string' ? Platform.Version : String(Platform.Version),
sdkVersion: Platform.OS === 'android' && typeof constants['Release'] === 'string'
? constants['Release']
: undefined,
screenWidth: width,
screenHeight: height,
}
}
function _enqueue(event: BugCollectEvent): void {
if (!_queue) {
const cfg = getConfig()
if (!cfg.bugCollectApiUrl || !cfg.bugCollectEnabled) return
_queue = new LogQueue({ bugCollectApiUrl: cfg.bugCollectApiUrl, appKey: cfg.appKey })
}
void _queue.push(event)
}
function _buildIssue(level: Level, error: unknown, metadata?: Record<string, unknown>): IssueEvent {
const err = error instanceof Error ? error : new Error(String(error))
return {
type: 'issue',
level,
platform: _getPlatform(),
fingerprint: computeFingerprint(level, err.message, err.stack),
timestamp: Date.now(),
appKey: getConfig().appKey,
release: _getAppVersion(),
environment: _environment,
exception: {
type: err.name ?? 'Error',
value: err.message,
stacktrace: err.stack,
},
breadcrumbs: _breadcrumbs.length > 0 ? [..._breadcrumbs] : undefined,
userId: getUserId() ?? undefined,
sessionId: _sessionId,
tags: metadata,
device: _getDeviceInfo(),
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
}
// ─── BugCollect public API ────────────────────────────────────────────────────
export const BugCollect = {
setLogLevel(level: LogLevel): void {
_logLevel = level
},
setEnvironment(env: Environment): void {
_environment = env
},
/** Add a breadcrumb to the in-memory circular buffer (max 100). */
addBreadcrumb(item: Omit<BreadcrumbItem, 'timestamp'> & { timestamp?: number }): void {
const crumb: BreadcrumbItem = { ...item, timestamp: item.timestamp ?? Date.now() }
if (_breadcrumbs.length >= MAX_BREADCRUMBS) _breadcrumbs.shift()
_breadcrumbs.push(crumb)
},
/** Record a custom analytics event (funnel analysis, behaviour tracking). */
event(name: string, properties?: Record<string, unknown>): void {
if (!isInitialized()) return
const event: LogEvent = {
type: 'event',
name,
properties,
timestamp: Date.now(),
userId: getUserId() ?? undefined,
sessionId: _sessionId,
appKey: getConfig().appKey,
platform: _getPlatform(),
release: _getAppVersion(),
environment: _environment,
device: _getDeviceInfo(),
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
_enqueue(event)
FunnelTracker.track(name, properties)
},
/** Upload a JS exception as an error-level issue. */
captureError(error: unknown, metadata?: Record<string, unknown>): void {
if (!isInitialized()) return
_enqueue(_buildIssue('error', error, metadata))
},
/** Upload a JS exception as a fatal-level issue (persisted across restarts). */
captureFatal(error: unknown, metadata?: Record<string, unknown>): void {
if (!isInitialized()) return
_enqueue(_buildIssue('fatal', error, metadata))
},
/** Log a warning (if log level allows). */
warn(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('warn')) return
if (!isInitialized()) return
const issue: IssueEvent = {
type: 'issue',
level: 'warning',
platform: _getPlatform(),
fingerprint: computeFingerprint('warning', message),
timestamp: Date.now(),
appKey: getConfig().appKey,
release: _getAppVersion(),
environment: _environment,
exception: { type: 'Warning', value: message },
breadcrumbs: _breadcrumbs.length > 0 ? [..._breadcrumbs] : undefined,
userId: getUserId() ?? undefined,
sessionId: _sessionId,
tags: metadata,
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
_enqueue(issue)
},
/** Log an informational event (if log level allows). */
info(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('info')) return
if (!isInitialized()) return
const issue: IssueEvent = {
type: 'issue',
level: 'info',
platform: _getPlatform(),
fingerprint: computeFingerprint('info', message),
timestamp: Date.now(),
appKey: getConfig().appKey,
release: _getAppVersion(),
environment: _environment,
exception: { type: 'Info', value: message },
userId: getUserId() ?? undefined,
sessionId: _sessionId,
tags: metadata,
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
_enqueue(issue)
},
/**
* Enable automatic capture of JS global errors and unhandled Promise rejections.
* Call once at app startup after XuqmSDK.initialize().
*/
startCapture(): void {
if (_errorCaptureStarted) return
_errorCaptureStarted = true
ErrorCapture.start(BugCollect.captureError.bind(BugCollect))
},
defineFunnel: FunnelTracker.define.bind(FunnelTracker),
getFunnelProgress: FunnelTracker.getProgress.bind(FunnelTracker),
async flush(): Promise<void> {
if (_queue) await _queue.flush()
},
destroy(): void {
if (_queue) {
_queue.destroy()
_queue = null
}
_breadcrumbs = []
},
}