@arms/rum-reactnative 0.1.3-beta.5 → 0.1.4-beta.2
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 +9 -2
- package/android/build.gradle +12 -1
- package/android/gradle.properties +3 -1
- package/android/src/main/AndroidManifest.xml +4 -1
- package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativeModule.kt +67 -14
- package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativePackage.kt +9 -2
- package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +25 -0
- package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +20 -0
- package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +2 -1
- package/ios/RumAppStateRecorder.h +69 -0
- package/ios/RumAppStateRecorder.mm +547 -0
- package/ios/RumCrashReporter.h +32 -0
- package/ios/RumCrashReporter.mm +97 -11
- package/ios/RumSdkReactnative.h +11 -1
- package/ios/RumSdkReactnative.mm +115 -29
- package/lib/commonjs/NativeRumSdkReactnative.js +1 -54
- package/lib/commonjs/collector/action/index.js +1 -108
- package/lib/commonjs/collector/exception/crash.js +1 -148
- package/lib/commonjs/collector/exception/index.js +1 -238
- package/lib/commonjs/collector/exception/oom.js +1 -0
- package/lib/commonjs/collector/index.js +1 -42
- package/lib/commonjs/collector/resource/api.js +1 -292
- package/lib/commonjs/collector/view/perf.js +1 -79
- package/lib/commonjs/collector/view/pv.js +1 -88
- package/lib/commonjs/expoConfigPlugin.js +1 -37
- package/lib/commonjs/index.js +1 -29
- package/lib/commonjs/plugin.js +1 -47
- package/lib/commonjs/processor/default-processor.js +1 -27
- package/lib/commonjs/processor/session-processor.js +1 -47
- package/lib/commonjs/reporter/index.js +1 -148
- package/lib/commonjs/reporter/monitor-reporter.js +1 -43
- package/lib/commonjs/shell.js +1 -91
- package/lib/commonjs/types/client.js +1 -6
- package/lib/commonjs/types/crash.js +1 -37
- package/lib/commonjs/types/index.js +1 -2
- package/lib/commonjs/types/oom.js +1 -0
- package/lib/commonjs/utils/action.js +1 -305
- package/lib/commonjs/utils/api.js +1 -84
- package/lib/commonjs/utils/base.js +1 -95
- package/lib/commonjs/utils/crash.js +1 -149
- package/lib/commonjs/utils/platform.js +1 -162
- package/lib/commonjs/utils/session.js +1 -158
- package/lib/commonjs/utils/url.js +1 -59
- package/lib/commonjs/utils/view.js +1 -93
- package/lib/module/NativeRumSdkReactnative.js +1 -51
- package/lib/module/collector/action/index.js +1 -103
- package/lib/module/collector/exception/crash.js +1 -143
- package/lib/module/collector/exception/index.js +1 -233
- package/lib/module/collector/exception/oom.js +1 -0
- package/lib/module/collector/index.js +1 -8
- package/lib/module/collector/resource/api.js +1 -287
- package/lib/module/collector/view/perf.js +1 -79
- package/lib/module/collector/view/pv.js +1 -83
- package/lib/module/expoConfigPlugin.js +1 -33
- package/lib/module/index.js +1 -7
- package/lib/module/plugin.js +1 -42
- package/lib/module/processor/default-processor.js +1 -22
- package/lib/module/processor/session-processor.js +1 -42
- package/lib/module/reporter/index.js +1 -144
- package/lib/module/reporter/monitor-reporter.js +1 -38
- package/lib/module/shell.js +1 -84
- package/lib/module/types/client.js +1 -4
- package/lib/module/types/crash.js +1 -33
- package/lib/module/types/index.js +1 -2
- package/lib/module/types/oom.js +1 -0
- package/lib/module/utils/action.js +1 -299
- package/lib/module/utils/api.js +1 -76
- package/lib/module/utils/base.js +1 -90
- package/lib/module/utils/crash.js +1 -144
- package/lib/module/utils/platform.js +1 -150
- package/lib/module/utils/session.js +1 -154
- package/lib/module/utils/url.js +1 -54
- package/lib/module/utils/view.js +1 -88
- package/lib/typescript/commonjs/src/NativeRumSdkReactnative.d.ts +10 -0
- package/lib/typescript/commonjs/src/collector/exception/oom.d.ts +34 -0
- package/lib/typescript/commonjs/src/collector/index.d.ts +1 -0
- package/lib/typescript/commonjs/src/shell.d.ts +1 -1
- package/lib/typescript/commonjs/src/types/client.d.ts +27 -0
- package/lib/typescript/commonjs/src/types/oom.d.ts +58 -0
- package/lib/typescript/commonjs/src/utils/crash.d.ts +13 -0
- package/lib/typescript/module/src/NativeRumSdkReactnative.d.ts +10 -0
- package/lib/typescript/module/src/collector/exception/oom.d.ts +34 -0
- package/lib/typescript/module/src/collector/index.d.ts +1 -0
- package/lib/typescript/module/src/shell.d.ts +1 -1
- package/lib/typescript/module/src/types/client.d.ts +27 -0
- package/lib/typescript/module/src/types/oom.d.ts +58 -0
- package/lib/typescript/module/src/utils/crash.d.ts +13 -0
- package/package.json +1 -2
- package/src/NativeRumSdkReactnative.ts +18 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#import "RumAppStateRecorder.h"
|
|
2
|
+
#import "RumCrashReporter.h"
|
|
3
|
+
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
5
|
+
#import <sys/sysctl.h>
|
|
6
|
+
#import <sys/utsname.h>
|
|
7
|
+
|
|
8
|
+
#pragma mark - Constants
|
|
9
|
+
|
|
10
|
+
/** 状态文件与 OOM 事件文件命名(与 context.json / crashed_last_run.flag 同目录) */
|
|
11
|
+
static NSString *const kRumAppStateFileName = @"app_state.json";
|
|
12
|
+
static NSString *const kRumOomFilePrefix = @"oom_";
|
|
13
|
+
static NSString *const kRumOomFileSuffix = @".json";
|
|
14
|
+
|
|
15
|
+
/** OOM 事件固定文案(D8:英文证据式表述,推断性质由 JS 侧 snapshots.estimated 承载) */
|
|
16
|
+
static NSString *const kRumOomEventMessage =
|
|
17
|
+
@"App was killed while active with no crash report (suspected watchdog termination)";
|
|
18
|
+
|
|
19
|
+
/** OOM 事件 context 字段仅透传 crash context 的四个关联键(排除开关等其他键) */
|
|
20
|
+
static NSArray<NSString *> *const kRumOomContextKeys =
|
|
21
|
+
@[ @"session_id", @"view_id", @"view_name", @"user_id" ];
|
|
22
|
+
|
|
23
|
+
#pragma mark - Helpers
|
|
24
|
+
|
|
25
|
+
/** nil 安全的字符串相等比较(任一非字符串视为不等,保守不判定) */
|
|
26
|
+
static BOOL RumStringEqual(id a, id b) {
|
|
27
|
+
if (![a isKindOfClass:[NSString class]] || ![b isKindOfClass:[NSString class]]) {
|
|
28
|
+
return NO;
|
|
29
|
+
}
|
|
30
|
+
return [(NSString *)a isEqualToString:(NSString *)b];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 从字典安全取布尔值(非 NSNumber 视为 NO) */
|
|
34
|
+
static BOOL RumBoolValue(NSDictionary *dict, NSString *key) {
|
|
35
|
+
id value = dict[key];
|
|
36
|
+
return [value isKindOfClass:[NSNumber class]] ? [(NSNumber *)value boolValue] : NO;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@implementation RumAppStateRecorder {
|
|
40
|
+
/** 上次持久化的状态快照(内存缓存,仅状态变化时写盘;仅在主线程访问——
|
|
41
|
+
生命周期通知在主线程、bootstrap 的 start 经主队列 async 收敛,见 bootstrap) */
|
|
42
|
+
NSDictionary *_lastPersistedState;
|
|
43
|
+
/** 生命周期通知是否已订阅(防重复) */
|
|
44
|
+
BOOL _observingLifecycle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#pragma mark - Setup(判定 + 启动)
|
|
48
|
+
|
|
49
|
+
+ (void)setup {
|
|
50
|
+
static dispatch_once_t onceToken;
|
|
51
|
+
dispatch_once(&onceToken, ^{
|
|
52
|
+
[[self sharedRecorder] bootstrap];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
- (void)bootstrap {
|
|
57
|
+
@try {
|
|
58
|
+
// 0. crash context 快照:OOM 事件 context 字段的恢复来源。读的是上次会话
|
|
59
|
+
// 落盘的 context.json——bridge 队列 FIFO 保证本次会话的 updateCrashContext
|
|
60
|
+
// 尚未执行;缺失时事件 context 为空对象,事件仍落盘。开关门控不在本层:
|
|
61
|
+
// setup 由 RumSdkReactnative.updateCollectorsConfig 驱动(JS 侧仅在
|
|
62
|
+
// collectors.oom === true 时调用),未开启时本方法不会被调用。
|
|
63
|
+
NSDictionary *context = [RumCrashReporter currentCrashContext];
|
|
64
|
+
|
|
65
|
+
// 1. 上次快照与文件 mtime(timestamp 估算依据,必须在覆写本次状态之前读取)
|
|
66
|
+
NSString *statePath = [self appStatePath];
|
|
67
|
+
NSDictionary *previousState = [[self class] readStateJSONFileAtPath:statePath];
|
|
68
|
+
long long stateMtimeMs = [self fileMtimeMsAtPath:statePath];
|
|
69
|
+
long long contextMtimeMs = [self fileMtimeMsAtPath:[RumCrashReporter contextPath]];
|
|
70
|
+
|
|
71
|
+
// 2. 采集本次环境快照(appVersion / osVersion / systemBootTime / vendorId 比对基准)
|
|
72
|
+
NSDictionary *currentState = [self collectCurrentState];
|
|
73
|
+
|
|
74
|
+
// 3. 崩溃证据检查(判定顺序:先完成 pending crash 检查,再执行 OOM 判定——
|
|
75
|
+
// 方案 4.4 去重原则第 2 条;flag 与 pending 报告任一存在即让位 crash 链路,D7)
|
|
76
|
+
BOOL flagExists = [RumCrashReporter crashedLastRunFlagExists];
|
|
77
|
+
// 条件⑩时间范围限定:pending 报告仅当最新时间戳晚于上次快照 updatedAt(上次
|
|
78
|
+
// 运行最后一次状态写入)时视为"上次运行内的崩溃"而抑制判定;早于该时间的
|
|
79
|
+
// 陈旧报告(离线堆积、crash 关闭时遗留——crash 关闭时 KSCrash 报告只写
|
|
80
|
+
// 不删,唯一清理入口在 CrashCollector.setup 链内)不抑制。基准不可得时
|
|
81
|
+
// 退化为原语义(任一 pending 报告即抑制,保守兜底)。
|
|
82
|
+
long long lastStateUpdatedAtMs = 0;
|
|
83
|
+
id lastUpdatedAt = previousState[@"updatedAt"];
|
|
84
|
+
if ([lastUpdatedAt isKindOfClass:[NSNumber class]]) {
|
|
85
|
+
lastStateUpdatedAtMs = [(NSNumber *)lastUpdatedAt longLongValue];
|
|
86
|
+
}
|
|
87
|
+
BOOL hasPendingReports = lastStateUpdatedAtMs > 0
|
|
88
|
+
? [RumCrashReporter hasPendingCrashReportsAfterEpochMs:lastStateUpdatedAtMs]
|
|
89
|
+
: [RumCrashReporter hasPendingCrashReports];
|
|
90
|
+
|
|
91
|
+
// 4. 10 项排除条件判定(②的模拟器标志由编译宏求值,参数化便于测试伪造)
|
|
92
|
+
#if TARGET_OS_SIMULATOR
|
|
93
|
+
BOOL isSimulator = YES;
|
|
94
|
+
#else
|
|
95
|
+
BOOL isSimulator = NO;
|
|
96
|
+
#endif
|
|
97
|
+
BOOL isWatchdogTermination =
|
|
98
|
+
[[self class] evaluateWatchdogTerminationFromPreviousState:previousState
|
|
99
|
+
currentState:currentState
|
|
100
|
+
crashedLastRunFlagExists:flagExists
|
|
101
|
+
hasPendingCrashReports:hasPendingReports
|
|
102
|
+
runningOnSimulator:isSimulator];
|
|
103
|
+
|
|
104
|
+
// 5. 判定通过 → 构造事件落盘 oom_{timestamp}_{uuid}.json(原生落盘、JS 回捞,
|
|
105
|
+
// 每次启动最多 1 条事件)
|
|
106
|
+
if (isWatchdogTermination) {
|
|
107
|
+
[self writeOomEventWithPreviousState:previousState
|
|
108
|
+
crashContext:context
|
|
109
|
+
stateFileMtimeMs:stateMtimeMs
|
|
110
|
+
contextFileMtimeMs:contextMtimeMs];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 6. 覆写本次会话的 app_state.json 并启动 Recorder(含 4.2.4 补丁)。
|
|
114
|
+
// 不清除 crashed_last_run.flag:flag 生命周期由 install 全权管理——install
|
|
115
|
+
// 在同一进程内必然先于 bootstrap(updateCollectorsConfig 驱动)执行,判定
|
|
116
|
+
// 读到的恒为本启动 install 刚按 KSCrash.crashedLastLaunch 重写/删除的新鲜
|
|
117
|
+
// 值,不存在"上上次崩溃残留到判定时刻"的路径(T-4 复核结论);反之此处
|
|
118
|
+
// 清除会让 crashedLastRun 回退 KSCrash 状态,破坏 JS 侧查询契约。
|
|
119
|
+
// 状态写入与 _lastPersistedState 读写统一收敛到主队列:生命周期通知在主
|
|
120
|
+
// 线程读写该缓存,而 bootstrap 在 TurboModule method queue 执行,跨线程
|
|
121
|
+
// 无同步会引入竞争。此处 async 派发(避免 dispatch_sync 主线程的敏感
|
|
122
|
+
// 模式);判定读上次快照与写事件文件本身留在当前队列,无共享状态。
|
|
123
|
+
if ([NSThread isMainThread]) {
|
|
124
|
+
[self start];
|
|
125
|
+
} else {
|
|
126
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
127
|
+
@try {
|
|
128
|
+
[self start];
|
|
129
|
+
} @catch (NSException *e) {
|
|
130
|
+
NSLog(@"[RumAppStateRecorder] start exception: %@", e);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
} @catch (NSException *e) {
|
|
135
|
+
NSLog(@"[RumAppStateRecorder] setup exception: %@", e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#pragma mark - 判定逻辑(RumWatchdogTerminationLogic,纯函数)
|
|
140
|
+
|
|
141
|
+
+ (BOOL)evaluateWatchdogTerminationFromPreviousState:(nullable NSDictionary *)previousState
|
|
142
|
+
currentState:(NSDictionary *)currentState
|
|
143
|
+
crashedLastRunFlagExists:(BOOL)flagExists
|
|
144
|
+
hasPendingCrashReports:(BOOL)hasPendingReports
|
|
145
|
+
runningOnSimulator:(BOOL)isSimulator
|
|
146
|
+
{
|
|
147
|
+
// ① 存在上次快照(首次安装 / RN 初始化前的早期启动被杀 → 漏报,接受)
|
|
148
|
+
if (![previousState isKindOfClass:[NSDictionary class]]) {
|
|
149
|
+
return NO;
|
|
150
|
+
}
|
|
151
|
+
if (![currentState isKindOfClass:[NSDictionary class]]) {
|
|
152
|
+
return NO;
|
|
153
|
+
}
|
|
154
|
+
// ② 非模拟器(模拟器频繁启停误报,TARGET_OS_SIMULATOR 编译宏)
|
|
155
|
+
if (isSimulator) {
|
|
156
|
+
return NO;
|
|
157
|
+
}
|
|
158
|
+
// ③ appVersion 未变(App 升级覆盖排除)
|
|
159
|
+
if (!RumStringEqual(previousState[@"appVersion"], currentState[@"appVersion"])) {
|
|
160
|
+
return NO;
|
|
161
|
+
}
|
|
162
|
+
// ④ osVersion 未变(系统 OTA 升级排除)
|
|
163
|
+
if (!RumStringEqual(previousState[@"osVersion"], currentState[@"osVersion"])) {
|
|
164
|
+
return NO;
|
|
165
|
+
}
|
|
166
|
+
// ⑤ systemBootTime 未变(设备重启/关机排除)
|
|
167
|
+
if (!RumStringEqual(previousState[@"systemBootTime"], currentState[@"systemBootTime"])) {
|
|
168
|
+
return NO;
|
|
169
|
+
}
|
|
170
|
+
// ⑥ vendorId 一致且非空(重装/换包排除;空值不可信,保守不判定)
|
|
171
|
+
if (!RumStringEqual(previousState[@"vendorId"], currentState[@"vendorId"])) {
|
|
172
|
+
return NO;
|
|
173
|
+
}
|
|
174
|
+
if ([currentState[@"vendorId"] length] == 0) {
|
|
175
|
+
return NO;
|
|
176
|
+
}
|
|
177
|
+
// ⑦ 上次未附加调试器(Xcode 重启进程排除)
|
|
178
|
+
if (RumBoolValue(previousState, @"isDebugging")) {
|
|
179
|
+
return NO;
|
|
180
|
+
}
|
|
181
|
+
// ⑧ 上次非正常终止(用户上滑强杀/系统正常终止排除)
|
|
182
|
+
if (RumBoolValue(previousState, @"wasTerminated")) {
|
|
183
|
+
return NO;
|
|
184
|
+
}
|
|
185
|
+
// ⑨ 上次前台激活(D5:后台被杀不判定、不上报)
|
|
186
|
+
if (!RumBoolValue(previousState, @"isActive")) {
|
|
187
|
+
return NO;
|
|
188
|
+
}
|
|
189
|
+
// ⑩ 上次运行内没有产生崩溃(flag 与 KSCrash pending 报告任一存在即让位 crash
|
|
190
|
+
// 链路,D7 判定层互斥——flag 覆盖 KSCrash 状态被 JS 清理重置的边界,pending
|
|
191
|
+
// 报告覆盖崩溃发生但 flag 未及写入的场景;调用方对 pending 报告施加时间范围
|
|
192
|
+
// 限定,仅"上次运行内"的崩溃才计入,陈旧报告不抑制)
|
|
193
|
+
if (flagExists || hasPendingReports) {
|
|
194
|
+
return NO;
|
|
195
|
+
}
|
|
196
|
+
return YES;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#pragma mark - 状态采集
|
|
200
|
+
|
|
201
|
+
/** 采集当前环境快照(8 字段,方案 4.2.1) */
|
|
202
|
+
- (NSDictionary *)collectCurrentState {
|
|
203
|
+
NSBundle *bundle = [NSBundle mainBundle];
|
|
204
|
+
NSString *shortVersion =
|
|
205
|
+
[bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"";
|
|
206
|
+
NSString *buildNumber = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"";
|
|
207
|
+
NSString *appVersion = shortVersion.length > 0
|
|
208
|
+
? [NSString stringWithFormat:@"%@ (%@)", shortVersion, buildNumber]
|
|
209
|
+
: buildNumber;
|
|
210
|
+
|
|
211
|
+
UIDevice *device = [UIDevice currentDevice];
|
|
212
|
+
|
|
213
|
+
return @{
|
|
214
|
+
@"appVersion": appVersion,
|
|
215
|
+
@"osVersion": device.systemVersion ?: @"",
|
|
216
|
+
@"systemBootTime": [self currentSystemBootTimeISO8601],
|
|
217
|
+
@"vendorId": device.identifierForVendor.UUIDString ?: @"",
|
|
218
|
+
// 4.2.4 混合 SDK 适配补丁:主动查询当前 applicationState 补记激活状态——
|
|
219
|
+
// RN 延迟初始化会错过系统 DidBecomeActive 通知,而 isActive 恰是判定条件⑨
|
|
220
|
+
// 的依据,缺失会导致判定全错(Sentry #2326 同款问题的更直接解法)
|
|
221
|
+
@"isActive": @([self currentApplicationState] == UIApplicationStateActive),
|
|
222
|
+
@"wasTerminated": @NO,
|
|
223
|
+
@"isDebugging": @([self isDebuggerAttached]),
|
|
224
|
+
@"updatedAt": @((long long)([[NSDate date] timeIntervalSince1970] * 1000)),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* systemBootTime:NSProcessInfo.systemUptime 换算为系统启动时刻的 ISO8601(UTC,毫秒)。
|
|
230
|
+
* 已知局限(对齐竞品同款换算):systemUptime 不含睡眠时间,跨睡眠后本次换算值与
|
|
231
|
+
* 上次快照不一致 → 不判定(保守方向的漏报,可接受)。
|
|
232
|
+
*/
|
|
233
|
+
- (NSString *)currentSystemBootTimeISO8601 {
|
|
234
|
+
NSTimeInterval uptime = [NSProcessInfo processInfo].systemUptime;
|
|
235
|
+
NSDate *bootDate = [NSDate dateWithTimeIntervalSinceNow:-uptime];
|
|
236
|
+
static NSDateFormatter *formatter = nil;
|
|
237
|
+
static dispatch_once_t onceToken;
|
|
238
|
+
dispatch_once(&onceToken, ^{
|
|
239
|
+
formatter = [[NSDateFormatter alloc] init];
|
|
240
|
+
formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
|
241
|
+
formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
|
242
|
+
formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
|
243
|
+
});
|
|
244
|
+
return [formatter stringFromDate:bootDate] ?: @"";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** sysctl 查 P_TRACED:当前进程是否附加了调试器(Apple QA1361 标准做法) */
|
|
248
|
+
- (BOOL)isDebuggerAttached {
|
|
249
|
+
int name[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
|
|
250
|
+
struct kinfo_proc info;
|
|
251
|
+
memset(&info, 0, sizeof(info));
|
|
252
|
+
size_t size = sizeof(info);
|
|
253
|
+
if (sysctl(name, 4, &info, &size, NULL, 0) != 0) {
|
|
254
|
+
return NO;
|
|
255
|
+
}
|
|
256
|
+
// Darwin 的 kinfo_proc 字段为 kp_proc(FreeBSD 为 ki_proc,勿混淆)
|
|
257
|
+
return (info.kp_proc.p_flag & P_TRACED) != 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 当前 applicationState(UIApplication 属性要求主线程访问:主线程直读,否则同步切主线程) */
|
|
261
|
+
- (UIApplicationState)currentApplicationState {
|
|
262
|
+
if ([NSThread isMainThread]) {
|
|
263
|
+
return [UIApplication sharedApplication].applicationState;
|
|
264
|
+
}
|
|
265
|
+
__block UIApplicationState state = UIApplicationStateInactive;
|
|
266
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
267
|
+
state = [UIApplication sharedApplication].applicationState;
|
|
268
|
+
});
|
|
269
|
+
return state;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#pragma mark - 事件构造与落盘
|
|
273
|
+
|
|
274
|
+
- (void)writeOomEventWithPreviousState:(NSDictionary *)previousState
|
|
275
|
+
crashContext:(NSDictionary *)crashContext
|
|
276
|
+
stateFileMtimeMs:(long long)stateMtimeMs
|
|
277
|
+
contextFileMtimeMs:(long long)contextMtimeMs
|
|
278
|
+
{
|
|
279
|
+
// timestamp 估算:上次会话最后写入的持久化文件 mtime(Datadog
|
|
280
|
+
// mostRecentModifiedFileAt 思路,方案 4.2.3),代表"进程最后存活的已知时刻";
|
|
281
|
+
// 均不可得时退化为当前时间
|
|
282
|
+
long long timestamp = MAX(stateMtimeMs, contextMtimeMs);
|
|
283
|
+
if (timestamp <= 0) {
|
|
284
|
+
timestamp = (long long)([[NSDate date] timeIntervalSince1970] * 1000);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
NSString *oomId = [[NSUUID UUID] UUIDString];
|
|
288
|
+
|
|
289
|
+
// context 恢复:从 context.json 仅提取四个关联键(缺失时为空对象,事件仍落盘,
|
|
290
|
+
// session_id 为空交由 rum-core dispatchEvents 处理,与 NDK context 断裂兜底一致)
|
|
291
|
+
NSMutableDictionary *eventContext = [NSMutableDictionary dictionary];
|
|
292
|
+
for (NSString *key in kRumOomContextKeys) {
|
|
293
|
+
id value = crashContext[key];
|
|
294
|
+
if ([value isKindOfClass:[NSString class]]) {
|
|
295
|
+
eventContext[key] = value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// app / device:参照现有崩溃报告 normalizeKSCrashReport 的构造方式
|
|
300
|
+
struct utsname systemInfo;
|
|
301
|
+
uname(&systemInfo);
|
|
302
|
+
NSString *machine = [NSString stringWithCString:systemInfo.machine
|
|
303
|
+
encoding:NSUTF8StringEncoding] ?: @"";
|
|
304
|
+
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier] ?: @"";
|
|
305
|
+
NSString *shortVersion = [[NSBundle mainBundle]
|
|
306
|
+
objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"";
|
|
307
|
+
|
|
308
|
+
// 事件结构:与 JS 侧契约逐字一致(oom_id / timestamp / platform / subtype / name /
|
|
309
|
+
// message / mechanism / app_state / context / app / device)
|
|
310
|
+
NSDictionary *event = @{
|
|
311
|
+
@"oom_id": oomId,
|
|
312
|
+
@"timestamp": @(timestamp),
|
|
313
|
+
@"platform": @"ios",
|
|
314
|
+
@"subtype": @"oom",
|
|
315
|
+
@"name": @"WatchdogTermination",
|
|
316
|
+
@"message": kRumOomEventMessage,
|
|
317
|
+
@"mechanism": @{ @"type": @"watchdog_termination", @"handled": @NO },
|
|
318
|
+
@"app_state": previousState ?: @{},
|
|
319
|
+
@"context": eventContext,
|
|
320
|
+
@"app": @{ @"version": shortVersion, @"package": bundleId },
|
|
321
|
+
@"device": @{ @"model": machine, @"brand": @"apple" },
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// 落盘 oom_{timestamp}_{uuid}.json(oom_id 与文件名 uuid 同源;删除时按内容匹配)
|
|
325
|
+
NSString *fileName = [NSString stringWithFormat:@"oom_%lld_%@.json", timestamp, oomId];
|
|
326
|
+
NSString *path = [[RumCrashReporter crashDir] stringByAppendingPathComponent:fileName];
|
|
327
|
+
[RumCrashReporter atomicWriteJSONObject:event toPath:path];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#pragma mark - Recorder 启动与生命周期通知
|
|
331
|
+
|
|
332
|
+
- (void)start {
|
|
333
|
+
// start 时同步写一次快照(无条件覆写:上次快照已被判定消费,本次会话从此刻记录)
|
|
334
|
+
[self persistState:[self collectCurrentState]];
|
|
335
|
+
[self startObservingLifecycle];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
- (void)startObservingLifecycle {
|
|
339
|
+
if (_observingLifecycle) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
_observingLifecycle = YES;
|
|
343
|
+
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
344
|
+
[nc addObserver:self
|
|
345
|
+
selector:@selector(handleDidBecomeActive:)
|
|
346
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
347
|
+
object:nil];
|
|
348
|
+
[nc addObserver:self
|
|
349
|
+
selector:@selector(handleWillResignActive:)
|
|
350
|
+
name:UIApplicationWillResignActiveNotification
|
|
351
|
+
object:nil];
|
|
352
|
+
[nc addObserver:self
|
|
353
|
+
selector:@selector(handleDidEnterBackground:)
|
|
354
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
355
|
+
object:nil];
|
|
356
|
+
[nc addObserver:self
|
|
357
|
+
selector:@selector(handleWillEnterForeground:)
|
|
358
|
+
name:UIApplicationWillEnterForegroundNotification
|
|
359
|
+
object:nil];
|
|
360
|
+
[nc addObserver:self
|
|
361
|
+
selector:@selector(handleWillTerminate:)
|
|
362
|
+
name:UIApplicationWillTerminateNotification
|
|
363
|
+
object:nil];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
- (void)handleDidBecomeActive:(NSNotification *)notification {
|
|
367
|
+
[self updateStateWithBlock:^(NSMutableDictionary *state) {
|
|
368
|
+
state[@"isActive"] = @YES;
|
|
369
|
+
}];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
- (void)handleWillResignActive:(NSNotification *)notification {
|
|
373
|
+
[self updateStateWithBlock:^(NSMutableDictionary *state) {
|
|
374
|
+
state[@"isActive"] = @NO;
|
|
375
|
+
}];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
- (void)handleDidEnterBackground:(NSNotification *)notification {
|
|
379
|
+
[self updateStateWithBlock:^(NSMutableDictionary *state) {
|
|
380
|
+
state[@"isActive"] = @NO;
|
|
381
|
+
}];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
- (void)handleWillEnterForeground:(NSNotification *)notification {
|
|
385
|
+
// 前台过渡期 app 尚未 active(inactive 态),等 DidBecomeActive 再置 YES;
|
|
386
|
+
// 过渡窗口内被杀则不判定(前后台切换中归因不可靠)
|
|
387
|
+
[self updateStateWithBlock:^(NSMutableDictionary *state) {
|
|
388
|
+
state[@"isActive"] = @NO;
|
|
389
|
+
}];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
- (void)handleWillTerminate:(NSNotification *)notification {
|
|
393
|
+
// 用户上滑强杀 / 系统正常终止:置 wasTerminated=true 后落盘(判定条件⑧的排除依据)
|
|
394
|
+
[self updateStateWithBlock:^(NSMutableDictionary *state) {
|
|
395
|
+
state[@"wasTerminated"] = @YES;
|
|
396
|
+
}];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 通知驱动的状态变更:刷新 updatedAt 与 isDebugging 后仅在与上次持久化快照语义不同才写盘 */
|
|
400
|
+
- (void)updateStateWithBlock:(void (^)(NSMutableDictionary *state))block {
|
|
401
|
+
NSMutableDictionary *state = [_lastPersistedState mutableCopy];
|
|
402
|
+
if (state == nil) {
|
|
403
|
+
// 内存缓存缺失(异常路径):重新采集,避免丢字段
|
|
404
|
+
state = [[self collectCurrentState] mutableCopy];
|
|
405
|
+
}
|
|
406
|
+
if (state == nil) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
block(state);
|
|
410
|
+
// 重采样 isDebugging:会话启动时仅采样一次,会话中途 attach 调试器若不刷新,
|
|
411
|
+
// 判定条件⑦会在下次启动误排除(sysctl 查 P_TRACED 开销极低,Sentry/Datadog
|
|
412
|
+
// 同款做法)
|
|
413
|
+
state[@"isDebugging"] = @([self isDebuggerAttached]);
|
|
414
|
+
state[@"updatedAt"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000));
|
|
415
|
+
[self persistStateIfChanged:state];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
- (void)persistState:(NSDictionary *)state {
|
|
419
|
+
_lastPersistedState = [state copy];
|
|
420
|
+
[RumCrashReporter atomicWriteJSONObject:state toPath:[self appStatePath]];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
- (void)persistStateIfChanged:(NSDictionary *)state {
|
|
424
|
+
// 写入开销控制:仅状态变化时写(updatedAt 只是写入时间戳,不计入语义比对)
|
|
425
|
+
if ([self isStateSemanticallyEqualToLastPersisted:state]) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
[self persistState:state];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
- (BOOL)isStateSemanticallyEqualToLastPersisted:(NSDictionary *)state {
|
|
432
|
+
if (_lastPersistedState.count != state.count) {
|
|
433
|
+
return NO;
|
|
434
|
+
}
|
|
435
|
+
NSMutableDictionary *last = [_lastPersistedState mutableCopy];
|
|
436
|
+
NSMutableDictionary *next = [state mutableCopy];
|
|
437
|
+
[last removeObjectForKey:@"updatedAt"];
|
|
438
|
+
[next removeObjectForKey:@"updatedAt"];
|
|
439
|
+
return [last isEqualToDictionary:next];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#pragma mark - Pending OOM reports(供 JS 回捞,T-8 逐文件处理)
|
|
443
|
+
|
|
444
|
+
+ (NSArray<NSDictionary *> *)pendingOomReports {
|
|
445
|
+
NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
|
|
446
|
+
@try {
|
|
447
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
448
|
+
NSString *dir = [RumCrashReporter crashDir];
|
|
449
|
+
NSArray<NSString *> *files = [fm contentsOfDirectoryAtPath:dir error:nil] ?: @[];
|
|
450
|
+
for (NSString *file in files) {
|
|
451
|
+
// 文件过滤器限定 oom_ 前缀 + .json 后缀,与 context.json /
|
|
452
|
+
// app_state.json / crashed_last_run.flag 互不干扰
|
|
453
|
+
if (![file hasPrefix:kRumOomFilePrefix] || ![file hasSuffix:kRumOomFileSuffix]) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
@autoreleasepool {
|
|
457
|
+
NSString *path = [dir stringByAppendingPathComponent:file];
|
|
458
|
+
NSDictionary *event = [[self class] readStateJSONFileAtPath:path];
|
|
459
|
+
if ([event isKindOfClass:[NSDictionary class]]) {
|
|
460
|
+
[result addObject:event];
|
|
461
|
+
} else {
|
|
462
|
+
// 损坏文件:删除防止每次启动重复尝试解析(OOM 文件每次启动最多
|
|
463
|
+
// 新增 1 个、ack 后删除,天然不堆积,无需独立 cleanup——方案 R3)
|
|
464
|
+
[fm removeItemAtPath:path error:nil];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} @catch (NSException *e) {
|
|
469
|
+
NSLog(@"[RumAppStateRecorder] pendingOomReports exception: %@", e);
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
+ (void)clearOomReportsWithIds:(NSArray<NSString *> *)oomIds {
|
|
475
|
+
if (oomIds.count == 0) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
NSMutableSet<NSString *> *idSet = [NSMutableSet set];
|
|
479
|
+
for (NSString *oomId in oomIds) {
|
|
480
|
+
if ([oomId isKindOfClass:[NSString class]] && oomId.length > 0) {
|
|
481
|
+
[idSet addObject:oomId];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (idSet.count == 0) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
@try {
|
|
488
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
489
|
+
NSString *dir = [RumCrashReporter crashDir];
|
|
490
|
+
NSArray<NSString *> *files = [fm contentsOfDirectoryAtPath:dir error:nil] ?: @[];
|
|
491
|
+
for (NSString *file in files) {
|
|
492
|
+
if (![file hasPrefix:kRumOomFilePrefix] || ![file hasSuffix:kRumOomFileSuffix]) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
@autoreleasepool {
|
|
496
|
+
// 按文件内容中的 oom_id 匹配删除(与 clearCrashReportsWithIds
|
|
497
|
+
// 遍历读 report.id 匹配的模式一致,不依赖文件名解析)
|
|
498
|
+
NSString *path = [dir stringByAppendingPathComponent:file];
|
|
499
|
+
NSDictionary *event = [[self class] readStateJSONFileAtPath:path];
|
|
500
|
+
NSString *oomId = [event isKindOfClass:[NSDictionary class]]
|
|
501
|
+
? event[@"oom_id"]
|
|
502
|
+
: nil;
|
|
503
|
+
if ([oomId isKindOfClass:[NSString class]] && [idSet containsObject:oomId]) {
|
|
504
|
+
[fm removeItemAtPath:path error:nil];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} @catch (NSException *e) {
|
|
509
|
+
NSLog(@"[RumAppStateRecorder] clearOomReports exception: %@", e);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#pragma mark - File IO
|
|
514
|
+
|
|
515
|
+
- (NSString *)appStatePath {
|
|
516
|
+
return [[RumCrashReporter crashDir] stringByAppendingPathComponent:kRumAppStateFileName];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
+ (nullable NSDictionary *)readStateJSONFileAtPath:(NSString *)path {
|
|
520
|
+
id obj = [RumCrashReporter readJSONFileAtPath:path];
|
|
521
|
+
return [obj isKindOfClass:[NSDictionary class]] ? (NSDictionary *)obj : nil;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
- (long long)fileMtimeMsAtPath:(NSString *)path {
|
|
525
|
+
NSDictionary<NSString *, id> *attrs =
|
|
526
|
+
[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
527
|
+
NSDate *modifiedDate = [attrs[NSFileModificationDate] isKindOfClass:[NSDate class]]
|
|
528
|
+
? attrs[NSFileModificationDate]
|
|
529
|
+
: nil;
|
|
530
|
+
if (modifiedDate == nil) {
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
return (long long)([modifiedDate timeIntervalSince1970] * 1000);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#pragma mark - Singleton
|
|
537
|
+
|
|
538
|
+
+ (instancetype)sharedRecorder {
|
|
539
|
+
static RumAppStateRecorder *shared = nil;
|
|
540
|
+
static dispatch_once_t onceToken;
|
|
541
|
+
dispatch_once(&onceToken, ^{
|
|
542
|
+
shared = [[RumAppStateRecorder alloc] init];
|
|
543
|
+
});
|
|
544
|
+
return shared;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@end
|
package/ios/RumCrashReporter.h
CHANGED
|
@@ -33,6 +33,38 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
33
33
|
/** 更新 crash 上下文(JSON 字符串:session_id / view_id / view_name 等) */
|
|
34
34
|
+ (void)updateCrashContext:(NSString *)context;
|
|
35
35
|
|
|
36
|
+
#pragma mark - OOM support(供 RumAppStateRecorder 复用)
|
|
37
|
+
|
|
38
|
+
/** SDK 私有目录:<Library>/Caches/RumCrashReporter/(含旧目录 ARMSCrashReporter 迁移) */
|
|
39
|
+
+ (NSString *)crashDir;
|
|
40
|
+
|
|
41
|
+
/** crash 上下文快照文件路径(<crashDir>/context.json) */
|
|
42
|
+
+ (NSString *)contextPath;
|
|
43
|
+
|
|
44
|
+
/** 读取 JSON 文件(供 OOM 状态记录模块复用) */
|
|
45
|
+
+ (nullable id)readJSONFileAtPath:(NSString *)path;
|
|
46
|
+
|
|
47
|
+
/** 原子写 JSON 文件(临时文件 + rename,供 OOM 状态记录模块复用) */
|
|
48
|
+
+ (void)atomicWriteJSONObject:(id)obj toPath:(NSString *)path;
|
|
49
|
+
|
|
50
|
+
/** 读取当前持久化的 crash 上下文(context.json,缺失时返回空字典) */
|
|
51
|
+
+ (NSDictionary *)currentCrashContext;
|
|
52
|
+
|
|
53
|
+
/** crashed_last_run.flag 是否存在(OOM 判定排除条件⑩的崩溃证据之一;生命周期由 install 全权管理——每次启动按 KSCrash.crashedLastLaunch 重写/删除,判定层只读不清除) */
|
|
54
|
+
+ (BOOL)crashedLastRunFlagExists;
|
|
55
|
+
|
|
56
|
+
/** KSCrash reportStore 是否存在待处理崩溃报告(OOM 判定排除条件⑩的崩溃证据之一) */
|
|
57
|
+
+ (BOOL)hasPendingCrashReports;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* KSCrash reportStore 是否存在**晚于给定时刻**的待处理崩溃报告(条件⑩时间范围限定):
|
|
61
|
+
* 仅当最新 pending 报告的时间戳晚于 epochMs(上次 app_state.json 的 updatedAt,即
|
|
62
|
+
* 上次运行最后一次状态写入)时视为"上次运行内的崩溃"返回 YES;早于该时间的陈旧报告
|
|
63
|
+
* (离线堆积、crash 关闭时遗留)返回 NO。时间戳不可得或比较异常时返回 YES(保守
|
|
64
|
+
* 兜底,维持抑制);epochMs <= 0 时退化为"任一 pending 报告即抑制"(原语义)。
|
|
65
|
+
*/
|
|
66
|
+
+ (BOOL)hasPendingCrashReportsAfterEpochMs:(long long)epochMs;
|
|
67
|
+
|
|
36
68
|
@end
|
|
37
69
|
|
|
38
70
|
NS_ASSUME_NONNULL_END
|