2026-06-16 12:10:28 +08:00
|
|
|
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()
|
|
|
|
|
},
|
2026-06-16 13:10:16 +08:00
|
|
|
|
|
|
|
|
/** Clean up resources (timers, etc.). Call on app termination if needed. */
|
|
|
|
|
destroy(): void {
|
|
|
|
|
if (_queue) {
|
|
|
|
|
_queue.destroy()
|
|
|
|
|
_queue = null
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-16 12:10:28 +08:00
|
|
|
}
|