@arms/rum-reactnative 0.1.1 → 0.1.3-beta.0

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 (178) hide show
  1. package/README.md +399 -15
  2. package/RumSdkReactnative.podspec +28 -0
  3. package/android/build.gradle +83 -0
  4. package/android/gradle.properties +6 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativeModule.kt +512 -0
  7. package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativePackage.kt +33 -0
  8. package/android/src/main/java/com/rumsdkreactnative/crash/CrashContextManager.kt +157 -0
  9. package/android/src/main/java/com/rumsdkreactnative/crash/CrashHandler.kt +444 -0
  10. package/android/src/main/java/com/rumsdkreactnative/crash/CrashInitProvider.kt +50 -0
  11. package/android/src/main/java/com/rumsdkreactnative/crash/NdkCrashHandler.kt +66 -0
  12. package/android/src/main/jni/CMakeLists.txt +21 -0
  13. package/android/src/main/jni/rum_backtrace.c +144 -0
  14. package/android/src/main/jni/rum_crash_handler.c +200 -0
  15. package/android/src/main/jni/rum_crash_writer.c +442 -0
  16. package/android/src/main/jni/rum_module_info.c +316 -0
  17. package/android/src/main/jni/rum_module_info.h +54 -0
  18. package/android/src/main/jni/rum_test_crash.cpp +55 -0
  19. package/app.plugin.js +11 -0
  20. package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +671 -0
  21. package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +12 -0
  22. package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +238 -0
  23. package/ios/PrivacyInfo.xcprivacy +93 -0
  24. package/ios/RumCrashReporter.h +38 -0
  25. package/ios/RumCrashReporter.mm +861 -0
  26. package/ios/RumSdkReactnative.h +5 -0
  27. package/ios/RumSdkReactnative.mm +291 -0
  28. package/ios/fishhook/fishhook.c +264 -0
  29. package/ios/fishhook/fishhook.h +76 -0
  30. package/lib/commonjs/NativeRumSdkReactnative.js +50 -0
  31. package/lib/commonjs/collector/action/index.js +108 -0
  32. package/lib/commonjs/collector/exception/crash.js +148 -0
  33. package/lib/commonjs/collector/exception/index.js +238 -0
  34. package/lib/commonjs/collector/index.js +42 -0
  35. package/lib/commonjs/collector/resource/api.js +286 -0
  36. package/lib/commonjs/collector/view/perf.js +79 -0
  37. package/lib/commonjs/collector/view/pv.js +88 -0
  38. package/lib/commonjs/expoConfigPlugin.js +37 -0
  39. package/lib/commonjs/index.js +29 -0
  40. package/lib/commonjs/package.json +1 -0
  41. package/lib/commonjs/plugin.js +47 -0
  42. package/lib/commonjs/processor/default-processor.js +27 -0
  43. package/lib/commonjs/processor/session-processor.js +47 -0
  44. package/lib/commonjs/reporter/index.js +148 -0
  45. package/lib/commonjs/reporter/monitor-reporter.js +43 -0
  46. package/lib/commonjs/shell.js +91 -0
  47. package/lib/commonjs/types/client.js +6 -0
  48. package/lib/commonjs/types/crash.js +37 -0
  49. package/lib/commonjs/types/index.js +2 -0
  50. package/lib/commonjs/utils/action.js +305 -0
  51. package/lib/commonjs/utils/api.js +83 -0
  52. package/lib/commonjs/utils/base.js +95 -0
  53. package/lib/commonjs/utils/crash.js +146 -0
  54. package/lib/commonjs/utils/platform.js +162 -0
  55. package/lib/commonjs/utils/session.js +158 -0
  56. package/lib/commonjs/utils/url.js +59 -0
  57. package/lib/commonjs/utils/view.js +93 -0
  58. package/lib/module/NativeRumSdkReactnative.js +47 -0
  59. package/lib/module/collector/action/index.js +45 -10
  60. package/lib/module/collector/exception/crash.js +143 -0
  61. package/lib/module/collector/exception/index.js +144 -46
  62. package/lib/module/collector/index.js +8 -0
  63. package/lib/module/collector/resource/api.js +6 -6
  64. package/lib/module/collector/view/pv.js +13 -2
  65. package/lib/module/expoConfigPlugin.js +33 -0
  66. package/lib/module/index.js +1 -0
  67. package/lib/module/plugin.js +4 -1
  68. package/lib/module/processor/session-processor.js +22 -0
  69. package/lib/module/reporter/index.js +102 -16
  70. package/lib/module/reporter/monitor-reporter.js +38 -0
  71. package/lib/module/shell.js +32 -7
  72. package/lib/module/types/crash.js +33 -0
  73. package/lib/module/utils/action.js +299 -0
  74. package/lib/module/utils/api.js +10 -10
  75. package/lib/module/utils/base.js +9 -9
  76. package/lib/module/utils/crash.js +141 -0
  77. package/lib/module/utils/platform.js +127 -72
  78. package/lib/module/utils/session.js +23 -4
  79. package/lib/module/utils/view.js +2 -1
  80. package/lib/typescript/commonjs/package.json +1 -0
  81. package/lib/typescript/commonjs/src/NativeRumSdkReactnative.d.ts +49 -0
  82. package/lib/typescript/{src → commonjs/src}/collector/action/index.d.ts +7 -1
  83. package/lib/typescript/commonjs/src/collector/exception/crash.d.ts +28 -0
  84. package/lib/typescript/{src → commonjs/src}/collector/exception/index.d.ts +7 -0
  85. package/lib/typescript/commonjs/src/collector/index.d.ts +6 -0
  86. package/lib/typescript/{src → commonjs/src}/collector/view/pv.d.ts +2 -2
  87. package/lib/typescript/commonjs/src/expoConfigPlugin.d.ts +28 -0
  88. package/lib/typescript/commonjs/src/index.d.ts +6 -0
  89. package/lib/typescript/{src → commonjs/src}/processor/session-processor.d.ts +2 -0
  90. package/lib/typescript/{src → commonjs/src}/reporter/index.d.ts +3 -1
  91. package/lib/typescript/commonjs/src/reporter/monitor-reporter.d.ts +8 -0
  92. package/lib/typescript/commonjs/src/shell.d.ts +57 -0
  93. package/lib/typescript/{src → commonjs/src}/types/client.d.ts +2 -2
  94. package/lib/typescript/commonjs/src/types/crash.d.ts +95 -0
  95. package/lib/typescript/commonjs/src/utils/action.d.ts +63 -0
  96. package/lib/typescript/{src → commonjs/src}/utils/api.d.ts +1 -1
  97. package/lib/typescript/commonjs/src/utils/crash.d.ts +12 -0
  98. package/lib/typescript/commonjs/src/utils/platform.d.ts +22 -0
  99. package/lib/typescript/{src → commonjs/src}/utils/session.d.ts +4 -0
  100. package/lib/typescript/module/src/NativeRumSdkReactnative.d.ts +49 -0
  101. package/lib/typescript/module/src/collector/action/index.d.ts +21 -0
  102. package/lib/typescript/module/src/collector/exception/crash.d.ts +28 -0
  103. package/lib/typescript/module/src/collector/exception/index.d.ts +32 -0
  104. package/lib/typescript/module/src/collector/index.d.ts +6 -0
  105. package/lib/typescript/module/src/collector/resource/api.d.ts +21 -0
  106. package/lib/typescript/module/src/collector/view/perf.d.ts +1 -0
  107. package/lib/typescript/module/src/collector/view/pv.d.ts +16 -0
  108. package/lib/typescript/module/src/expoConfigPlugin.d.ts +28 -0
  109. package/lib/typescript/module/src/index.d.ts +6 -0
  110. package/lib/typescript/module/src/plugin.d.ts +3 -0
  111. package/lib/typescript/module/src/processor/default-processor.d.ts +6 -0
  112. package/lib/typescript/module/src/processor/session-processor.d.ts +11 -0
  113. package/lib/typescript/module/src/reporter/index.d.ts +18 -0
  114. package/lib/typescript/module/src/reporter/monitor-reporter.d.ts +8 -0
  115. package/lib/typescript/module/src/shell.d.ts +57 -0
  116. package/lib/typescript/module/src/types/client.d.ts +27 -0
  117. package/lib/typescript/module/src/types/crash.d.ts +95 -0
  118. package/lib/typescript/module/src/types/index.d.ts +118 -0
  119. package/lib/typescript/module/src/utils/action.d.ts +63 -0
  120. package/lib/typescript/module/src/utils/api.d.ts +59 -0
  121. package/lib/typescript/module/src/utils/base.d.ts +8 -0
  122. package/lib/typescript/module/src/utils/crash.d.ts +12 -0
  123. package/lib/typescript/module/src/utils/platform.d.ts +22 -0
  124. package/lib/typescript/module/src/utils/session.d.ts +41 -0
  125. package/lib/typescript/module/src/utils/url.d.ts +17 -0
  126. package/lib/typescript/module/src/utils/view.d.ts +13 -0
  127. package/package.json +61 -26
  128. package/LICENSE +0 -20
  129. package/lib/module/collector/action/index.js.map +0 -1
  130. package/lib/module/collector/exception/index.js.map +0 -1
  131. package/lib/module/collector/resource/api.js.map +0 -1
  132. package/lib/module/collector/view/perf.js.map +0 -1
  133. package/lib/module/collector/view/pv.js.map +0 -1
  134. package/lib/module/index.js.map +0 -1
  135. package/lib/module/plugin.js.map +0 -1
  136. package/lib/module/processor/default-processor.js.map +0 -1
  137. package/lib/module/processor/session-processor.js.map +0 -1
  138. package/lib/module/reporter/index.js.map +0 -1
  139. package/lib/module/shell.js.map +0 -1
  140. package/lib/module/types/client.js.map +0 -1
  141. package/lib/module/types/index.js.map +0 -1
  142. package/lib/module/utils/api.js.map +0 -1
  143. package/lib/module/utils/base.js.map +0 -1
  144. package/lib/module/utils/platform.js.map +0 -1
  145. package/lib/module/utils/session.js.map +0 -1
  146. package/lib/module/utils/url.js.map +0 -1
  147. package/lib/module/utils/view.js.map +0 -1
  148. package/lib/typescript/src/collector/action/index.d.ts.map +0 -1
  149. package/lib/typescript/src/collector/exception/index.d.ts.map +0 -1
  150. package/lib/typescript/src/collector/resource/api.d.ts.map +0 -1
  151. package/lib/typescript/src/collector/view/perf.d.ts.map +0 -1
  152. package/lib/typescript/src/collector/view/pv.d.ts.map +0 -1
  153. package/lib/typescript/src/index.d.ts +0 -4
  154. package/lib/typescript/src/index.d.ts.map +0 -1
  155. package/lib/typescript/src/plugin.d.ts.map +0 -1
  156. package/lib/typescript/src/processor/default-processor.d.ts.map +0 -1
  157. package/lib/typescript/src/processor/session-processor.d.ts.map +0 -1
  158. package/lib/typescript/src/reporter/index.d.ts.map +0 -1
  159. package/lib/typescript/src/shell.d.ts +0 -26
  160. package/lib/typescript/src/shell.d.ts.map +0 -1
  161. package/lib/typescript/src/types/client.d.ts.map +0 -1
  162. package/lib/typescript/src/types/index.d.ts.map +0 -1
  163. package/lib/typescript/src/utils/api.d.ts.map +0 -1
  164. package/lib/typescript/src/utils/base.d.ts.map +0 -1
  165. package/lib/typescript/src/utils/platform.d.ts +0 -15
  166. package/lib/typescript/src/utils/platform.d.ts.map +0 -1
  167. package/lib/typescript/src/utils/session.d.ts.map +0 -1
  168. package/lib/typescript/src/utils/url.d.ts.map +0 -1
  169. package/lib/typescript/src/utils/view.d.ts.map +0 -1
  170. /package/lib/typescript/{src → commonjs/src}/collector/resource/api.d.ts +0 -0
  171. /package/lib/typescript/{src → commonjs/src}/collector/view/perf.d.ts +0 -0
  172. /package/lib/typescript/{src → commonjs/src}/plugin.d.ts +0 -0
  173. /package/lib/typescript/{src → commonjs/src}/processor/default-processor.d.ts +0 -0
  174. /package/lib/typescript/{src → commonjs/src}/types/index.d.ts +0 -0
  175. /package/lib/typescript/{src → commonjs/src}/utils/base.d.ts +0 -0
  176. /package/lib/typescript/{src → commonjs/src}/utils/url.d.ts +0 -0
  177. /package/lib/typescript/{src → commonjs/src}/utils/view.d.ts +0 -0
  178. /package/lib/typescript/{package.json → module/package.json} +0 -0
@@ -0,0 +1,861 @@
1
+ #import "RumCrashReporter.h"
2
+
3
+ #import <KSCrash/KSCrash.h>
4
+ #import <KSCrash/KSCrashConfiguration.h>
5
+ #import <KSCrash/KSCrashMonitorType.h>
6
+ #import <KSCrash/KSCrashReportStore.h>
7
+ #import <KSCrash/KSCrashReport.h>
8
+ #import <KSCrash/KSCrashReportFilter.h>
9
+
10
+ #import <UIKit/UIKit.h>
11
+ #import <dlfcn.h>
12
+ #import <signal.h>
13
+ #import <stdlib.h>
14
+
15
+ #import "fishhook.h"
16
+
17
+ #pragma mark - Constants
18
+
19
+ /** RUM 自定义字段在 KSCrash userInfo 中的命名空间 */
20
+ static NSString *const kRumCrashUserInfoContextKey = @"rum_context";
21
+
22
+ /** RUM SDK 数据子目录 */
23
+ static NSString *const kRumCrashDirName = @"RumCrashReporter";
24
+ static NSString *const kRumContextFileName = @"context.json";
25
+ static NSString *const kRumCrashedLastRunFlagName = @"crashed_last_run.flag";
26
+
27
+ /** 旧版目录名(用于升级迁移) */
28
+ static NSString *const kLegacyCrashDirName = @"ARMSCrashReporter";
29
+
30
+ /** Forward-declare private class methods used by the constructor */
31
+ @interface RumCrashReporter (Private)
32
+ + (NSString *)crashDir;
33
+ + (NSString *)contextPath;
34
+ + (id)readJSONFileAtPath:(NSString *)path;
35
+ + (void)atomicWriteJSONObject:(id)obj toPath:(NSString *)path;
36
+ @end
37
+
38
+ #pragma mark - Early constructor
39
+
40
+ /**
41
+ * 早期安装:dyld 加载本动态库时立即安装 Mach + Signal 监控。
42
+ * 这两类监控不需要 RN bridge,可在 app 启动最早期捕获原生崩溃。
43
+ * NSException + CPPException 由 +install 补充安装(需要 RN 环境)。
44
+ */
45
+ static volatile int gRumCrashLoaded = 0;
46
+ static BOOL gKSCrashInstalled = NO;
47
+
48
+ __attribute__((constructor))
49
+ static void RumCrashReporterEarlyLoad(void) {
50
+ // 不在 constructor 里安装 KSCrash —— RN bridge 启动时会调用 RCTInstallSignalHandlers
51
+ // 覆盖 KSCrash 的信号处理程序。改为在 +install(RN 模块实例化时)一次性安装全部
52
+ // monitor,确保 KSCrash 的 signal handler 是最后安装的,能真正捕获到崩溃信号。
53
+ gRumCrashLoaded = 1;
54
+ }
55
+
56
+ #pragma mark - Helpers
57
+
58
+ @interface RumCrashReporter ()
59
+ @end
60
+
61
+ @implementation RumCrashReporter
62
+
63
+ /** SDK 私有目录:<Library>/Caches/RumCrashReporter/ */
64
+ + (NSString *)crashDir {
65
+ static NSString *dir;
66
+ static dispatch_once_t onceToken;
67
+ dispatch_once(&onceToken, ^{
68
+ NSString *caches = [NSSearchPathForDirectoriesInDomains(
69
+ NSCachesDirectory, NSUserDomainMask, YES) firstObject];
70
+ dir = [caches stringByAppendingPathComponent:kRumCrashDirName];
71
+ NSFileManager *fm = [NSFileManager defaultManager];
72
+
73
+ // 迁移旧版目录 ARMSCrashReporter → RumCrashReporter
74
+ NSString *legacyDir = [caches stringByAppendingPathComponent:kLegacyCrashDirName];
75
+ if ([fm fileExistsAtPath:legacyDir]) {
76
+ // 如果新目录已存在,合并内容;否则直接重命名
77
+ if (![fm fileExistsAtPath:dir]) {
78
+ [fm moveItemAtPath:legacyDir toPath:dir error:nil];
79
+ } else {
80
+ // 逐个迁移文件
81
+ NSArray *files = [fm contentsOfDirectoryAtPath:legacyDir error:nil];
82
+ for (NSString *file in files) {
83
+ NSString *src = [legacyDir stringByAppendingPathComponent:file];
84
+ NSString *dst = [dir stringByAppendingPathComponent:file];
85
+ if (![fm fileExistsAtPath:dst]) {
86
+ [fm moveItemAtPath:src toPath:dst error:nil];
87
+ }
88
+ }
89
+ [fm removeItemAtPath:legacyDir error:nil];
90
+ }
91
+ }
92
+
93
+ if (![fm fileExistsAtPath:dir]) {
94
+ [fm createDirectoryAtPath:dir
95
+ withIntermediateDirectories:YES
96
+ attributes:nil
97
+ error:nil];
98
+ }
99
+ });
100
+ return dir;
101
+ }
102
+
103
+ + (NSString *)contextPath {
104
+ return [[self crashDir] stringByAppendingPathComponent:kRumContextFileName];
105
+ }
106
+
107
+ #pragma mark - Install
108
+
109
+ + (void)install {
110
+ static dispatch_once_t onceToken;
111
+ dispatch_once(&onceToken, ^{
112
+ @try {
113
+ KSCrashConfiguration *config = [[KSCrashConfiguration alloc] init];
114
+ // 重新安装全部监控:Mach + Signal + NSException + CPPException + ApplicationState
115
+ // KSCrash installWithConfiguration: 会覆盖之前的配置,
116
+ // 因此必须包含 constructor 中已安装的 Mach + Signal,否则会丢失
117
+ // ApplicationState monitor 必须启用:它负责持久化 crashedThisLaunch 状态
118
+ // 到 CrashState.json,下次启动读取为 crashedLastLaunch。不启用的话
119
+ // crashedLastLaunch 永远是 NO,UI 上的"上次运行是否崩溃"始终为否。
120
+ config.monitors = KSCrashMonitorTypeMachException
121
+ | KSCrashMonitorTypeSignal
122
+ | KSCrashMonitorTypeCPPException
123
+ | KSCrashMonitorTypeNSException
124
+ | KSCrashMonitorTypeApplicationState;
125
+
126
+ // 透传当前 context(session/view)作为 userInfo,崩溃时一同落盘
127
+ NSDictionary *currentContext = [self readJSONFileAtPath:[self contextPath]];
128
+ if ([currentContext isKindOfClass:[NSDictionary class]]) {
129
+ config.userInfoJSON = @{ kRumCrashUserInfoContextKey: currentContext };
130
+ }
131
+
132
+ // Detox(如果注入)通过 fishhook swizzle 了 sigaction,会静默丢弃后续对
133
+ // SIGABRT/SIGSEGV 等致命信号的 sigaction 调用。KSCrash 的 signal monitor
134
+ // 调用 sigaction 安装自己的 handler 时就会被丢弃,导致 KSCrash 无法捕获
135
+ // 信号类崩溃。
136
+ // 这里在 KSCrash install 之前用 fishhook 把 sigaction 的 lazy pointer
137
+ // 重绑回 libsystem 的真实函数(dlsym 返回的是真实函数地址,不受 fishhook
138
+ // 影响)。重绑后 KSCrash 的 sigaction 调用会真正生效,其 signal handler
139
+ // 会覆盖 Detox 的 handler。崩溃发生时 KSCrash 先写报告,再 uninstall +
140
+ // raise(sig) 触发 Detox 的 handler(KSCrash 在 uninstall 时恢复了 previous
141
+ // handler),Detox 仍能感知崩溃并通知测试 runner。
142
+ static int (*real_sigaction)(int, const struct sigaction * __restrict,
143
+ struct sigaction * __restrict) = NULL;
144
+ if (real_sigaction == NULL) {
145
+ real_sigaction = (int (*)(int, const struct sigaction * __restrict,
146
+ struct sigaction * __restrict))
147
+ dlsym(RTLD_DEFAULT, "sigaction");
148
+ }
149
+ if (real_sigaction != NULL) {
150
+ struct rebinding rebindings[] = {
151
+ {"sigaction", (void *)real_sigaction, NULL},
152
+ };
153
+ rebind_symbols(rebindings, sizeof(rebindings) / sizeof(rebindings[0]));
154
+ }
155
+
156
+ NSError *error = nil;
157
+ BOOL ok = [[KSCrash sharedInstance] installWithConfiguration:config error:&error];
158
+ if (ok) {
159
+ gKSCrashInstalled = YES;
160
+ } else {
161
+ NSLog(@"[RumCrashReporter] KSCrash install failed: %@", error);
162
+ }
163
+
164
+ // 同步 crashedLastRun 到本地 flag file。
165
+ // KSCrash 的 crashedLastLaunch 在 reports 被删除后会重置为 NO,
166
+ // 但 SDK 在 setup 阶段就会读取并清理 pending 文件,导致用户后续查询时
167
+ // 始终拿到 NO。这里把"上次是否崩溃"持久化到文件,install 时按 KSCrash
168
+ // 状态写入/删除,crashedLastRun 优先读文件。
169
+ NSString *flagPath = [[self crashDir]
170
+ stringByAppendingPathComponent:kRumCrashedLastRunFlagName];
171
+ if ([KSCrash sharedInstance].crashedLastLaunch) {
172
+ [[NSFileManager defaultManager]
173
+ createFileAtPath:flagPath
174
+ contents:[NSData data]
175
+ attributes:nil];
176
+ } else {
177
+ [[NSFileManager defaultManager] removeItemAtPath:flagPath error:nil];
178
+ }
179
+ (void)ok;
180
+ } @catch (NSException *e) {
181
+ NSLog(@"[RumCrashReporter] install exception: %@", e);
182
+ }
183
+ });
184
+ }
185
+
186
+ + (BOOL)crashedLastRun {
187
+ // 优先读 flag file —— KSCrash.crashedLastLaunch 在 reports 被清理后会被重置,
188
+ // 而 SDK 会在 app 启动早期就处理 pending 文件,因此直接读 KSCrash 会导致
189
+ // 用户在 UI 上查询时永远得到 NO。
190
+ NSString *flagPath = [[self crashDir]
191
+ stringByAppendingPathComponent:kRumCrashedLastRunFlagName];
192
+ if ([[NSFileManager defaultManager] fileExistsAtPath:flagPath]) {
193
+ return YES;
194
+ }
195
+ @try {
196
+ return [KSCrash sharedInstance].crashedLastLaunch;
197
+ } @catch (...) {
198
+ return NO;
199
+ }
200
+ }
201
+
202
+ #pragma mark - Pending reports
203
+
204
+ + (NSArray<NSDictionary *> *)pendingCrashReports {
205
+ NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
206
+ @try {
207
+ KSCrashReportStore *store = [KSCrash sharedInstance].reportStore;
208
+ if (store == nil) {
209
+ NSLog(@"[RumCrashReporter] pendingCrashReports: reportStore is nil (gKSCrashInstalled=%d), returning empty.", gKSCrashInstalled);
210
+ return result;
211
+ }
212
+ NSArray<NSNumber *> *reportIDs = store.reportIDs ?: @[];
213
+ for (NSNumber *reportID in reportIDs) {
214
+ @autoreleasepool {
215
+ KSCrashReportDictionary *report = [store reportForID:[reportID longLongValue]];
216
+ NSDictionary *raw = nil;
217
+ if (report != nil) {
218
+ raw = report.value;
219
+ }
220
+ if (![raw isKindOfClass:[NSDictionary class]]) {
221
+ continue;
222
+ }
223
+ NSDictionary *normalized = [self normalizeKSCrashReport:raw
224
+ reportID:reportID];
225
+ if (normalized != nil) {
226
+ [result addObject:normalized];
227
+ }
228
+ }
229
+ }
230
+ } @catch (NSException *e) {
231
+ NSLog(@"[RumCrashReporter] pendingCrashReports exception: %@", e);
232
+ }
233
+ return result;
234
+ }
235
+
236
+ + (void)clearCrashReportsWithIds:(NSArray<NSString *> *)crashIds {
237
+ if (crashIds.count == 0) {
238
+ return;
239
+ }
240
+ @try {
241
+ KSCrashReportStore *store = [KSCrash sharedInstance].reportStore;
242
+ if (store == nil) {
243
+ return;
244
+ }
245
+
246
+ // crash_id 可能是 KSCrash 的 report.id(UUID 字符串)或数字 reportID 字符串。
247
+ // 数字 ID 直接走 deleteReportWithID;UUID 需要遍历 reports 匹配 report.id。
248
+ NSMutableSet<NSString *> *uuidSet = [NSMutableSet set];
249
+ NSMutableSet<NSNumber *> *numericIDs = [NSMutableSet set];
250
+ for (NSString *cid in crashIds) {
251
+ if (![cid isKindOfClass:[NSString class]] || cid.length == 0) {
252
+ continue;
253
+ }
254
+ // 判断 cid 是否为纯数字字符串(reportID)
255
+ // 不能用 longLongValue > 0,因为 reportID = 0 时会被误判为 UUID
256
+ NSCharacterSet *nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
257
+ if ([cid rangeOfCharacterFromSet:nonDigits].location == NSNotFound) {
258
+ [numericIDs addObject:@([cid longLongValue])];
259
+ } else {
260
+ [uuidSet addObject:cid];
261
+ }
262
+ }
263
+
264
+ // 1. 数字 ID 直接删除
265
+ for (NSNumber *rid in numericIDs) {
266
+ [store deleteReportWithID:[rid longLongValue]];
267
+ }
268
+
269
+ // 2. UUID 匹配:遍历所有 reportIDs,读 report.id,命中则删除
270
+ if (uuidSet.count > 0) {
271
+ NSArray<NSNumber *> *allReportIDs = [store.reportIDs copy] ?: @[];
272
+ for (NSNumber *rid in allReportIDs) {
273
+ @autoreleasepool {
274
+ KSCrashReportDictionary *report = [store reportForID:[rid longLongValue]];
275
+ NSDictionary *raw = report ? report.value : nil;
276
+ NSDictionary *reportMeta = [raw isKindOfClass:[NSDictionary class]]
277
+ ? raw[@"report"]
278
+ : nil;
279
+ NSString *uuid = [reportMeta isKindOfClass:[NSDictionary class]]
280
+ ? reportMeta[@"id"]
281
+ : nil;
282
+ if ([uuid isKindOfClass:[NSString class]] && [uuidSet containsObject:uuid]) {
283
+ [store deleteReportWithID:[rid longLongValue]];
284
+ }
285
+ }
286
+ }
287
+ }
288
+ } @catch (NSException *e) {
289
+ NSLog(@"[RumCrashReporter] clearCrashReports exception: %@", e);
290
+ }
291
+ }
292
+
293
+ #pragma mark - Clear All & Cleanup
294
+
295
+ + (void)clearAllCrashReports {
296
+ @try {
297
+ KSCrashReportStore *store = [KSCrash sharedInstance].reportStore;
298
+ if (store == nil) {
299
+ return;
300
+ }
301
+ NSArray<NSNumber *> *reportIDs = [store.reportIDs copy] ?: @[];
302
+ for (NSNumber *reportID in reportIDs) {
303
+ [store deleteReportWithID:[reportID longLongValue]];
304
+ }
305
+ } @catch (NSException *e) {
306
+ NSLog(@"[RumCrashReporter] clearAllCrashReports exception: %@", e);
307
+ }
308
+ }
309
+
310
+ + (void)cleanupOldCrashesWithMaxFiles:(NSInteger)maxFiles maxAgeMs:(double)maxAgeMs {
311
+ @try {
312
+ KSCrashReportStore *store = [KSCrash sharedInstance].reportStore;
313
+ if (store == nil) {
314
+ return;
315
+ }
316
+ NSArray<NSNumber *> *reportIDs = [store.reportIDs copy] ?: @[];
317
+ if (reportIDs.count == 0) {
318
+ return;
319
+ }
320
+
321
+ NSInteger maxCount = MAX(maxFiles, 1);
322
+ double now = [[NSDate date] timeIntervalSince1970] * 1000.0;
323
+
324
+ // Collect report timestamps and sort by newest first
325
+ NSMutableArray<NSDictionary *> *reports = [NSMutableArray array];
326
+ for (NSNumber *reportID in reportIDs) {
327
+ @autoreleasepool {
328
+ KSCrashReportDictionary *report = [store reportForID:[reportID longLongValue]];
329
+ NSDictionary *raw = report ? report.value : nil;
330
+ double timestamp = now; // default to now if unknown
331
+ if ([raw isKindOfClass:[NSDictionary class]]) {
332
+ NSDictionary *reportMeta = raw[@"report"];
333
+ if ([reportMeta isKindOfClass:[NSDictionary class]]) {
334
+ id t = reportMeta[@"timestamp"];
335
+ if ([t isKindOfClass:[NSString class]]) {
336
+ NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
337
+ fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
338
+ NSArray<NSString *> *formats = @[
339
+ @"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
340
+ @"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
341
+ @"yyyy-MM-dd'T'HH:mm:ss'Z'",
342
+ @"yyyy-MM-dd'T'HH:mm:ssXXX"
343
+ ];
344
+ NSDate *d = nil;
345
+ for (NSString *format in formats) {
346
+ fmt.dateFormat = format;
347
+ d = [fmt dateFromString:t];
348
+ if (d) break;
349
+ }
350
+ if (d) {
351
+ timestamp = [d timeIntervalSince1970] * 1000.0;
352
+ }
353
+ } else if ([t isKindOfClass:[NSNumber class]]) {
354
+ timestamp = [(NSNumber *)t doubleValue] * 1000.0;
355
+ }
356
+ }
357
+ }
358
+ [reports addObject:@{
359
+ @"id": reportID,
360
+ @"timestamp": @(timestamp)
361
+ }];
362
+ }
363
+ }
364
+
365
+ // Sort by timestamp descending (newest first)
366
+ [reports sortUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
367
+ return [b[@"timestamp"] compare:a[@"timestamp"]];
368
+ }];
369
+
370
+ // 1. Delete expired reports
371
+ NSMutableArray<NSDictionary *> *remaining = [NSMutableArray array];
372
+ for (NSDictionary *entry in reports) {
373
+ double ts = [entry[@"timestamp"] doubleValue];
374
+ if (now - ts > maxAgeMs) {
375
+ [store deleteReportWithID:[entry[@"id"] longLongValue]];
376
+ } else {
377
+ [remaining addObject:entry];
378
+ }
379
+ }
380
+
381
+ // 2. Delete oldest beyond maxFiles limit
382
+ if ((NSInteger)remaining.count > maxCount) {
383
+ for (NSInteger i = maxCount; i < (NSInteger)remaining.count; i++) {
384
+ [store deleteReportWithID:[remaining[i][@"id"] longLongValue]];
385
+ }
386
+ }
387
+ } @catch (NSException *e) {
388
+ NSLog(@"[RumCrashReporter] cleanupOldCrashes exception: %@", e);
389
+ }
390
+ }
391
+
392
+ #pragma mark - Context
393
+
394
+ + (void)updateCrashContext:(NSString *)context {
395
+ if (context.length == 0) {
396
+ return;
397
+ }
398
+
399
+ // 1. 同步写入文件(保证崩溃时可读,与 Android 行为一致)
400
+ NSData *data = [context dataUsingEncoding:NSUTF8StringEncoding];
401
+ id obj = data ? [NSJSONSerialization JSONObjectWithData:data
402
+ options:0
403
+ error:nil] : nil;
404
+ if (![obj isKindOfClass:[NSDictionary class]]) {
405
+ return;
406
+ }
407
+ @try {
408
+ [self atomicWriteJSONObject:obj toPath:[self contextPath]];
409
+ } @catch (NSException *e) {
410
+ NSLog(@"[RumCrashReporter] updateCrashContext sync write failed: %@", e);
411
+ }
412
+
413
+ // 2. KSCrash userInfo 同步更新(context 变更不频繁,同步开销可忽略;
414
+ // 消除异步竞态:崩溃发生在 dispatch_async 排队期间时报告中 context 为旧值)
415
+ NSDictionary *contextObj = obj;
416
+ @try {
417
+ [KSCrash sharedInstance].userInfo = @{ kRumCrashUserInfoContextKey: contextObj };
418
+ } @catch (...) {}
419
+ }
420
+
421
+ #pragma mark - JSON file IO
422
+
423
+ + (id)readJSONFileAtPath:(NSString *)path {
424
+ NSData *data = [NSData dataWithContentsOfFile:path];
425
+ if (data.length == 0) {
426
+ return nil;
427
+ }
428
+ return [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
429
+ }
430
+
431
+ + (NSArray *)readJSONArrayAtPath:(NSString *)path {
432
+ id obj = [self readJSONFileAtPath:path];
433
+ return [obj isKindOfClass:[NSArray class]] ? (NSArray *)obj : nil;
434
+ }
435
+
436
+ + (void)atomicWriteJSONObject:(id)obj toPath:(NSString *)path {
437
+ if (obj == nil || path.length == 0) {
438
+ return;
439
+ }
440
+ NSError *error = nil;
441
+ NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:0 error:&error];
442
+ if (data == nil) {
443
+ return;
444
+ }
445
+ [data writeToFile:path atomically:YES];
446
+ }
447
+
448
+ #pragma mark - Normalization
449
+
450
+ /**
451
+ * 将 KSCrash v2 原始报告标准化为协议字典。
452
+ * 字段映射依据 KSCrash JSON schema:
453
+ * raw[@"crash"] -> 错误/线程
454
+ * raw[@"binary_images"] -> 镜像
455
+ * raw[@"system"] -> 设备/系统/应用
456
+ * raw[@"report"][@"id"] -> uuid
457
+ * raw[@"user"] -> 我们注入的 rum_context
458
+ */
459
+ + (NSDictionary *)normalizeKSCrashReport:(NSDictionary *)raw
460
+ reportID:(NSNumber *)reportID {
461
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
462
+
463
+ // 原生层事件去重:过滤已被 JS 层上报的 JS 异常
464
+ // 避免同一错误被 JS ErrorUtils 和原生 NSException 各上报一次
465
+ NSDictionary *earlyCrash = [raw objectForKey:@"crash"];
466
+ if ([earlyCrash isKindOfClass:[NSDictionary class]]) {
467
+ NSDictionary *earlyError = [earlyCrash objectForKey:@"error"];
468
+ if ([earlyError isKindOfClass:[NSDictionary class]]) {
469
+ NSString *reason = earlyError[@"reason"] ?: @"";
470
+ NSString *type = earlyError[@"type"] ?: @"";
471
+ // JS 异常特征:NSException 类型且 reason 包含 JS 桥相关关键词
472
+ // 限定 type 为 nsexception,避免 Mach/Signal 崩溃因 reason 碰巧
473
+ // 包含这些字符串而被误过滤
474
+ if ([type containsString:@"nsexception"] &&
475
+ ([reason containsString:@"ExceptionsManager"] ||
476
+ [reason containsString:@"Unhandled JS Exception"] ||
477
+ [reason containsString:@"RCTFatal"])
478
+ ) {
479
+ return nil; // 跳过,已被 JS 层上报
480
+ }
481
+ }
482
+ }
483
+
484
+ // crash_id
485
+ NSDictionary *reportMeta = [raw objectForKey:@"report"];
486
+ NSString *uuid = nil;
487
+ if ([reportMeta isKindOfClass:[NSDictionary class]]) {
488
+ uuid = [reportMeta objectForKey:@"id"];
489
+ }
490
+ out[@"crash_id"] = uuid.length > 0 ? uuid : [reportID stringValue];
491
+
492
+ // timestamp(毫秒)
493
+ NSNumber *ts = nil;
494
+ if ([reportMeta isKindOfClass:[NSDictionary class]]) {
495
+ id t = reportMeta[@"timestamp"];
496
+ if ([t isKindOfClass:[NSString class]]) {
497
+ NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
498
+ fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
499
+
500
+ NSArray<NSString *> *formats = @[
501
+ @"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
502
+ @"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
503
+ @"yyyy-MM-dd'T'HH:mm:ss'Z'",
504
+ @"yyyy-MM-dd'T'HH:mm:ssXXX"
505
+ ];
506
+
507
+ NSDate *d = nil;
508
+ for (NSString *format in formats) {
509
+ fmt.dateFormat = format;
510
+ d = [fmt dateFromString:t];
511
+ if (d) break;
512
+ }
513
+
514
+ if (d) {
515
+ ts = @((long long)([d timeIntervalSince1970] * 1000));
516
+ } else {
517
+ // 所有格式都失败,降级为当前时间
518
+ ts = @((long long)([[NSDate date] timeIntervalSince1970] * 1000));
519
+ }
520
+ } else if ([t isKindOfClass:[NSNumber class]]) {
521
+ // 已是数值(秒级),转毫秒
522
+ ts = @((long long)([(NSNumber *)t doubleValue] * 1000));
523
+ }
524
+ }
525
+ out[@"timestamp"] = ts ?: @((long long)([[NSDate date] timeIntervalSince1970] * 1000));
526
+
527
+ out[@"platform"] = @"ios";
528
+
529
+ NSDictionary *crash = [raw objectForKey:@"crash"];
530
+ if (![crash isKindOfClass:[NSDictionary class]]) {
531
+ crash = @{};
532
+ }
533
+ NSDictionary *errorInfo = [crash objectForKey:@"error"];
534
+ if (![errorInfo isKindOfClass:[NSDictionary class]]) {
535
+ errorInfo = @{};
536
+ }
537
+
538
+ // signal
539
+ NSMutableDictionary *signal = [NSMutableDictionary dictionary];
540
+ NSDictionary *signalRaw = errorInfo[@"signal"];
541
+ if ([signalRaw isKindOfClass:[NSDictionary class]]) {
542
+ signal[@"name"] = signalRaw[@"name"] ?: @"";
543
+ signal[@"code"] = signalRaw[@"code_name"] ?: signalRaw[@"code"] ?: @"";
544
+ // 信号编号:KSCrash raw report 中信号编号存储在 "signal" 键下,
545
+ // 与 Android NDK 的 signal.number 对齐,供 JS 层读取放入 snapshots。
546
+ signal[@"number"] = signalRaw[@"signal"] ?: @0;
547
+ }
548
+ id addr = errorInfo[@"address"];
549
+ if (addr) {
550
+ signal[@"fault_address"] = [self hexStringFromValue:addr];
551
+ }
552
+ out[@"signal"] = signal;
553
+
554
+ // exception
555
+ NSMutableDictionary *exception = [NSMutableDictionary dictionary];
556
+ NSString *errType = errorInfo[@"type"];
557
+ if (errType.length == 0) {
558
+ errType = @"unknown";
559
+ }
560
+ NSString *normalizedType = errType;
561
+ if ([errType isEqualToString:@"mach"]) {
562
+ normalizedType = @"mach_exception";
563
+ } else if ([errType isEqualToString:@"signal"]) {
564
+ normalizedType = @"signal";
565
+ } else if ([errType isEqualToString:@"cpp_exception"]) {
566
+ normalizedType = @"cpp_exception";
567
+ } else if ([errType isEqualToString:@"nsexception"]) {
568
+ normalizedType = @"ns_exception";
569
+ } else if ([errType isEqualToString:@"user"]) {
570
+ normalizedType = @"user_reported";
571
+ }
572
+ exception[@"type"] = normalizedType;
573
+
574
+ NSDictionary *nsex = errorInfo[@"nsexception"];
575
+ NSDictionary *cppex = errorInfo[@"cpp_exception"];
576
+ if ([nsex isKindOfClass:[NSDictionary class]]) {
577
+ exception[@"class_name"] = nsex[@"name"] ?: @"";
578
+ exception[@"message"] = nsex[@"reason"] ?: errorInfo[@"reason"] ?: @"";
579
+ } else if ([cppex isKindOfClass:[NSDictionary class]]) {
580
+ exception[@"class_name"] = cppex[@"name"] ?: @"";
581
+ exception[@"message"] = cppex[@"reason"] ?: errorInfo[@"reason"] ?: @"";
582
+ } else {
583
+ exception[@"class_name"] = normalizedType;
584
+ exception[@"message"] = errorInfo[@"reason"] ?: @"";
585
+ }
586
+ out[@"exception"] = exception;
587
+
588
+ // mechanism:标记崩溃捕获机制,参照 Sentry 实践
589
+ // JS 层会将此字段放入 snapshots 容器
590
+ NSMutableDictionary *mechanism = [NSMutableDictionary dictionary];
591
+ mechanism[@"type"] = normalizedType;
592
+ mechanism[@"handled"] = @NO;
593
+ out[@"mechanism"] = mechanism;
594
+
595
+ // crashed_thread + registers
596
+ NSArray *threads = crash[@"threads"];
597
+ NSDictionary *crashedThread = nil;
598
+ if ([threads isKindOfClass:[NSArray class]]) {
599
+ for (NSDictionary *t in threads) {
600
+ if (![t isKindOfClass:[NSDictionary class]]) {
601
+ continue;
602
+ }
603
+ if ([t[@"crashed"] boolValue]) {
604
+ crashedThread = t;
605
+ break;
606
+ }
607
+ }
608
+ if (crashedThread == nil && threads.count > 0) {
609
+ crashedThread = threads.firstObject;
610
+ }
611
+ }
612
+ out[@"crashed_thread"] = [self normalizeThread:crashedThread];
613
+ out[@"registers"] = [self normalizeRegisters:crashedThread];
614
+
615
+ // binary_images — 按需过滤:仅保留栈帧实际涉及的镜像(参照 Sentry 的实践)
616
+ // Sentry 在 SentryCrashReportConverter 的 debugMetaForThreads: 中通过遍历线程
617
+ // stacktrace frames 收集被引用的 imageAddress,仅保留这些镜像,减少 90%+ 数据量。
618
+ // 实现:从标准化后的 crashed_thread.frames 收集指令地址,通过地址范围匹配
619
+ // [image_addr, image_addr + image_size) 过滤 raw binary_images 数组(路径 B)。
620
+ NSArray *rawImages = raw[@"binary_images"];
621
+
622
+ // 1. 收集崩溃线程栈帧中的指令地址(已标准化的 hex 字符串),转为数值集合
623
+ NSDictionary *normalizedThread = out[@"crashed_thread"];
624
+ NSArray *normalizedFrames = normalizedThread[@"frames"];
625
+ NSMutableSet *frameAddrs = [NSMutableSet set];
626
+ for (NSDictionary *f in normalizedFrames) {
627
+ if (![f isKindOfClass:[NSDictionary class]]) {
628
+ continue;
629
+ }
630
+ NSString *addrStr = f[@"address"];
631
+ if (![addrStr isKindOfClass:[NSString class]] || addrStr.length == 0) {
632
+ continue;
633
+ }
634
+ unsigned long long addr = strtoull([addrStr UTF8String], NULL, 16);
635
+ if (addr != 0) {
636
+ [frameAddrs addObject:@(addr)];
637
+ }
638
+ }
639
+
640
+ // 2. 过滤 raw binary_images:仅保留地址范围包含至少一个栈帧地址的镜像
641
+ NSMutableArray *filteredImages = [NSMutableArray array];
642
+ if ([rawImages isKindOfClass:[NSArray class]]) {
643
+ for (NSDictionary *img in rawImages) {
644
+ if (![img isKindOfClass:[NSDictionary class]]) {
645
+ continue;
646
+ }
647
+ // 解析 image_addr(hex 字符串或数值)和 image_size(hex 字符串或数值)
648
+ id imgAddrVal = img[@"image_addr"];
649
+ unsigned long long imgAddr = 0;
650
+ if ([imgAddrVal isKindOfClass:[NSString class]]) {
651
+ imgAddr = strtoull([imgAddrVal UTF8String], NULL, 16);
652
+ } else if ([imgAddrVal isKindOfClass:[NSNumber class]]) {
653
+ imgAddr = [(NSNumber *)imgAddrVal unsignedLongLongValue];
654
+ }
655
+ id imgSizeVal = img[@"image_size"];
656
+ unsigned long long imgSize = 0;
657
+ if ([imgSizeVal isKindOfClass:[NSString class]]) {
658
+ // 使用 base 0 自动检测 0x 前缀,兼容 hex 字符串
659
+ imgSize = strtoull([imgSizeVal UTF8String], NULL, 0);
660
+ } else if ([imgSizeVal isKindOfClass:[NSNumber class]]) {
661
+ imgSize = [(NSNumber *)imgSizeVal unsignedLongLongValue];
662
+ }
663
+ if (imgAddr == 0) {
664
+ continue;
665
+ }
666
+ // imgSize 为 0 时无法确定地址范围,保守保留该镜像,避免丢失符号化信息
667
+ if (imgSize == 0) {
668
+ [filteredImages addObject:img];
669
+ continue;
670
+ }
671
+ // 检查是否有栈帧地址落在 [imgAddr, imgAddr + imgSize) 范围内
672
+ // 使用减法比较避免 imgAddr + imgSize 溢出
673
+ BOOL referenced = NO;
674
+ for (NSNumber *frameAddrNum in frameAddrs) {
675
+ unsigned long long fa = [frameAddrNum unsignedLongLongValue];
676
+ if (fa >= imgAddr && (fa - imgAddr) < imgSize) {
677
+ referenced = YES;
678
+ break;
679
+ }
680
+ }
681
+ if (referenced) {
682
+ [filteredImages addObject:img];
683
+ }
684
+ }
685
+ }
686
+
687
+ // 3. fallback:如果过滤结果为空(匹配失败),输出全量镜像,避免丢失符号化信息
688
+ if (filteredImages.count == 0 && [rawImages isKindOfClass:[NSArray class]]) {
689
+ filteredImages = (NSMutableArray *)[rawImages mutableCopy];
690
+ }
691
+
692
+ out[@"binary_images"] = [self normalizeBinaryImages:filteredImages];
693
+
694
+ // context(来自我们注入)
695
+ NSDictionary *user = raw[@"user"];
696
+ NSDictionary *context = nil;
697
+ if ([user isKindOfClass:[NSDictionary class]]) {
698
+ id ctx = user[kRumCrashUserInfoContextKey];
699
+ if ([ctx isKindOfClass:[NSDictionary class]]) {
700
+ context = (NSDictionary *)ctx;
701
+ }
702
+ }
703
+ out[@"context"] = context ?: @{};
704
+
705
+ // device / app
706
+ NSDictionary *system = raw[@"system"];
707
+ if (![system isKindOfClass:[NSDictionary class]]) {
708
+ system = @{};
709
+ }
710
+ out[@"device"] = @{
711
+ @"brand": @"Apple",
712
+ @"model": system[@"machine"] ?: system[@"model"] ?: @"",
713
+ @"os_version": system[@"os_version"] ?: system[@"system_version"] ?: @"",
714
+ };
715
+ NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier] ?: @"";
716
+ out[@"app"] = @{
717
+ @"version": system[@"CFBundleShortVersionString"] ?: @"",
718
+ @"build": system[@"CFBundleVersion"] ?: @"",
719
+ @"rn_version": system[@"react_native_version"] ?: @"",
720
+ @"package": bundleIdentifier,
721
+ };
722
+
723
+ return out;
724
+ }
725
+
726
+ + (NSDictionary *)normalizeThread:(NSDictionary *)thread {
727
+ if (![thread isKindOfClass:[NSDictionary class]]) {
728
+ return @{ @"id": @"", @"name": @"", @"frames": @[] };
729
+ }
730
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
731
+ id idx = thread[@"index"];
732
+ out[@"id"] = idx ? [NSString stringWithFormat:@"%@", idx] : @"";
733
+ out[@"name"] = thread[@"name"] ?: thread[@"dispatch_queue"] ?: @"";
734
+
735
+ NSMutableArray *frames = [NSMutableArray array];
736
+ NSArray *backtrace = thread[@"backtrace"];
737
+ NSArray *contents = nil;
738
+ if ([backtrace isKindOfClass:[NSDictionary class]]) {
739
+ contents = ((NSDictionary *)backtrace)[@"contents"];
740
+ } else if ([backtrace isKindOfClass:[NSArray class]]) {
741
+ contents = (NSArray *)backtrace;
742
+ }
743
+ // 双侧截断(参照 Sentry 推荐方案):帧数超过 512 时保留首尾各 256 帧,
744
+ // 中间插入标记帧,防止递归崩溃(如 StackOverflow 产生数千帧)丢失程序入口点。
745
+ // normalizeThread 不在信号处理器中运行,可安全访问完整帧数组。
746
+ static const NSUInteger kTruncationThreshold = 512;
747
+ static const NSUInteger kKeepPerSide = 256;
748
+ if ([contents isKindOfClass:[NSArray class]]) {
749
+ NSUInteger totalCount = contents.count;
750
+ if (totalCount > kTruncationThreshold) {
751
+ // 前 kKeepPerSide 帧
752
+ for (NSUInteger i = 0; i < kKeepPerSide; i++) {
753
+ NSDictionary *f = contents[i];
754
+ if ([f isKindOfClass:[NSDictionary class]]) {
755
+ [frames addObject:[self normalizeFrame:f]];
756
+ }
757
+ }
758
+ // 截断标记帧
759
+ [frames addObject:@{
760
+ @"truncated": @YES,
761
+ @"omitted_count": @(totalCount - kTruncationThreshold)
762
+ }];
763
+ // 后 kKeepPerSide 帧
764
+ NSUInteger tailStart = totalCount - kKeepPerSide;
765
+ for (NSUInteger i = tailStart; i < totalCount; i++) {
766
+ NSDictionary *f = contents[i];
767
+ if ([f isKindOfClass:[NSDictionary class]]) {
768
+ [frames addObject:[self normalizeFrame:f]];
769
+ }
770
+ }
771
+ } else {
772
+ // 帧数未超阈值,全部保留
773
+ for (NSDictionary *f in contents) {
774
+ if (![f isKindOfClass:[NSDictionary class]]) {
775
+ continue;
776
+ }
777
+ [frames addObject:[self normalizeFrame:f]];
778
+ }
779
+ }
780
+ }
781
+ out[@"frames"] = frames;
782
+ return out;
783
+ }
784
+
785
+ /** 将 KSCrash 原始帧字典标准化为协议帧字典。 */
786
+ + (NSMutableDictionary *)normalizeFrame:(NSDictionary *)f {
787
+ NSMutableDictionary *frame = [NSMutableDictionary dictionary];
788
+ frame[@"address"] = [self hexStringFromValue:f[@"instruction_addr"]];
789
+ frame[@"library"] = f[@"object_name"] ?: @"";
790
+ id objAddr = f[@"object_addr"];
791
+ id insAddr = f[@"instruction_addr"];
792
+ if ([objAddr isKindOfClass:[NSNumber class]]
793
+ && [insAddr isKindOfClass:[NSNumber class]]) {
794
+ long long offset = [insAddr longLongValue] - [objAddr longLongValue];
795
+ frame[@"offset"] = [NSString stringWithFormat:@"0x%llx", offset];
796
+ } else {
797
+ frame[@"offset"] = @"";
798
+ }
799
+ return frame;
800
+ }
801
+
802
+ + (NSDictionary *)normalizeRegisters:(NSDictionary *)thread {
803
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
804
+ out[@"pc"] = @"";
805
+ out[@"lr"] = @"";
806
+ out[@"sp"] = @"";
807
+ out[@"fp"] = @"";
808
+ if (![thread isKindOfClass:[NSDictionary class]]) {
809
+ return out;
810
+ }
811
+ NSDictionary *registers = thread[@"registers"];
812
+ if (![registers isKindOfClass:[NSDictionary class]]) {
813
+ return out;
814
+ }
815
+ NSDictionary *basic = registers[@"basic"];
816
+ if (![basic isKindOfClass:[NSDictionary class]]) {
817
+ basic = registers;
818
+ }
819
+ // arm64 寄存器名
820
+ out[@"pc"] = [self hexStringFromValue:basic[@"pc"]];
821
+ out[@"lr"] = [self hexStringFromValue:basic[@"lr"]];
822
+ out[@"sp"] = [self hexStringFromValue:basic[@"sp"]];
823
+ out[@"fp"] = [self hexStringFromValue:basic[@"fp"] ?: basic[@"x29"]];
824
+ return out;
825
+ }
826
+
827
+ + (NSArray *)normalizeBinaryImages:(NSArray *)images {
828
+ NSMutableArray *out = [NSMutableArray array];
829
+ if (![images isKindOfClass:[NSArray class]]) {
830
+ return out;
831
+ }
832
+ for (NSDictionary *img in images) {
833
+ if (![img isKindOfClass:[NSDictionary class]]) {
834
+ continue;
835
+ }
836
+ [out addObject:@{
837
+ @"name": img[@"name"] ?: @"",
838
+ @"uuid": img[@"uuid"] ?: @"",
839
+ @"image_addr": [self hexStringFromValue:img[@"image_addr"]],
840
+ @"image_size": img[@"image_size"] ?: @0,
841
+ }];
842
+ }
843
+ return out;
844
+ }
845
+
846
+ + (NSString *)hexStringFromValue:(id)value {
847
+ if ([value isKindOfClass:[NSString class]]) {
848
+ NSString *s = (NSString *)value;
849
+ if ([s hasPrefix:@"0x"] || [s hasPrefix:@"0X"]) {
850
+ return s;
851
+ }
852
+ return s;
853
+ }
854
+ if ([value isKindOfClass:[NSNumber class]]) {
855
+ unsigned long long v = [(NSNumber *)value unsignedLongLongValue];
856
+ return [NSString stringWithFormat:@"0x%llx", v];
857
+ }
858
+ return @"";
859
+ }
860
+
861
+ @end