XuqmGroup-RNSDK/packages/log/src/XLog.ts

134 行
4.8 KiB
TypeScript

import { Platform } 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, XLogEvent } from './types'
// ─── Internal state ───────────────────────────────────────────────────────────
let _logLevel: LogLevel = 'warn'
let _environment: Environment = 'production'
let _queue: LogQueue | null = null
let _errorCaptureStarted = false
// Stable session id for the lifetime of the JS runtime
const _sessionId: string = _generateSessionId()
function _generateSessionId(): string {
return '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' {
return Platform.OS === 'ios' ? 'ios' : 'android'
}
function _getAppVersion(): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 _enqueue(event: XLogEvent): void {
if (!_queue) {
const cfg = getConfig()
if (!cfg.logApiUrl || !cfg.logEnabled) return
_queue = new LogQueue({ logApiUrl: cfg.logApiUrl, appKey: cfg.appKey })
}
void _queue.push(event)
}
// ─── XLog public API ──────────────────────────────────────────────────────────
export const XLog = {
/** Set the minimum log level. Events below this level are discarded. */
setLogLevel(level: LogLevel): void {
_logLevel = level
},
/** Set an environment tag attached to every log event. */
setEnvironment(env: Environment): void {
_environment = env
},
/**
* Record a custom analytics event (funnel analysis, behaviour tracking).
* Also automatically advances any matching funnel step.
*/
event(name: string, properties?: Record<string, unknown>): void {
if (!isInitialized()) return
const event: LogEvent = {
type: 'event',
name,
properties: properties ? { ...properties, environment: _environment } : { environment: _environment },
timestamp: Date.now(),
userId: getUserId() ?? undefined,
sessionId: _sessionId,
appKey: getConfig().appKey,
platform: _getPlatform(),
appVersion: _getAppVersion(),
}
_enqueue(event)
FunnelTracker.track(name, properties)
},
/** Upload a JS exception to the log service. */
captureError(error: unknown, metadata?: Record<string, unknown>): void {
if (!isInitialized()) return
const err = error instanceof Error ? error : new Error(String(error))
const issue: IssueEvent = {
type: 'js_error',
message: err.message,
stack: err.stack,
fingerprint: computeFingerprint('js_error', err.message, err.stack),
timestamp: Date.now(),
userId: getUserId() ?? undefined,
sessionId: _sessionId,
appKey: getConfig().appKey,
platform: _getPlatform(),
appVersion: _getAppVersion(),
metadata: metadata ? { ...metadata, environment: _environment } : { environment: _environment },
}
_enqueue(issue)
},
/** Log a warning (if log level allows). */
warn(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('warn')) return
XLog.captureError(new Error(message), { ...metadata, level: 'warn' })
},
/** Log an informational event (if log level allows). */
info(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('info')) return
XLog.event('__log_info', { message, ...metadata })
},
/**
* 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(XLog.captureError.bind(XLog))
},
/** Define a funnel for step-by-step conversion tracking. */
defineFunnel: FunnelTracker.define.bind(FunnelTracker),
/** Get client-side funnel progress (server aggregates across sessions). */
getFunnelProgress: FunnelTracker.getProgress.bind(FunnelTracker),
/** Flush any pending events immediately (e.g. before app goes to background). */
async flush(): Promise<void> {
if (_queue) await _queue.flush()
},
}