@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.
- package/README.md +47 -0
- package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +13 -0
- package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +9 -0
- package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +64 -1
- package/harmony/rum_sdk/BuildProfile.ets +17 -0
- package/harmony/rum_sdk/Index.ets +2 -0
- package/harmony/rum_sdk/build-profile.json5 +22 -0
- package/harmony/rum_sdk/hvigorfile.ts +1 -0
- package/harmony/rum_sdk/oh-package.json5 +10 -0
- package/harmony/rum_sdk/src/main/ets/RumSdkPackage.ets +15 -0
- package/harmony/rum_sdk/src/main/ets/RumSdkTurboModule.ts +274 -0
- package/harmony/rum_sdk/src/main/ets/crash/CrashManager.ts +344 -0
- package/harmony/rum_sdk/src/main/ets/crash/FaultLogParser.ts +331 -0
- package/harmony/rum_sdk/src/main/ets/generated/components/ts.ts +5 -0
- package/harmony/rum_sdk/src/main/ets/generated/index.ets +5 -0
- package/harmony/rum_sdk/src/main/ets/generated/ts.ts +6 -0
- package/harmony/rum_sdk/src/main/ets/generated/turboModules/RumSdkReactnative.ts +46 -0
- package/harmony/rum_sdk/src/main/ets/generated/turboModules/ts.ts +5 -0
- package/harmony/rum_sdk/src/main/module.json5 +9 -0
- package/harmony/rum_sdk/ts.ts +1 -0
- package/lib/commonjs/NativeRumSdkReactnative.js +1 -1
- package/lib/commonjs/utils/crash.js +1 -1
- package/lib/commonjs/utils/platform.js +1 -1
- package/lib/module/NativeRumSdkReactnative.js +1 -1
- package/lib/module/utils/crash.js +1 -1
- package/lib/module/utils/platform.js +1 -1
- package/lib/typescript/commonjs/src/types/crash.d.ts +2 -0
- package/lib/typescript/commonjs/src/utils/platform.d.ts +7 -0
- package/lib/typescript/module/src/types/crash.d.ts +2 -0
- package/lib/typescript/module/src/utils/platform.d.ts +7 -0
- package/package.json +22 -3
- 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
|
+
}
|