@arms/rum-reactnative 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +47 -0
  2. package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +13 -0
  3. package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +9 -0
  4. package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +64 -1
  5. package/harmony/rum_sdk/BuildProfile.ets +17 -0
  6. package/harmony/rum_sdk/Index.ets +2 -0
  7. package/harmony/rum_sdk/build-profile.json5 +22 -0
  8. package/harmony/rum_sdk/hvigorfile.ts +1 -0
  9. package/harmony/rum_sdk/oh-package.json5 +10 -0
  10. package/harmony/rum_sdk/src/main/ets/RumSdkPackage.ets +15 -0
  11. package/harmony/rum_sdk/src/main/ets/RumSdkTurboModule.ts +274 -0
  12. package/harmony/rum_sdk/src/main/ets/crash/CrashManager.ts +344 -0
  13. package/harmony/rum_sdk/src/main/ets/crash/FaultLogParser.ts +331 -0
  14. package/harmony/rum_sdk/src/main/ets/generated/components/ts.ts +5 -0
  15. package/harmony/rum_sdk/src/main/ets/generated/index.ets +5 -0
  16. package/harmony/rum_sdk/src/main/ets/generated/ts.ts +6 -0
  17. package/harmony/rum_sdk/src/main/ets/generated/turboModules/RumSdkReactnative.ts +46 -0
  18. package/harmony/rum_sdk/src/main/ets/generated/turboModules/ts.ts +5 -0
  19. package/harmony/rum_sdk/src/main/module.json5 +9 -0
  20. package/harmony/rum_sdk/ts.ts +1 -0
  21. package/lib/commonjs/NativeRumSdkReactnative.js +1 -1
  22. package/lib/commonjs/utils/crash.js +1 -1
  23. package/lib/commonjs/utils/platform.js +1 -1
  24. package/lib/module/NativeRumSdkReactnative.js +1 -1
  25. package/lib/module/utils/crash.js +1 -1
  26. package/lib/module/utils/platform.js +1 -1
  27. package/lib/typescript/commonjs/src/types/crash.d.ts +2 -0
  28. package/lib/typescript/commonjs/src/utils/platform.d.ts +7 -0
  29. package/lib/typescript/module/src/types/crash.d.ts +2 -0
  30. package/lib/typescript/module/src/utils/platform.d.ts +7 -0
  31. package/package.json +22 -3
  32. package/src/NativeRumSdkReactnative.ts +18 -6
@@ -0,0 +1,344 @@
1
+ /**
2
+ * 崩溃文件管理器(鸿蒙端)。
3
+ *
4
+ * 与 Android 端语义对齐:崩溃报告以 JSON 文件形式持久化在应用沙箱
5
+ * {filesDir}/rum_crashes/ 目录,文件命名 crash_{timestamp}_{crash_id}.json;
6
+ * JS 层上报成功 ack 后按 crash_id 删除文件。
7
+ *
8
+ * 与 Android 的差异:鸿蒙应用无法在崩溃时同步落盘(ArkTS 无信号回调),
9
+ * 崩溃现场由系统 faultLogger 服务负责持久化;本模块在下次启动时调用
10
+ * faultLogger.query 拉取本应用 faultlog,解析转换为 Android 对齐结构的
11
+ * JSON 后落盘,再走既有的 getPendingCrashes / ack 链路。
12
+ *
13
+ * 去重策略:已落盘的 fault 指纹(type_timestamp_pid)记录在 preferences,
14
+ * 避免 ack 删除崩溃文件后 faultLogger 重复查询导致重复上报。
15
+ */
16
+ import fs from '@ohos.file.fs'
17
+ import faultLogger from '@ohos.faultLogger'
18
+ import dataPreferences from '@ohos.data.preferences'
19
+ import deviceInfo from '@ohos.deviceInfo'
20
+ import bundleManager from '@ohos.bundle.bundleManager'
21
+ import emitter from '@ohos.events.emitter'
22
+ import type common from '@ohos.app.ability.common'
23
+ import {
24
+ buildCrashJson,
25
+ makeCrashFileName,
26
+ parseFullLog,
27
+ type FaultInfo,
28
+ } from './FaultLogParser'
29
+
30
+ const CRASH_DIR_NAME = 'rum_crashes'
31
+ const PREFS_NAME = 'com.aliyun.rum.reactnative'
32
+ const CONTEXT_KEY = 'rum_crash_context'
33
+ const PROCESSED_KEY = 'rum_processed_faults'
34
+ /** 已处理 fault 指纹上限,防止 preferences 无限增长 */
35
+ const PROCESSED_MAX = 50
36
+ /** fault 过期时间:超过 7 天的 fault 不再落盘(与 JS 层 cleanupOldCrashes 对齐) */
37
+ const FAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
38
+
39
+ export class CrashManager {
40
+ private context: common.UIAbilityContext
41
+ private crashDir: string
42
+ private prefs: dataPreferences.Preferences
43
+
44
+ constructor(context: common.UIAbilityContext) {
45
+ this.context = context
46
+ this.crashDir = `${context.filesDir}/${CRASH_DIR_NAME}`
47
+ this.prefs = dataPreferences.getPreferencesSync(context, { name: PREFS_NAME })
48
+ this.ensureCrashDir()
49
+ }
50
+
51
+ private ensureCrashDir(): void {
52
+ try {
53
+ if (!fs.accessSync(this.crashDir)) {
54
+ fs.mkdirSync(this.crashDir)
55
+ }
56
+ } catch (_) {
57
+ // 目录创建失败时后续文件操作会静默降级
58
+ }
59
+ }
60
+
61
+ // region fault 同步(faultLogger → 崩溃文件)
62
+
63
+ /**
64
+ * 拉取 faultLogger 中未处理的崩溃类 fault 并落盘,随后读取全部待上报崩溃文件。
65
+ * 仅处理 CPP_CRASH(2) / JS_CRASH(3);APP_FREEZE 属卡顿范畴,不在崩溃采集范围。
66
+ */
67
+ async getPendingCrashes(): Promise<string> {
68
+ try {
69
+ const infos = (await faultLogger.query(
70
+ faultLogger.FaultType.NO_SPECIFIC
71
+ )) as Array<FaultInfo>
72
+ if (infos && infos.length > 0) {
73
+ const processed = this.loadProcessedFingerprints()
74
+ const now = Date.now()
75
+ for (const info of infos) {
76
+ if (info.type !== faultLogger.FaultType.CPP_CRASH &&
77
+ info.type !== faultLogger.FaultType.JS_CRASH) {
78
+ continue
79
+ }
80
+ const fingerprint = `${info.type}_${info.timestamp}_${info.pid}`
81
+ if (processed.has(fingerprint)) {
82
+ continue
83
+ }
84
+ // faultLogger.timestamp 为秒级
85
+ if (info.timestamp * 1000 >= now - FAULT_MAX_AGE_MS) {
86
+ this.persistFault(info)
87
+ }
88
+ processed.add(fingerprint)
89
+ }
90
+ this.saveProcessedFingerprints(processed)
91
+ }
92
+ } catch (_) {
93
+ // faultLogger 服务不可用时仅读取已落盘的崩溃文件
94
+ }
95
+ const names = this.listCrashFiles()
96
+ const arr: string[] = []
97
+ for (const name of names) {
98
+ try {
99
+ const text = fs.readTextSync(`${this.crashDir}/${name}`)
100
+ if (text) {
101
+ JSON.parse(text)
102
+ arr.push(text)
103
+ }
104
+ } catch (_) {
105
+ // 损坏文件跳过,由 cleanupOldCrashes 兜底清理
106
+ }
107
+ }
108
+ return `[${arr.join(',')}]`
109
+ }
110
+
111
+ /** 单条 fault 解析落盘 */
112
+ private persistFault(info: FaultInfo): void {
113
+ try {
114
+ const parsed = parseFullLog(info.fullLog || '')
115
+ const json = buildCrashJson({
116
+ info,
117
+ parsed,
118
+ contextJson: this.readContextRaw(),
119
+ deviceJson: JSON.stringify(buildDevice()),
120
+ appJson: JSON.stringify(buildApp(this.context)),
121
+ })
122
+ this.writeFile(`${this.crashDir}/${makeCrashFileName(info)}`, json)
123
+ } catch (_) {
124
+ // 单条 fault 处理失败不影响其余数据
125
+ }
126
+ }
127
+
128
+ // endregion
129
+
130
+ // region 崩溃文件删除(对齐 Android 端语义)
131
+
132
+ /** 按 crash_id 删除崩溃文件(文件名后缀匹配) */
133
+ clearCrashReports(crashIds: string): void {
134
+ const ids = new Set<string>()
135
+ try {
136
+ const parsed = JSON.parse(crashIds) as string[]
137
+ for (const id of parsed) {
138
+ if (id) {
139
+ ids.add(id)
140
+ }
141
+ }
142
+ } catch (_) {
143
+ return
144
+ }
145
+ if (ids.size === 0) {
146
+ return
147
+ }
148
+ for (const name of this.listCrashFiles()) {
149
+ for (const id of ids) {
150
+ if (name.endsWith(`_${id}.json`) || name === `${id}.json`) {
151
+ this.deleteFile(`${this.crashDir}/${name}`)
152
+ break
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ clearAllCrashReports(): void {
159
+ for (const name of this.listCrashFiles()) {
160
+ this.deleteFile(`${this.crashDir}/${name}`)
161
+ }
162
+ }
163
+
164
+ /** 清理过期与超限的崩溃文件 */
165
+ cleanupOldCrashes(maxFiles: number, maxAgeMs: number): void {
166
+ const limit = maxFiles > 0 ? maxFiles : 1
167
+ const names = this.listCrashFiles()
168
+ if (names.length === 0) {
169
+ return
170
+ }
171
+ const now = Date.now()
172
+ const entries: Array<{ name: string; ts: number }> = []
173
+ for (const name of names) {
174
+ const ts = parseTimestampFromName(name)
175
+ const expired = ts > 0 && now - ts > maxAgeMs
176
+ if (expired) {
177
+ this.deleteFile(`${this.crashDir}/${name}`)
178
+ } else {
179
+ entries.push({ name, ts })
180
+ }
181
+ }
182
+ if (entries.length > limit) {
183
+ entries.sort((a, b) => a.ts - b.ts)
184
+ const overflow = entries.slice(0, entries.length - limit)
185
+ for (const entry of overflow) {
186
+ this.deleteFile(`${this.crashDir}/${entry.name}`)
187
+ }
188
+ }
189
+ }
190
+
191
+ /** 崩溃目录中是否存在崩溃文件 */
192
+ hasCrashFiles(): boolean {
193
+ return this.listCrashFiles().length > 0
194
+ }
195
+
196
+ // endregion
197
+
198
+ // region 崩溃上下文(session / view 快照,JS 侧变更时同步)
199
+
200
+ updateContext(contextJson: string): void {
201
+ try {
202
+ this.prefs.putSync(CONTEXT_KEY, contextJson)
203
+ this.prefs.flush()
204
+ } catch (_) {
205
+ // ignore
206
+ }
207
+ }
208
+
209
+ readContextRaw(): string {
210
+ try {
211
+ if (this.prefs.hasSync(CONTEXT_KEY)) {
212
+ return this.prefs.getSync(CONTEXT_KEY, '') as string
213
+ }
214
+ } catch (_) {
215
+ // ignore
216
+ }
217
+ return ''
218
+ }
219
+
220
+ // endregion
221
+
222
+ // region 内部工具
223
+
224
+ /** 列出崩溃目录下的崩溃报告文件(排除子目录与临时文件) */
225
+ private listCrashFiles(): string[] {
226
+ try {
227
+ if (!fs.accessSync(this.crashDir)) {
228
+ return []
229
+ }
230
+ const names = fs.listFileSync(this.crashDir)
231
+ return names.filter((name) => {
232
+ return name.endsWith('.json') && name.startsWith('crash_')
233
+ })
234
+ } catch (_) {
235
+ return []
236
+ }
237
+ }
238
+
239
+ private writeFile(path: string, content: string): void {
240
+ const file = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC)
241
+ try {
242
+ fs.writeSync(file.fd, content)
243
+ } finally {
244
+ fs.closeSync(file)
245
+ }
246
+ }
247
+
248
+ private deleteFile(path: string): void {
249
+ try {
250
+ fs.unlinkSync(path)
251
+ } catch (_) {
252
+ // ignore
253
+ }
254
+ }
255
+
256
+ private loadProcessedFingerprints(): Set<string> {
257
+ const result = new Set<string>()
258
+ try {
259
+ if (this.prefs.hasSync(PROCESSED_KEY)) {
260
+ const raw = this.prefs.getSync(PROCESSED_KEY, '[]') as string
261
+ const arr = JSON.parse(raw) as string[]
262
+ for (const item of arr) {
263
+ result.add(item)
264
+ }
265
+ }
266
+ } catch (_) {
267
+ // ignore
268
+ }
269
+ return result
270
+ }
271
+
272
+ private saveProcessedFingerprints(set: Set<string>): void {
273
+ try {
274
+ // 只保留最近的指纹,超出上限时丢最旧的(Set 保持插入序)
275
+ let items = Array.from(set)
276
+ if (items.length > PROCESSED_MAX) {
277
+ items = items.slice(items.length - PROCESSED_MAX)
278
+ }
279
+ this.prefs.putSync(PROCESSED_KEY, JSON.stringify(items))
280
+ this.prefs.flush()
281
+ } catch (_) {
282
+ // ignore
283
+ }
284
+ }
285
+
286
+ // endregion
287
+ }
288
+
289
+ /** 从文件名 crash_{timestamp}_{crash_id}.json 提取毫秒时间戳,解析失败返回 -1 */
290
+ function parseTimestampFromName(name: string): number {
291
+ const m = name.match(/^crash_(\d+)_/)
292
+ if (m) {
293
+ const ts = Number(m[1])
294
+ if (Number.isFinite(ts) && ts > 0) {
295
+ return ts
296
+ }
297
+ }
298
+ return -1
299
+ }
300
+
301
+ /** 设备信息(对齐 Android CrashHandler.buildDevice 字段) */
302
+ function buildDevice(): Record<string, Object> {
303
+ const obj: Record<string, Object> = {}
304
+ try {
305
+ obj.model = deviceInfo.productModel
306
+ obj.brand = deviceInfo.manufacture
307
+ obj.os_version = deviceInfo.osFullName
308
+ } catch (_) {
309
+ // ignore
310
+ }
311
+ return obj
312
+ }
313
+
314
+ /** 应用信息(对齐 Android CrashHandler.buildApp 字段) */
315
+ function buildApp(context: common.UIAbilityContext): Record<string, Object> {
316
+ const obj: Record<string, Object> = {}
317
+ try {
318
+ const bundleInfo = bundleManager.getBundleInfoForSelfSync(
319
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
320
+ )
321
+ obj.package = bundleInfo.name
322
+ obj.version = bundleInfo.versionName
323
+ obj.build = String(bundleInfo.versionCode)
324
+ } catch (_) {
325
+ obj.package = context.applicationInfo.name
326
+ }
327
+ return obj
328
+ }
329
+
330
+ /**
331
+ * 测试崩溃事件 ID:SDK 侧 triggerTestCrash 通过 emitter 发出,
332
+ * 由应用主线程(UIAbility)侧订阅并抛出未捕获异常。
333
+ *
334
+ * HarmonyOS 新版本中 worker 线程的未捕获异常不再导致进程退出(也不生成
335
+ * faultlog),只有主线程(JSAPP)未捕获异常会产生 JS_CRASH faultlog;
336
+ * 而 TurboModule 运行在 RNOH worker 线程,因此测试崩溃需要应用侧在
337
+ * 主线程配合订阅(见 example-harmony EntryAbility)。
338
+ */
339
+ export const TEST_CRASH_EVENT_ID = 9527
340
+
341
+ /** 测试崩溃触发(triggerTestCrash 调用):通知应用主线程抛出未捕获异常 */
342
+ export function triggerArkTsCrash(): void {
343
+ emitter.emit({ eventId: TEST_CRASH_EVENT_ID })
344
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * faultLogger fullLog 文本解析与崩溃 JSON 构建。
3
+ *
4
+ * faultlog 文本格式无强契约(JS_CRASH / CPP_CRASH / 不同系统版本的段落存在差异),
5
+ * 解析策略为「容忍式提取」:能解析出结构化堆栈则输出帧数据,
6
+ * 解析失败时依赖 raw_log 透传保证信息不丢失。
7
+ *
8
+ * 已实测的 JS_CRASH 格式(API 20)关键段落:
9
+ * Reason:Error
10
+ * Error name:Error
11
+ * Error message:RUM SDK Test: Uncaught ArkTS Exception
12
+ * Stacktrace:
13
+ * at anonymous entry (entry/src/main/ets/entryability/EntryAbility.ets:14:13)
14
+ * HybridStack:
15
+ * #00 pc 0000000000413db0 /system/lib64/platformsdk/libark_jsruntime.so(hash)
16
+ * HiLog:
17
+ * (日志内容,不应参与堆栈解析)
18
+ */
19
+
20
+ /** 崩溃帧(与 JS 层 NativeCrashFrame 对齐,采用 NDK 风格字段以便 formatStack 消费) */
21
+ export interface ParsedFrame {
22
+ library?: string
23
+ symbol?: string
24
+ address?: string
25
+ offset?: string
26
+ line?: number
27
+ }
28
+
29
+ /** fullLog 解析结果 */
30
+ export interface ParsedFullLog {
31
+ /** 崩溃线程 id(取自 "Tid:" 行,解析失败时由调用方用 pid 兜底) */
32
+ tid: string
33
+ /** JS 栈帧(JS_CRASH 的 "at fn (file:line:col)" 格式) */
34
+ jsFrames: ParsedFrame[]
35
+ /** 原生机帧("#NN pc addr lib(sym+off)" 格式) */
36
+ pcFrames: ParsedFrame[]
37
+ /** "Error name:" 行(JS_CRASH) */
38
+ errorName: string
39
+ /** "Error message:" 行(JS_CRASH) */
40
+ errorMessage: string
41
+ }
42
+
43
+ /** faultLogger FaultLogInfo(@ohos.faultLogger 已废弃但 API 20 仍可用,此处仅声明用到的字段) */
44
+ export interface FaultInfo {
45
+ pid: number
46
+ uid: number
47
+ type: number
48
+ /** 秒级时间戳 */
49
+ timestamp: number
50
+ reason: string
51
+ module: string
52
+ summary: string
53
+ fullLog: string
54
+ }
55
+
56
+ /** 原生帧行:#00 pc 0000000000014d74 /system/lib/xxx.so(func+0xb4) */
57
+ const PC_FRAME_RE = /^#(\w+)\s+pc\s+(0x[0-9a-fA-F]+|\d+)\s+(\S+)\s*(.*)$/
58
+
59
+ /** 原生帧尾部符号:func+0xb4 */
60
+ const PC_SYMBOL_RE = /^(\S+?)\+(0x[0-9a-fA-F]+|\d+)$/
61
+
62
+ /** JS 帧(at 格式,symbol 可含空格):at anonymous entry (file.ets:12:5) */
63
+ const JS_FRAME_PAREN_RE = /^\s*at\s+(.+?)\s*\((.+?):(\d+)(?::(\d+))?\)\s*$/
64
+
65
+ /** JS 帧(无括号格式):at file.ets:12:5 */
66
+ const JS_FRAME_BARE_RE = /^\s*at\s+(.+?):(\d+)(?::(\d+))?\s*$/
67
+
68
+ /** 头部属性行:Tid:825 */
69
+ const TID_RE = /^Tid:\s*(\d+)/
70
+
71
+ /** 键值属性行:Error name:xxx / Reason:xxx */
72
+ const ATTRIB_RE = /^([A-Za-z][A-Za-z0-9 _]*?):\s*(.*)$/
73
+
74
+ /** 段落标题行(无值):Stacktrace: / HybridStack: / HiLog: */
75
+ const SECTION_RE = /^([A-Za-z][A-Za-z0-9 _]*?):\s*$/
76
+
77
+ /** 帧数超过 512 时双侧截断,中间插入标记帧(与 Android 端策略一致) */
78
+ const TRUNCATION_THRESHOLD = 512
79
+ const KEEP_PER_SIDE = 256
80
+
81
+ function truncateFrames(frames: ParsedFrame[]): ParsedFrame[] {
82
+ if (frames.length <= TRUNCATION_THRESHOLD) {
83
+ return frames
84
+ }
85
+ const head = frames.slice(0, KEEP_PER_SIDE)
86
+ const tail = frames.slice(frames.length - KEEP_PER_SIDE)
87
+ const marker: ParsedFrame = {
88
+ symbol: 'frames omitted',
89
+ library: 'truncated',
90
+ address: `omitted_count=${frames.length - TRUNCATION_THRESHOLD}`,
91
+ }
92
+ return head.concat([marker]).concat(tail)
93
+ }
94
+
95
+ /** 段落内堆栈解析开关:HiLog 等日志段内的 "at"/pc 行不参与解析 */
96
+ function isStackSection(section: string): boolean {
97
+ return section === 'Stacktrace' ||
98
+ section === 'HybridStack' ||
99
+ section === 'Fault thread Info' ||
100
+ section === 'JsStack' ||
101
+ section === 'Js stack'
102
+ }
103
+
104
+ /**
105
+ * 解析 fullLog 文本,提取崩溃线程、异常信息与堆栈帧。
106
+ */
107
+ export function parseFullLog(fullLog: string): ParsedFullLog {
108
+ const result: ParsedFullLog = { tid: '', jsFrames: [], pcFrames: [], errorName: '', errorMessage: '' }
109
+ if (!fullLog) {
110
+ return result
111
+ }
112
+ let section = ''
113
+ const lines = fullLog.split('\n')
114
+ for (const rawLine of lines) {
115
+ const line = rawLine.replace(/\r$/, '')
116
+ const sectionMatch = line.match(SECTION_RE)
117
+ if (sectionMatch) {
118
+ section = sectionMatch[1] ?? ''
119
+ continue
120
+ }
121
+ const attribMatch = line.match(ATTRIB_RE)
122
+ if (attribMatch) {
123
+ const key = attribMatch[1] ?? ''
124
+ const value = attribMatch[2] ?? ''
125
+ if (key === 'Error name') {
126
+ result.errorName = value
127
+ continue
128
+ }
129
+ if (key === 'Error message') {
130
+ result.errorMessage = value
131
+ continue
132
+ }
133
+ const tidMatch = line.match(TID_RE)
134
+ if (tidMatch && !result.tid) {
135
+ result.tid = tidMatch[1] ?? ''
136
+ continue
137
+ }
138
+ continue
139
+ }
140
+ if (!isStackSection(section)) {
141
+ continue
142
+ }
143
+ if (line.startsWith('#')) {
144
+ const frame = parsePcFrame(line)
145
+ if (frame) {
146
+ result.pcFrames.push(frame)
147
+ }
148
+ continue
149
+ }
150
+ const trimmed = line.trim()
151
+ if (trimmed.startsWith('at ')) {
152
+ const frame = parseJsFrame(trimmed)
153
+ if (frame) {
154
+ result.jsFrames.push(frame)
155
+ }
156
+ }
157
+ }
158
+ return result
159
+ }
160
+
161
+ /** 解析原生机帧行 */
162
+ function parsePcFrame(line: string): ParsedFrame | undefined {
163
+ const m = line.match(PC_FRAME_RE)
164
+ if (!m) {
165
+ return undefined
166
+ }
167
+ const address = m[2] ?? ''
168
+ const library = m[3] ?? ''
169
+ const tail = (m[4] ?? '').trim()
170
+ const frame: ParsedFrame = { library, address }
171
+ if (tail) {
172
+ // 尾部形如 "(func+0xb4)" 或 "(func+0xb4)(build_id_hash)"
173
+ const symbolPart = tail.replace(/^\(/, '').split(')')[0] ?? ''
174
+ const sym = symbolPart.match(PC_SYMBOL_RE)
175
+ if (sym) {
176
+ frame.symbol = sym[1]
177
+ frame.offset = sym[2]
178
+ } else if (symbolPart) {
179
+ frame.symbol = symbolPart
180
+ }
181
+ }
182
+ return frame
183
+ }
184
+
185
+ /** 解析 JS 栈帧行 */
186
+ function parseJsFrame(line: string): ParsedFrame | undefined {
187
+ const paren = line.match(JS_FRAME_PAREN_RE)
188
+ if (paren) {
189
+ const frame = buildJsFrame(paren[2] ?? '', paren[3], paren[4])
190
+ const symbol = paren[1]?.trim() ?? ''
191
+ if (symbol) {
192
+ frame.symbol = symbol
193
+ }
194
+ return frame
195
+ }
196
+ const bare = line.match(JS_FRAME_BARE_RE)
197
+ if (bare) {
198
+ return buildJsFrame(bare[1] ?? '', bare[2], bare[3])
199
+ }
200
+ return undefined
201
+ }
202
+
203
+ function buildJsFrame(file: string, lineNo: string, colNo?: string): ParsedFrame {
204
+ const lineNum = Number(lineNo)
205
+ const frame: ParsedFrame = {
206
+ library: file,
207
+ line: Number.isFinite(lineNum) ? lineNum : 0,
208
+ }
209
+ if (colNo) {
210
+ frame.offset = colNo
211
+ }
212
+ return frame
213
+ }
214
+
215
+ /** fault 类别到崩溃 JSON exception.type 的映射 */
216
+ function faultTypeToName(type: number): string {
217
+ if (type === 2) {
218
+ return 'cpp_crash'
219
+ }
220
+ if (type === 4) {
221
+ return 'app_freeze'
222
+ }
223
+ return 'js_crash'
224
+ }
225
+
226
+ /** 从 reason 提取异常类名(如 "TypeError: xxx" → "TypeError";信号名整行返回) */
227
+ function extractClassName(reason: string): string {
228
+ const text = (reason || '').trim()
229
+ if (!text) {
230
+ return ''
231
+ }
232
+ const firstLine = text.split('\n')[0] ?? text
233
+ const idx = firstLine.indexOf(':')
234
+ if (idx > 0 && !firstLine.slice(0, idx).includes(' ')) {
235
+ return firstLine.slice(0, idx)
236
+ }
237
+ return firstLine
238
+ }
239
+
240
+ /** raw_log 截断上限:faultlog 全文(含 HiLog 段)可能超过 100KB,防止事件体积失控 */
241
+ const RAW_LOG_MAX_LENGTH = 32 * 1024
242
+
243
+ export interface BuildCrashJsonParams {
244
+ info: FaultInfo
245
+ parsed: ParsedFullLog
246
+ contextJson: string
247
+ deviceJson: string
248
+ appJson: string
249
+ }
250
+
251
+ /**
252
+ * 构建 Android 对齐结构的崩溃 JSON(JS 层 convertCrashToRumEvent 直接消费):
253
+ * crash_id / timestamp(ms) / platform / exception / crashed_thread /
254
+ * context / device / app / mechanism / raw_log。
255
+ */
256
+ export function buildCrashJson(params: BuildCrashJsonParams): string {
257
+ const { info, parsed } = params
258
+ const crashId = `hm_${info.type}_${info.timestamp}_${info.pid}`
259
+ const type = faultTypeToName(info.type)
260
+
261
+ // JS 崩溃优先使用 JS 栈(符号化、可读),否则使用原生机帧
262
+ const frames = truncateFrames(
263
+ parsed.jsFrames.length > 0 ? parsed.jsFrames : parsed.pcFrames
264
+ )
265
+
266
+ // FaultLogInfo.reason 对 JS 崩溃仅为错误名(如 "Error"),
267
+ // 完整信息需从 fullLog 的 Error name / Error message 提取
268
+ const className = parsed.errorName || extractClassName(info.reason)
269
+ const message = parsed.errorMessage || (info.reason || '').trim()
270
+
271
+ const exception: Record<string, Object> = {
272
+ type,
273
+ class_name: className || 'HarmonyCrash',
274
+ message,
275
+ }
276
+
277
+ const crashedThread: Record<string, Object> = {
278
+ id: parsed.tid !== '' ? parsed.tid : String(info.pid),
279
+ name: info.module || '',
280
+ frames: frames as Object[],
281
+ }
282
+
283
+ const mechanism: Record<string, Object> = {
284
+ type,
285
+ handled: false,
286
+ fault_type: String(info.type),
287
+ }
288
+
289
+ const root: Record<string, Object> = {
290
+ crash_id: crashId,
291
+ timestamp: info.timestamp * 1000,
292
+ platform: 'harmony',
293
+ exception: exception as Object,
294
+ crashed_thread: crashedThread as Object,
295
+ mechanism: mechanism as Object,
296
+ pid: info.pid,
297
+ context: safeParse(params.contextJson) as Object,
298
+ device: safeParse(params.deviceJson) as Object,
299
+ app: safeParse(params.appJson) as Object,
300
+ }
301
+
302
+ const rawLog = info.fullLog || ''
303
+ if (rawLog.length > RAW_LOG_MAX_LENGTH) {
304
+ root.raw_log = `${rawLog.slice(0, RAW_LOG_MAX_LENGTH)}...[truncated]`
305
+ } else {
306
+ root.raw_log = rawLog
307
+ }
308
+
309
+ return JSON.stringify(root)
310
+ }
311
+
312
+ function safeParse(text: string): Record<string, Object> | Object {
313
+ if (!text) {
314
+ return {}
315
+ }
316
+ try {
317
+ const obj = JSON.parse(text)
318
+ if (obj !== null && typeof obj === 'object') {
319
+ return obj as Record<string, Object>
320
+ }
321
+ return {}
322
+ } catch (_) {
323
+ return {}
324
+ }
325
+ }
326
+
327
+ /** 崩溃文件命名:crash_{毫秒时间戳}_{crash_id}.json(crash_id 与 buildCrashJson 派生一致) */
328
+ export function makeCrashFileName(info: FaultInfo): string {
329
+ const crashId = `hm_${info.type}_${info.timestamp}_${info.pid}`
330
+ return `crash_${info.timestamp * 1000}_${crashId}.json`
331
+ }
@@ -0,0 +1,5 @@
1
+
2
+ /**
3
+ */
4
+
5
+ export {}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This code was generated by "react-native codegen-lib-harmony"
3
+ */
4
+
5
+ export * from "./ts"
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This code was generated by "react-native codegen-lib-harmony"
3
+ */
4
+
5
+ export * as RNC from "./components/ts"
6
+ export * as TM from "./turboModules/ts"