feat: 鸿蒙 BugCollect 崩溃收集模块新建

新增模块:bugcollect/
- BugCollect.ets - 主入口,错误捕获 API
- BugCollectTypes.ets - 数据模型
- Fingerprint.ets - 错误指纹计算(SHA-256)
- LogQueue.ets - 内存队列 + 定时上传

功能:
- captureError/captureCrash 捕获错误
- addBreadcrumb 添加面包屑
- event 记录自定义事件
- warn/info 日志级别记录
- 自动初始化(全局错误处理器)

Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-19 15:10:18 +08:00
父节点 5a913ab4f0
当前提交 65e7a3e2c3
共有 5 个文件被更改,包括 548 次插入0 次删除

查看文件

@ -6,6 +6,7 @@ export { SDKContext } from './src/main/ets/core/SDKContext'
export { HttpClient } from './src/main/ets/core/HttpClient'
export { XWebViewComponent, XWebViewBuilder } from './src/main/ets/xwebview/XWebView'
export { XWebViewBridge, openXWebView, getXWebViewControl, setXWebViewController, getXWebViewConfig } from './src/main/ets/xwebview/XWebViewBridge'
export { BugCollect } from './src/main/ets/bugcollect/BugCollect'
export type {
ConfigFile,
SDKConfig,

查看文件

@ -0,0 +1,261 @@
/**
* BugCollect 崩溃收集 SDK
*/
import deviceInfo from '@ohos.deviceInfo'
import bundleManager from '@ohos.bundle.bundleManager'
import { SDKContext } from '../core/SDKContext'
import { computeFingerprint } from './Fingerprint'
import { LogQueue } from './LogQueue'
import type { IssueEvent, LogEvent, Breadcrumb, DeviceInfo, SdkInfo, LogLevel } from './BugCollectTypes'
const SDK_NAME = 'bugcollect.harmony'
const SDK_VERSION = '1.0.0'
const MAX_BREADCRUMBS = 50
export class BugCollect {
private static logLevel: LogLevel = 2 // WARN
private static environment: string = 'production'
private static queue: LogQueue | null = null
private static breadcrumbs: Breadcrumb[] = []
private static pendingErrors: IssueEvent[] = []
private static initialized = false
/**
* 设置日志级别
*/
static setLogLevel(level: LogLevel): void {
BugCollect.logLevel = level
}
/**
* 设置环境
*/
static setEnvironment(env: string): void {
BugCollect.environment = env
}
/**
* 添加面包屑
*/
static addBreadcrumb(category: string, message: string, level: string = 'info', data?: Record<string, Object>): void {
if (BugCollect.breadcrumbs.length >= MAX_BREADCRUMBS) {
BugCollect.breadcrumbs.shift()
}
BugCollect.breadcrumbs.push({
timestamp: Date.now(),
category,
message,
level,
data,
})
}
/**
* 记录事件
*/
static event(name: string, properties?: Record<string, Object>): void {
if (!BugCollect.initialized) return
const config = SDKContext.getConfig()
const logEvent: LogEvent = {
type: 'event',
eventId: BugCollect.generateUuid(),
name,
properties,
appKey: config.appKey,
userId: SDKContext.getUserId(),
platform: 'harmony',
release: BugCollect.getAppVersion(),
environment: BugCollect.environment,
device: BugCollect.buildDeviceInfo(),
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
BugCollect.queue?.push(logEvent)
}
/**
* 捕获错误
*/
static captureError(error: Error, tags?: Record<string, Object>, fingerprint?: string): void {
const issueEvent = BugCollect.buildIssueEvent('error', error, tags, fingerprint)
if (!BugCollect.initialized) {
BugCollect.pendingErrors.push(issueEvent)
return
}
BugCollect.flushPendingErrors()
BugCollect.queue?.push(issueEvent)
}
/**
* 捕获致命错误
*/
static captureCrash(error: Error, tags?: Record<string, Object>): void {
if (!BugCollect.initialized) return
BugCollect.queue?.push(BugCollect.buildIssueEvent('fatal', error, tags))
}
/**
* 记录警告
*/
static warn(message: string, tags?: Record<string, Object>): void {
if (BugCollect.logLevel > 2) return
if (!BugCollect.initialized) return
const config = SDKContext.getConfig()
const issue: IssueEvent = {
type: 'issue',
eventId: BugCollect.generateUuid(),
level: 'warning',
platform: 'harmony',
fingerprint: '', // will be computed async
appKey: config.appKey,
exception: { type: 'Warning', value: message },
breadcrumbs: [...BugCollect.breadcrumbs],
release: BugCollect.getAppVersion(),
environment: BugCollect.environment,
userId: SDKContext.getUserId(),
user: { id: SDKContext.getUserId() ?? '' },
device: BugCollect.buildDeviceInfo(),
tags,
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
// Compute fingerprint async
computeFingerprint('Warning', message, '').then(fp => {
issue.fingerprint = fp
BugCollect.queue?.push(issue)
})
}
/**
* 记录信息
*/
static info(message: string, tags?: Record<string, Object>): void {
if (BugCollect.logLevel > 1) return
BugCollect.event('__log_info', { ...tags, message })
}
/**
* 启动
*/
static start(): void {
if (BugCollect.initialized) return
BugCollect.initialized = true
const config = SDKContext.getConfig()
BugCollect.queue = new LogQueue(config.appKey, config.apiUrl)
// 注册崩溃处理器
BugCollect.registerCrashHandler()
// 刷新待处理的错误
BugCollect.flushPendingErrors()
}
/**
* 注册崩溃处理器
*/
private static registerCrashHandler(): void {
// HarmonyOS 错误处理
try {
// 注册全局错误处理器
ErrorUtils?.setGlobalHandler?.((error: Error) => {
BugCollect.captureCrash(error)
})
} catch {
// ignore
}
}
/**
* 刷新待处理的错误
*/
private static flushPendingErrors(): void {
if (BugCollect.pendingErrors.length === 0) return
const errors = [...BugCollect.pendingErrors]
BugCollect.pendingErrors = []
for (const error of errors) {
BugCollect.queue?.push(error)
}
}
/**
* 构建 IssueEvent
*/
private static buildIssueEvent(level: IssueEvent['level'], error: Error, tags?: Record<string, Object>, fingerprint?: string): IssueEvent {
const config = SDKContext.getConfig()
const stacktrace = error.stack ?? ''
const event: IssueEvent = {
type: 'issue',
eventId: BugCollect.generateUuid(),
level,
platform: 'harmony',
fingerprint: fingerprint ?? '', // will be computed async
appKey: config.appKey,
exception: {
type: error.name ?? 'Error',
value: error.message ?? '',
stacktrace,
},
breadcrumbs: [...BugCollect.breadcrumbs],
release: BugCollect.getAppVersion(),
environment: BugCollect.environment,
userId: SDKContext.getUserId(),
user: { id: SDKContext.getUserId() ?? '' },
device: BugCollect.buildDeviceInfo(),
tags,
sdk: { name: SDK_NAME, version: SDK_VERSION },
}
// Compute fingerprint async
if (!fingerprint) {
computeFingerprint(error.name ?? 'Error', error.message ?? '', stacktrace).then(fp => {
event.fingerprint = fp
})
}
return event
}
/**
* 构建设备信息
*/
private static buildDeviceInfo(): DeviceInfo {
return {
model: deviceInfo.product ?? '',
manufacturer: deviceInfo.brand ?? '',
osName: 'HarmonyOS',
osVersion: deviceInfo.osReleaseType ?? '',
locale: deviceInfo.locale ?? '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}
}
/**
* 获取应用版本
*/
private static getAppVersion(): string {
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT)
return bundleInfo.versionName
} catch {
return '1.0.0'
}
}
/**
* 生成 UUID
*/
private static generateUuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.floor(Math.random() * 16)
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
}

查看文件

@ -0,0 +1,86 @@
/**
* BugCollect 类型定义
*/
/// Issue 事件
export interface IssueEvent {
type: 'issue'
eventId: string
level: 'fatal' | 'error' | 'warning' | 'info' | 'debug'
platform: string
fingerprint: string
appKey: string
exception?: ExceptionInfo
breadcrumbs?: Breadcrumb[]
release: string
environment: string
userId?: string
user?: UserInfo
device?: DeviceInfo
tags?: Record<string, Object>
sdk?: SdkInfo
}
/// 异常信息
export interface ExceptionInfo {
type: string
value: string
stacktrace?: string
}
/// 用户信息
export interface UserInfo {
id: string
}
/// 设备信息
export interface DeviceInfo {
model?: string
manufacturer?: string
osName?: string
osVersion?: string
locale?: string
timezone?: string
isEmulator?: boolean
freeMemoryMb?: number
buildType?: string
}
/// SDK 信息
export interface SdkInfo {
name: string
version: string
}
/// 面包屑
export interface Breadcrumb {
timestamp: number
category: string
message: string
level: string
data?: Record<string, Object>
}
/// 日志事件
export interface LogEvent {
type: 'event'
eventId: string
name: string
properties?: Record<string, Object>
appKey: string
userId?: string
platform: string
release: string
environment: string
device?: DeviceInfo
sdk?: SdkInfo
}
/// 日志级别
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
NONE = 4
}

查看文件

@ -0,0 +1,59 @@
/**
* 错误指纹计算
*/
import cryptoFramework from '@ohos.security.cryptoFramework'
/**
* 计算错误指纹
*/
export async function computeFingerprint(type: string, message: string, stacktrace?: string): Promise<string> {
const normalizedMessage = normalizeMessage(message)
const top3Frames = extractTop3Frames(stacktrace)
const raw = `${type}:${normalizedMessage}:${top3Frames}`
try {
const md = cryptoFramework.createMd('SHA256')
const input = new Uint8Array(Buffer.from(raw, 'utf-8').buffer)
md.update({ data: input })
const output = md.digest()
const hash = Buffer.from(output.data).toString('hex')
return hash
} catch {
// Fallback: simple hash
return simpleHash(raw)
}
}
/**
* 归一化消息
*/
function normalizeMessage(message: string): string {
return message
.replace(/\d+/g, 'N')
.replace(/[0-9a-fA-F]{8,}/g, 'H')
}
/**
* 提取堆栈顶部帧
*/
function extractTop3Frames(stacktrace?: string): string {
if (!stacktrace) return ''
return stacktrace
.split('\n')
.slice(0, 3)
.join('|')
}
/**
* 简单哈希fallback
*/
function simpleHash(str: string): string {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(16)
}

查看文件

@ -0,0 +1,141 @@
/**
* 日志队列
*/
import { HttpClient } from '../core/HttpClient'
import { SDKContext } from '../core/SDKContext'
import type { IssueEvent, LogEvent } from './BugCollectTypes'
const BATCH_SIZE = 30
const FLUSH_INTERVAL_MS = 10000
const MAX_QUEUE_SIZE = 500
const MAX_RETRY = 3
const STORAGE_KEY = 'xuqm_bugcollect_queue'
type Event = IssueEvent | LogEvent
export class LogQueue {
private memory: Event[] = []
private flushTimer: number = 0
private retryCount = 0
private appKey: string
private baseUrl: string
constructor(appKey: string, baseUrl: string) {
this.appKey = appKey
this.baseUrl = baseUrl
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS)
}
/**
* 推送事件
*/
async push(event: Event): Promise<void> {
if (this.shouldPersist(event)) {
const stored = await this.readStored()
if (stored.length >= MAX_QUEUE_SIZE) stored.shift()
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> {
const stored = await this.readStored()
const combined = [...stored, ...this.memory]
if (combined.length === 0) return
const batch = combined.slice(0, BATCH_SIZE)
const storedUsed = Math.min(stored.length, BATCH_SIZE)
const memUsed = batch.length - storedUsed
// 乐观移除
await this.writeStored(stored.slice(storedUsed))
this.memory.splice(0, memUsed)
const issues = batch.filter((e): e is IssueEvent => e.type === 'issue')
const events = batch.filter((e): e is LogEvent => e.type === 'event')
try {
if (issues.length > 0) {
await this.post('/bugcollect/v1/issues/batch', issues)
}
if (events.length > 0) {
await this.post('/bugcollect/v1/events/batch', events)
}
this.retryCount = 0
} catch {
this.retryCount++
if (this.retryCount < MAX_RETRY) {
const toRequeue = batch.filter(e => this.shouldPersist(e))
if (toRequeue.length > 0) {
const current = await this.readStored()
await this.writeStored([...toRequeue, ...current])
}
}
}
}
/**
* 销毁队列
*/
destroy(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer)
this.flushTimer = 0
}
}
/**
* 判断是否需要持久化
*/
private shouldPersist(event: Event): boolean {
return event.type === 'issue' && (event.level === 'fatal' || event.level === 'error')
}
/**
* 上传数据
*/
private async post(path: string, data: Event[]): Promise<void> {
const body = JSON.stringify({
sentAt: new Date().toISOString(),
sdk: { name: 'bugcollect.harmony', version: '1.0.0' },
events: data,
})
await HttpClient.post(path, body)
}
/**
* 读取持久化队列
*/
private async readStored(): Promise<Event[]> {
try {
const raw = await SDKContext.getPreference(STORAGE_KEY)
if (!raw) return []
return JSON.parse(raw) as Event[]
} catch {
return []
}
}
/**
* 写入持久化队列
*/
private async writeStored(queue: Event[]): Promise<void> {
try {
if (queue.length === 0) {
await SDKContext.setPreference(STORAGE_KEY, '')
} else {
await SDKContext.setPreference(STORAGE_KEY, JSON.stringify(queue))
}
} catch {
// ignore
}
}
}