fix(bugcollect-rn): 修复 BugCollect-API-v1-Review RN SDK 问题
- types.ts: 新增 BreadcrumbItem 类型;DeviceInfo 补全 brand/sdkVersion/screen/locale/timezone/isEmulator;UserInfo 补全 username/email;IssueEvent 新增 breadcrumbs 字段 - BugCollect.ts: 新增 addBreadcrumb API;新增 captureFatal API;_getPlatform() 支持 harmonyos;_getDeviceInfo() 采集完整设备信息;版本号改为常量;sessionId 加长 - LogQueue.ts: 按 level 分级持久化——fatal/error 写 AsyncStorage(进程崩溃后仍可重传),其余仅内存;重试时只重新入队 fatal/error 事件 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
76099d897e
当前提交
d6c42ecf56
@ -1,40 +1,75 @@
|
|||||||
import { Platform } from 'react-native'
|
import { Platform, Dimensions } from 'react-native'
|
||||||
import { getConfig, isInitialized, getUserId } from '@xuqm/rn-common'
|
import { getConfig, isInitialized, getUserId } from '@xuqm/rn-common'
|
||||||
import { LogQueue } from './queue/LogQueue'
|
import { LogQueue } from './queue/LogQueue'
|
||||||
import { ErrorCapture } from './capture/ErrorCapture'
|
import { ErrorCapture } from './capture/ErrorCapture'
|
||||||
import { computeFingerprint } from './fingerprint'
|
import { computeFingerprint } from './fingerprint'
|
||||||
import { FunnelTracker } from './funnel/FunnelTracker'
|
import { FunnelTracker } from './funnel/FunnelTracker'
|
||||||
import type { LogLevel, Environment, IssueEvent, LogEvent, BugCollectEvent } from './types'
|
import type {
|
||||||
|
LogLevel,
|
||||||
|
Environment,
|
||||||
|
IssueEvent,
|
||||||
|
LogEvent,
|
||||||
|
BugCollectEvent,
|
||||||
|
BreadcrumbItem,
|
||||||
|
DeviceInfo,
|
||||||
|
Level,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
// ─── Internal state ───────────────────────────────────────────────────────────
|
// ─── Internal state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SDK_NAME = 'bugcollect.rn'
|
||||||
|
const SDK_VERSION = '0.2.0'
|
||||||
|
const MAX_BREADCRUMBS = 100
|
||||||
|
|
||||||
let _logLevel: LogLevel = 'warn'
|
let _logLevel: LogLevel = 'warn'
|
||||||
let _environment: Environment = 'production'
|
let _environment: Environment = 'production'
|
||||||
let _queue: LogQueue | null = null
|
let _queue: LogQueue | null = null
|
||||||
let _errorCaptureStarted = false
|
let _errorCaptureStarted = false
|
||||||
|
let _breadcrumbs: BreadcrumbItem[] = []
|
||||||
|
|
||||||
// Stable session id for the lifetime of the JS runtime
|
|
||||||
const _sessionId: string = _generateSessionId()
|
const _sessionId: string = _generateSessionId()
|
||||||
|
|
||||||
function _generateSessionId(): string {
|
function _generateSessionId(): string {
|
||||||
return 'xxxx-xxxx'.replace(/x/g, () => ((Math.random() * 16) | 0).toString(16))
|
return 'xxxx-xxxx-xxxx'.replace(/x/g, () => ((Math.random() * 16) | 0).toString(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
function _levelNum(level: LogLevel): number {
|
function _levelNum(level: LogLevel): number {
|
||||||
return { debug: 0, info: 1, warn: 2, error: 3 }[level]
|
return { debug: 0, info: 1, warn: 2, error: 3 }[level]
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getPlatform(): 'ios' | 'android' {
|
function _getPlatform(): 'ios' | 'android' | 'harmonyos' | 'web' | 'react-native' {
|
||||||
return Platform.OS === 'ios' ? 'ios' : 'android'
|
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 {
|
function _getAppVersion(): string {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const constants = Platform.constants as Record<string, unknown>
|
const constants = Platform.constants as Record<string, unknown>
|
||||||
const v = constants['appVersion'] ?? constants['Version']
|
const v = constants['appVersion'] ?? constants['Version']
|
||||||
return typeof v === 'string' ? v : String(v ?? '0.0.0')
|
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 {
|
function _enqueue(event: BugCollectEvent): void {
|
||||||
if (!_queue) {
|
if (!_queue) {
|
||||||
const cfg = getConfig()
|
const cfg = getConfig()
|
||||||
@ -44,23 +79,50 @@ function _enqueue(event: BugCollectEvent): void {
|
|||||||
void _queue.push(event)
|
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 ────────────────────────────────────────────────────
|
// ─── BugCollect public API ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const BugCollect = {
|
export const BugCollect = {
|
||||||
/** Set the minimum log level. Events below this level are discarded. */
|
|
||||||
setLogLevel(level: LogLevel): void {
|
setLogLevel(level: LogLevel): void {
|
||||||
_logLevel = level
|
_logLevel = level
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Set an environment tag attached to every log event. */
|
|
||||||
setEnvironment(env: Environment): void {
|
setEnvironment(env: Environment): void {
|
||||||
_environment = env
|
_environment = env
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/** Add a breadcrumb to the in-memory circular buffer (max 100). */
|
||||||
* Record a custom analytics event (funnel analysis, behaviour tracking).
|
addBreadcrumb(item: Omit<BreadcrumbItem, 'timestamp'> & { timestamp?: number }): void {
|
||||||
* Also automatically advances any matching funnel step.
|
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 {
|
event(name: string, properties?: Record<string, unknown>): void {
|
||||||
if (!isInitialized()) return
|
if (!isInitialized()) return
|
||||||
const event: LogEvent = {
|
const event: LogEvent = {
|
||||||
@ -74,36 +136,23 @@ export const BugCollect = {
|
|||||||
platform: _getPlatform(),
|
platform: _getPlatform(),
|
||||||
release: _getAppVersion(),
|
release: _getAppVersion(),
|
||||||
environment: _environment,
|
environment: _environment,
|
||||||
sdk: { name: 'bugcollect.rn', version: '0.2.0' },
|
device: _getDeviceInfo(),
|
||||||
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
||||||
}
|
}
|
||||||
_enqueue(event)
|
_enqueue(event)
|
||||||
FunnelTracker.track(name, properties)
|
FunnelTracker.track(name, properties)
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Upload a JS exception to the log service. */
|
/** Upload a JS exception as an error-level issue. */
|
||||||
captureError(error: unknown, metadata?: Record<string, unknown>): void {
|
captureError(error: unknown, metadata?: Record<string, unknown>): void {
|
||||||
if (!isInitialized()) return
|
if (!isInitialized()) return
|
||||||
const err = error instanceof Error ? error : new Error(String(error))
|
_enqueue(_buildIssue('error', error, metadata))
|
||||||
const issue: IssueEvent = {
|
|
||||||
type: 'issue',
|
|
||||||
level: 'error',
|
|
||||||
platform: _getPlatform(),
|
|
||||||
fingerprint: computeFingerprint('error', 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,
|
|
||||||
},
|
},
|
||||||
userId: getUserId() ?? undefined,
|
|
||||||
sessionId: _sessionId,
|
/** Upload a JS exception as a fatal-level issue (persisted across restarts). */
|
||||||
tags: metadata,
|
captureFatal(error: unknown, metadata?: Record<string, unknown>): void {
|
||||||
sdk: { name: 'bugcollect.rn', version: '0.2.0' },
|
if (!isInitialized()) return
|
||||||
}
|
_enqueue(_buildIssue('fatal', error, metadata))
|
||||||
_enqueue(issue)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Log a warning (if log level allows). */
|
/** Log a warning (if log level allows). */
|
||||||
@ -119,14 +168,12 @@ export const BugCollect = {
|
|||||||
appKey: getConfig().appKey,
|
appKey: getConfig().appKey,
|
||||||
release: _getAppVersion(),
|
release: _getAppVersion(),
|
||||||
environment: _environment,
|
environment: _environment,
|
||||||
exception: {
|
exception: { type: 'Warning', value: message },
|
||||||
type: 'Warning',
|
breadcrumbs: _breadcrumbs.length > 0 ? [..._breadcrumbs] : undefined,
|
||||||
value: message,
|
|
||||||
},
|
|
||||||
userId: getUserId() ?? undefined,
|
userId: getUserId() ?? undefined,
|
||||||
sessionId: _sessionId,
|
sessionId: _sessionId,
|
||||||
tags: metadata,
|
tags: metadata,
|
||||||
sdk: { name: 'bugcollect.rn', version: '0.2.0' },
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
||||||
}
|
}
|
||||||
_enqueue(issue)
|
_enqueue(issue)
|
||||||
},
|
},
|
||||||
@ -144,14 +191,11 @@ export const BugCollect = {
|
|||||||
appKey: getConfig().appKey,
|
appKey: getConfig().appKey,
|
||||||
release: _getAppVersion(),
|
release: _getAppVersion(),
|
||||||
environment: _environment,
|
environment: _environment,
|
||||||
exception: {
|
exception: { type: 'Info', value: message },
|
||||||
type: 'Info',
|
|
||||||
value: message,
|
|
||||||
},
|
|
||||||
userId: getUserId() ?? undefined,
|
userId: getUserId() ?? undefined,
|
||||||
sessionId: _sessionId,
|
sessionId: _sessionId,
|
||||||
tags: metadata,
|
tags: metadata,
|
||||||
sdk: { name: 'bugcollect.rn', version: '0.2.0' },
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
||||||
}
|
}
|
||||||
_enqueue(issue)
|
_enqueue(issue)
|
||||||
},
|
},
|
||||||
@ -166,22 +210,18 @@ export const BugCollect = {
|
|||||||
ErrorCapture.start(BugCollect.captureError.bind(BugCollect))
|
ErrorCapture.start(BugCollect.captureError.bind(BugCollect))
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Define a funnel for step-by-step conversion tracking. */
|
|
||||||
defineFunnel: FunnelTracker.define.bind(FunnelTracker),
|
defineFunnel: FunnelTracker.define.bind(FunnelTracker),
|
||||||
|
|
||||||
/** Get client-side funnel progress (server aggregates across sessions). */
|
|
||||||
getFunnelProgress: FunnelTracker.getProgress.bind(FunnelTracker),
|
getFunnelProgress: FunnelTracker.getProgress.bind(FunnelTracker),
|
||||||
|
|
||||||
/** Flush any pending events immediately (e.g. before app goes to background). */
|
|
||||||
async flush(): Promise<void> {
|
async flush(): Promise<void> {
|
||||||
if (_queue) await _queue.flush()
|
if (_queue) await _queue.flush()
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Clean up resources (timers, etc.). Call on app termination if needed. */
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
if (_queue) {
|
if (_queue) {
|
||||||
_queue.destroy()
|
_queue.destroy()
|
||||||
_queue = null
|
_queue = null
|
||||||
}
|
}
|
||||||
|
_breadcrumbs = []
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
import type { BugCollectEvent } from '../types'
|
import type { BugCollectEvent, IssueEvent } from '../types'
|
||||||
|
|
||||||
const QUEUE_KEY = '@xuqm_bugcollect:queue'
|
const STORED_QUEUE_KEY = '@xuqm_bugcollect:queue'
|
||||||
const BATCH_SIZE = 30
|
const BATCH_SIZE = 30
|
||||||
const FLUSH_INTERVAL_MS = 10_000 // 10 seconds
|
const FLUSH_INTERVAL_MS = 10_000 // 10 seconds
|
||||||
const MAX_QUEUE_SIZE = 500
|
const MAX_QUEUE_SIZE = 500
|
||||||
const MAX_RETRY = 3
|
const MAX_RETRY = 3
|
||||||
|
|
||||||
|
/** Only fatal and error issues are persisted to AsyncStorage (survive restarts). */
|
||||||
|
function shouldPersist(event: BugCollectEvent): boolean {
|
||||||
|
if (event.type !== 'issue') return false
|
||||||
|
const level = (event as IssueEvent).level
|
||||||
|
return level === 'fatal' || level === 'error'
|
||||||
|
}
|
||||||
|
|
||||||
export class LogQueue {
|
export class LogQueue {
|
||||||
|
private _memory: BugCollectEvent[] = []
|
||||||
private flushTimer: ReturnType<typeof setInterval> | null = null
|
private flushTimer: ReturnType<typeof setInterval> | null = null
|
||||||
private retryCount = 0
|
private retryCount = 0
|
||||||
|
|
||||||
@ -18,18 +26,29 @@ export class LogQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async push(event: BugCollectEvent): Promise<void> {
|
async push(event: BugCollectEvent): Promise<void> {
|
||||||
const queue = await this._read()
|
if (shouldPersist(event)) {
|
||||||
if (queue.length >= MAX_QUEUE_SIZE) queue.shift() // drop oldest
|
const stored = await this._readStored()
|
||||||
queue.push(event)
|
if (stored.length >= MAX_QUEUE_SIZE) stored.shift()
|
||||||
await this._write(queue)
|
stored.push(event)
|
||||||
|
await this._writeStored(stored)
|
||||||
|
} else {
|
||||||
|
if (this._memory.length >= MAX_QUEUE_SIZE) this._memory.shift()
|
||||||
|
this._memory.push(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async flush(): Promise<void> {
|
async flush(): Promise<void> {
|
||||||
const queue = await this._read()
|
const stored = await this._readStored()
|
||||||
if (queue.length === 0) return
|
const combined = [...stored, ...this._memory]
|
||||||
|
if (combined.length === 0) return
|
||||||
|
|
||||||
const batch = queue.splice(0, BATCH_SIZE)
|
const batch = combined.slice(0, BATCH_SIZE)
|
||||||
await this._write(queue) // remove from queue first to prevent duplicate sends
|
const storedUsed = Math.min(stored.length, BATCH_SIZE)
|
||||||
|
const memUsed = batch.length - storedUsed
|
||||||
|
|
||||||
|
// Optimistically remove from sources before sending to avoid duplicates
|
||||||
|
await this._writeStored(stored.slice(storedUsed))
|
||||||
|
this._memory = this._memory.slice(memUsed)
|
||||||
|
|
||||||
const issues = batch.filter(e => e.type !== 'event')
|
const issues = batch.filter(e => e.type !== 'event')
|
||||||
const events = batch.filter(e => e.type === 'event')
|
const events = batch.filter(e => e.type === 'event')
|
||||||
@ -41,10 +60,13 @@ export class LogQueue {
|
|||||||
} catch {
|
} catch {
|
||||||
this.retryCount++
|
this.retryCount++
|
||||||
if (this.retryCount < MAX_RETRY) {
|
if (this.retryCount < MAX_RETRY) {
|
||||||
const current = await this._read()
|
// Re-queue only the persistent subset (re-queuing memory events would inflate duplicates)
|
||||||
await this._write([...batch, ...current])
|
const toRequeue = batch.filter(shouldPersist)
|
||||||
|
if (toRequeue.length > 0) {
|
||||||
|
const current = await this._readStored()
|
||||||
|
await this._writeStored([...toRequeue, ...current])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 超过重试次数,丢弃该批次
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +78,11 @@ export class LogQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _post(path: string, data: BugCollectEvent[]): Promise<void> {
|
private async _post(path: string, data: BugCollectEvent[]): Promise<void> {
|
||||||
const body = JSON.stringify({ sentAt: new Date().toISOString(), sdk: { name: 'bugcollect.rn', version: '0.2.0' }, events: data })
|
const body = JSON.stringify({
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
sdk: { name: 'bugcollect.rn', version: '0.2.0' },
|
||||||
|
events: data,
|
||||||
|
})
|
||||||
const res = await fetch(`${this.cfg.bugCollectApiUrl}${path}`, {
|
const res = await fetch(`${this.cfg.bugCollectApiUrl}${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -70,12 +96,16 @@ export class LogQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _read(): Promise<BugCollectEvent[]> {
|
private async _readStored(): Promise<BugCollectEvent[]> {
|
||||||
const raw = await AsyncStorage.getItem(QUEUE_KEY)
|
const raw = await AsyncStorage.getItem(STORED_QUEUE_KEY)
|
||||||
return raw ? (JSON.parse(raw) as BugCollectEvent[]) : []
|
return raw ? (JSON.parse(raw) as BugCollectEvent[]) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _write(queue: BugCollectEvent[]): Promise<void> {
|
private async _writeStored(queue: BugCollectEvent[]): Promise<void> {
|
||||||
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue))
|
if (queue.length === 0) {
|
||||||
|
await AsyncStorage.removeItem(STORED_QUEUE_KEY)
|
||||||
|
} else {
|
||||||
|
await AsyncStorage.setItem(STORED_QUEUE_KEY, JSON.stringify(queue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,17 @@ export type Environment = 'development' | 'staging' | 'production'
|
|||||||
export type Level = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
|
export type Level = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
|
||||||
export type Platform = 'ios' | 'android' | 'harmonyos' | 'web' | 'react-native'
|
export type Platform = 'ios' | 'android' | 'harmonyos' | 'web' | 'react-native'
|
||||||
|
|
||||||
|
export type BreadcrumbType = 'navigation' | 'http' | 'user' | 'console' | 'error' | 'default'
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
type: BreadcrumbType
|
||||||
|
category?: string
|
||||||
|
message?: string
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
level?: Level
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface SdkInfo {
|
export interface SdkInfo {
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
@ -16,14 +27,23 @@ export interface ExceptionInfo {
|
|||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id?: string
|
id?: string
|
||||||
|
username?: string
|
||||||
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceInfo {
|
export interface DeviceInfo {
|
||||||
name?: string
|
name?: string
|
||||||
model?: string
|
model?: string
|
||||||
manufacturer?: string
|
manufacturer?: string
|
||||||
|
brand?: string
|
||||||
osName?: string
|
osName?: string
|
||||||
osVersion?: string
|
osVersion?: string
|
||||||
|
sdkVersion?: string
|
||||||
|
screenWidth?: number
|
||||||
|
screenHeight?: number
|
||||||
|
locale?: string
|
||||||
|
timezone?: string
|
||||||
|
isEmulator?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogEvent {
|
export interface LogEvent {
|
||||||
@ -52,6 +72,7 @@ export interface IssueEvent {
|
|||||||
release: string
|
release: string
|
||||||
environment?: Environment
|
environment?: Environment
|
||||||
exception?: ExceptionInfo
|
exception?: ExceptionInfo
|
||||||
|
breadcrumbs?: BreadcrumbItem[]
|
||||||
userId?: string
|
userId?: string
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
user?: UserInfo
|
user?: UserInfo
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户