@rejourneyco/react-native 1.0.2 → 1.0.3
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/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
- package/ios/Capture/RJViewHierarchyScanner.m +68 -51
- package/ios/Core/RJLifecycleManager.m +0 -14
- package/ios/Core/Rejourney.mm +24 -37
- package/ios/Network/RJDeviceAuthManager.m +0 -2
- package/ios/Network/RJUploadManager.h +8 -0
- package/ios/Network/RJUploadManager.m +45 -0
- package/ios/Privacy/RJPrivacyMask.m +5 -31
- package/ios/Rejourney.h +0 -14
- package/ios/Touch/RJTouchInterceptor.m +21 -15
- package/ios/Utils/RJEventBuffer.m +57 -69
- package/ios/Utils/RJWindowUtils.m +87 -86
- package/lib/commonjs/index.js +42 -30
- package/lib/commonjs/sdk/autoTracking.js +0 -3
- package/lib/commonjs/sdk/networkInterceptor.js +0 -11
- package/lib/commonjs/sdk/utils.js +73 -14
- package/lib/module/index.js +42 -30
- package/lib/module/sdk/autoTracking.js +0 -3
- package/lib/module/sdk/networkInterceptor.js +0 -11
- package/lib/module/sdk/utils.js +73 -14
- package/lib/typescript/sdk/utils.d.ts +31 -1
- package/package.json +16 -4
- package/src/index.ts +40 -19
- package/src/sdk/autoTracking.ts +0 -2
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/networkInterceptor.ts +0 -9
- package/src/sdk/utils.ts +76 -14
|
@@ -152,8 +152,10 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
152
152
|
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableVideoFrames;
|
|
153
153
|
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewFrames;
|
|
154
154
|
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewPointers;
|
|
155
|
-
@property(nonatomic, strong)
|
|
156
|
-
|
|
155
|
+
@property(nonatomic, strong)
|
|
156
|
+
NSMutableArray<NSValue *> *mutableScrollViewPointers;
|
|
157
|
+
@property(nonatomic, strong)
|
|
158
|
+
NSMutableArray<NSValue *> *mutableAnimatedViewPointers;
|
|
157
159
|
|
|
158
160
|
@property(nonatomic, strong) NSMapTable<Class, NSString *> *classNameCache;
|
|
159
161
|
|
|
@@ -251,11 +253,14 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
251
253
|
// because map tiles load asynchronously and layout signature doesn't
|
|
252
254
|
// capture them
|
|
253
255
|
_mapViewClasses = [NSSet setWithArray:@[
|
|
254
|
-
@"MKMapView",
|
|
255
|
-
@"AIRMap",
|
|
256
|
-
@"AIRMapView",
|
|
257
|
-
@"RNMMapView",
|
|
258
|
-
@"GMSMapView",
|
|
256
|
+
@"MKMapView", // Apple Maps
|
|
257
|
+
@"AIRMap", // react-native-maps (iOS)
|
|
258
|
+
@"AIRMapView", // react-native-maps alternate
|
|
259
|
+
@"RNMMapView", // react-native-maps newer versions
|
|
260
|
+
@"GMSMapView", // Google Maps SDK
|
|
261
|
+
@"MGLMapView", // Mapbox GL Native (< v10)
|
|
262
|
+
@"RCTMGLMapView", // React Native Mapbox wrapper
|
|
263
|
+
@"MapboxMapView", // Mapbox Maps SDK (v10+)
|
|
259
264
|
]];
|
|
260
265
|
|
|
261
266
|
_layoutSignatureHash = 14695981039346656037ULL;
|
|
@@ -331,7 +336,8 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
331
336
|
BOOL needsPrivacyFallback =
|
|
332
337
|
(self.config.detectTextInputs &&
|
|
333
338
|
self.mutableTextInputFrames.count == 0) ||
|
|
334
|
-
(self.config.detectCameraViews &&
|
|
339
|
+
(self.config.detectCameraViews &&
|
|
340
|
+
self.mutableCameraFrames.count == 0) ||
|
|
335
341
|
(self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
|
|
336
342
|
(self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
|
|
337
343
|
if (needsPrivacyFallback && (hitViewLimit || self.didBailOutEarly)) {
|
|
@@ -358,9 +364,7 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
358
364
|
result.hasAnyAnimations = self.scanHasAnimations;
|
|
359
365
|
CGFloat screenArea = window.bounds.size.width * window.bounds.size.height;
|
|
360
366
|
result.animationAreaRatio =
|
|
361
|
-
(screenArea > 0)
|
|
362
|
-
? MIN(self.scanAnimatedArea / screenArea, 1.0)
|
|
363
|
-
: 0.0;
|
|
367
|
+
(screenArea > 0) ? MIN(self.scanAnimatedArea / screenArea, 1.0) : 0.0;
|
|
364
368
|
result.didBailOutEarly = self.didBailOutEarly;
|
|
365
369
|
|
|
366
370
|
if (self.layoutSignatureHash != 14695981039346656037ULL) {
|
|
@@ -498,9 +502,9 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
498
502
|
self.mutableTextInputFrames.count == 0) ||
|
|
499
503
|
(self.config.detectCameraViews &&
|
|
500
504
|
self.mutableCameraFrames.count == 0) ||
|
|
501
|
-
(self.config.detectWebViews &&
|
|
502
|
-
|
|
503
|
-
|
|
505
|
+
(self.config.detectWebViews &&
|
|
506
|
+
self.mutableWebViewFrames.count == 0) ||
|
|
507
|
+
(self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
|
|
504
508
|
if (needsPrivacyFallback && hitViewLimit) {
|
|
505
509
|
[self scanSensitiveViewsOnlyInWindow:window];
|
|
506
510
|
}
|
|
@@ -511,7 +515,8 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
511
515
|
BOOL needsPrivacyFallback =
|
|
512
516
|
(self.config.detectTextInputs &&
|
|
513
517
|
self.mutableTextInputFrames.count == 0) ||
|
|
514
|
-
(self.config.detectCameraViews &&
|
|
518
|
+
(self.config.detectCameraViews &&
|
|
519
|
+
self.mutableCameraFrames.count == 0) ||
|
|
515
520
|
(self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
|
|
516
521
|
(self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
|
|
517
522
|
if (needsPrivacyFallback && self.didBailOutEarly) {
|
|
@@ -542,9 +547,7 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
542
547
|
CGFloat screenArea =
|
|
543
548
|
primaryWindow.bounds.size.width * primaryWindow.bounds.size.height;
|
|
544
549
|
result.animationAreaRatio =
|
|
545
|
-
(screenArea > 0)
|
|
546
|
-
? MIN(self.scanAnimatedArea / screenArea, 1.0)
|
|
547
|
-
: 0.0;
|
|
550
|
+
(screenArea > 0) ? MIN(self.scanAnimatedArea / screenArea, 1.0) : 0.0;
|
|
548
551
|
result.didBailOutEarly = self.didBailOutEarly;
|
|
549
552
|
|
|
550
553
|
if (self.layoutSignatureHash != 14695981039346656037ULL) {
|
|
@@ -679,8 +682,10 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
679
682
|
|
|
680
683
|
@try {
|
|
681
684
|
BOOL isWebView = self.config.detectWebViews && [self isWebView:view];
|
|
682
|
-
BOOL isCamera =
|
|
683
|
-
|
|
685
|
+
BOOL isCamera =
|
|
686
|
+
self.config.detectCameraViews && [self isCameraPreview:view];
|
|
687
|
+
BOOL isVideo =
|
|
688
|
+
self.config.detectVideoLayers && [self isVideoLayerView:view];
|
|
684
689
|
BOOL isBlockedSurface = isWebView || isCamera || isVideo;
|
|
685
690
|
|
|
686
691
|
[self checkSensitiveView:view inWindow:window];
|
|
@@ -762,10 +767,11 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
762
767
|
UIEdgeInsets inset = ((UIScrollView *)view).contentInset;
|
|
763
768
|
[self mixInt:(int32_t)lrintf(isfinite(inset.top) ? inset.top * 100 : 0)];
|
|
764
769
|
[self mixInt:(int32_t)lrintf(isfinite(inset.bottom) ? inset.bottom * 100
|
|
765
|
-
|
|
766
|
-
[self
|
|
770
|
+
: 0)];
|
|
771
|
+
[self
|
|
772
|
+
mixInt:(int32_t)lrintf(isfinite(inset.left) ? inset.left * 100 : 0)];
|
|
767
773
|
[self mixInt:(int32_t)lrintf(isfinite(inset.right) ? inset.right * 100
|
|
768
|
-
|
|
774
|
+
: 0)];
|
|
769
775
|
}
|
|
770
776
|
|
|
771
777
|
// 5. Mix Text Content (avoid input content; use length only)
|
|
@@ -840,7 +846,8 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
840
846
|
}
|
|
841
847
|
}
|
|
842
848
|
|
|
843
|
-
- (void)appendBlockedSurfaceInfoToSignature:(UIView *)view
|
|
849
|
+
- (void)appendBlockedSurfaceInfoToSignature:(UIView *)view
|
|
850
|
+
depth:(NSInteger)depth {
|
|
844
851
|
if (!view) {
|
|
845
852
|
return;
|
|
846
853
|
}
|
|
@@ -998,18 +1005,18 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
998
1005
|
}
|
|
999
1006
|
|
|
1000
1007
|
#ifdef DEBUG
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1008
|
+
RJLogDebug(
|
|
1009
|
+
@"ViewHierarchyScanner: Found %@ at (%.0f,%.0f,%.0f,%.0f) - "
|
|
1010
|
+
@"view.window=%@ targetWindow=%@",
|
|
1011
|
+
isTextInput
|
|
1012
|
+
? @"TextInput"
|
|
1013
|
+
: (isCamera ? @"Camera"
|
|
1014
|
+
: (isWebView ? @"WebView"
|
|
1015
|
+
: (isVideo ? @"Video" : @"MaskedView"))),
|
|
1016
|
+
sanitizedFrame.origin.x, sanitizedFrame.origin.y,
|
|
1017
|
+
sanitizedFrame.size.width, sanitizedFrame.size.height,
|
|
1018
|
+
NSStringFromClass([view.window class]),
|
|
1019
|
+
NSStringFromClass([targetWindow class]));
|
|
1013
1020
|
#endif
|
|
1014
1021
|
}
|
|
1015
1022
|
}
|
|
@@ -1212,7 +1219,7 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1212
1219
|
|
|
1213
1220
|
@try {
|
|
1214
1221
|
if ([view respondsToSelector:@selector(isLoading)]) {
|
|
1215
|
-
BOOL (*loadingMsg)(id, SEL) = (BOOL
|
|
1222
|
+
BOOL (*loadingMsg)(id, SEL) = (BOOL(*)(id, SEL))objc_msgSend;
|
|
1216
1223
|
return loadingMsg(view, @selector(isLoading));
|
|
1217
1224
|
}
|
|
1218
1225
|
id loadingValue = [view valueForKey:@"loading"];
|
|
@@ -1314,8 +1321,9 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1314
1321
|
|
|
1315
1322
|
BOOL tracking = scrollView.isTracking || scrollView.isDragging ||
|
|
1316
1323
|
scrollView.isDecelerating;
|
|
1317
|
-
BOOL offsetMoved =
|
|
1318
|
-
|
|
1324
|
+
BOOL offsetMoved =
|
|
1325
|
+
(fabs(offset.x - state.contentOffset.x) > kRJScrollEpsilon ||
|
|
1326
|
+
fabs(offset.y - state.contentOffset.y) > kRJScrollEpsilon);
|
|
1319
1327
|
BOOL zoomMoved = fabs(zoomScale - state.zoomScale) > kRJZoomEpsilon;
|
|
1320
1328
|
if (tracking || offsetMoved || zoomMoved) {
|
|
1321
1329
|
self.scanScrollActive = YES;
|
|
@@ -1331,8 +1339,8 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1331
1339
|
}
|
|
1332
1340
|
|
|
1333
1341
|
if ([self isRefreshActiveForScrollView:scrollView
|
|
1334
|
-
|
|
1335
|
-
|
|
1342
|
+
offset:offset
|
|
1343
|
+
inset:inset]) {
|
|
1336
1344
|
self.scanRefreshActive = YES;
|
|
1337
1345
|
}
|
|
1338
1346
|
|
|
@@ -1351,16 +1359,16 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1351
1359
|
}
|
|
1352
1360
|
CGFloat topLimit = -inset.top - kRJScrollEpsilon;
|
|
1353
1361
|
CGFloat bottomLimit = scrollView.contentSize.height -
|
|
1354
|
-
scrollView.bounds.size.height +
|
|
1355
|
-
|
|
1362
|
+
scrollView.bounds.size.height + inset.bottom +
|
|
1363
|
+
kRJScrollEpsilon;
|
|
1356
1364
|
if (offset.y < topLimit || offset.y > bottomLimit) {
|
|
1357
1365
|
return YES;
|
|
1358
1366
|
}
|
|
1359
1367
|
|
|
1360
1368
|
CGFloat leftLimit = -inset.left - kRJScrollEpsilon;
|
|
1361
1369
|
CGFloat rightLimit = scrollView.contentSize.width -
|
|
1362
|
-
scrollView.bounds.size.width +
|
|
1363
|
-
|
|
1370
|
+
scrollView.bounds.size.width + inset.right +
|
|
1371
|
+
kRJScrollEpsilon;
|
|
1364
1372
|
return (offset.x < leftLimit || offset.x > rightLimit);
|
|
1365
1373
|
}
|
|
1366
1374
|
|
|
@@ -1376,8 +1384,8 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1376
1384
|
return YES;
|
|
1377
1385
|
}
|
|
1378
1386
|
|
|
1379
|
-
CGFloat triggerOffset =
|
|
1380
|
-
|
|
1387
|
+
CGFloat triggerOffset =
|
|
1388
|
+
-scrollView.adjustedContentInset.top - kRJScrollEpsilon;
|
|
1381
1389
|
if (offset.y < triggerOffset) {
|
|
1382
1390
|
return YES;
|
|
1383
1391
|
}
|
|
@@ -1448,14 +1456,23 @@ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
|
1448
1456
|
NSNumber *heading = [view valueForKeyPath:@"camera.heading"];
|
|
1449
1457
|
NSNumber *pitch = [view valueForKeyPath:@"camera.pitch"];
|
|
1450
1458
|
|
|
1459
|
+
// Mapbox uses zoomLevel instead of span
|
|
1460
|
+
NSNumber *zoomLevel = nil;
|
|
1461
|
+
@try {
|
|
1462
|
+
zoomLevel = [view valueForKey:@"zoomLevel"];
|
|
1463
|
+
} @catch (NSException *e) {
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1451
1466
|
double altitudeValue = altitude ? altitude.doubleValue : 0;
|
|
1452
1467
|
double headingValue = heading ? heading.doubleValue : 0;
|
|
1453
1468
|
double pitchValue = pitch ? pitch.doubleValue : 0;
|
|
1469
|
+
double zoomValue = zoomLevel ? zoomLevel.doubleValue : 0;
|
|
1454
1470
|
|
|
1455
|
-
return [NSString
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1471
|
+
return [NSString
|
|
1472
|
+
stringWithFormat:@"%.5f:%.5f:%.5f:%.5f:%.1f:%.1f:%.1f:%.2f",
|
|
1473
|
+
center.latitude, center.longitude, span.latitudeDelta,
|
|
1474
|
+
span.longitudeDelta, altitudeValue, headingValue,
|
|
1475
|
+
pitchValue, zoomValue];
|
|
1459
1476
|
} @catch (NSException *exception) {
|
|
1460
1477
|
return @"";
|
|
1461
1478
|
}
|
|
@@ -121,7 +121,6 @@
|
|
|
121
121
|
name:UIApplicationDidBecomeActiveNotification
|
|
122
122
|
object:nil];
|
|
123
123
|
|
|
124
|
-
// Text change notifications
|
|
125
124
|
[center addObserver:self
|
|
126
125
|
selector:@selector(textDidChange:)
|
|
127
126
|
name:UITextFieldTextDidChangeNotification
|
|
@@ -220,8 +219,6 @@
|
|
|
220
219
|
RJLogInfo(@"[RJ-LIFECYCLE] appDidEnterBackground (isRecording=%@)",
|
|
221
220
|
self.isRecording ? @"YES" : @"NO");
|
|
222
221
|
|
|
223
|
-
// Always track background entry time, even if not recording
|
|
224
|
-
// This allows us to detect timeout when session was ended while in background
|
|
225
222
|
self.inBackground = YES;
|
|
226
223
|
self.backgroundEntryTime = [[NSDate date] timeIntervalSince1970];
|
|
227
224
|
|
|
@@ -257,13 +254,10 @@
|
|
|
257
254
|
RJLogInfo(@"[RJ-LIFECYCLE] appDidBecomeActive - was NOT in background");
|
|
258
255
|
}
|
|
259
256
|
|
|
260
|
-
// Reset background tracking state
|
|
261
257
|
self.inBackground = NO;
|
|
262
258
|
self.backgroundEntryTime = 0;
|
|
263
259
|
|
|
264
|
-
// Handle the case where we weren't recording (session already ended)
|
|
265
260
|
if (!self.isRecording) {
|
|
266
|
-
// If we were in background long enough, signal that a new session should start
|
|
267
261
|
if (wasInBackground && backgroundDurationSec >= self.backgroundTimeoutThreshold) {
|
|
268
262
|
RJLogInfo(@"[RJ-LIFECYCLE] Was not recording, background >= %.0fs - signaling for new session start",
|
|
269
263
|
self.backgroundTimeoutThreshold);
|
|
@@ -274,14 +268,10 @@
|
|
|
274
268
|
return;
|
|
275
269
|
}
|
|
276
270
|
|
|
277
|
-
// We ARE recording - handle background return
|
|
278
271
|
if (wasInBackground) {
|
|
279
272
|
NSTimeInterval bgDurationMs = backgroundDurationSec * 1000;
|
|
280
273
|
|
|
281
274
|
if (backgroundDurationSec >= self.backgroundTimeoutThreshold) {
|
|
282
|
-
// TIMEOUT CASE: End old session, start new one
|
|
283
|
-
// Add this background duration to accumulated time BEFORE signaling timeout
|
|
284
|
-
// so the old session gets the correct total background time
|
|
285
275
|
self.accumulatedBackgroundTimeMs += bgDurationMs;
|
|
286
276
|
RJLogInfo(@"[RJ-LIFECYCLE] TIMEOUT: Added %.0fms, total background=%.0fms - signaling session restart",
|
|
287
277
|
bgDurationMs, self.accumulatedBackgroundTimeMs);
|
|
@@ -289,17 +279,13 @@
|
|
|
289
279
|
if ([self.delegate respondsToSelector:@selector(lifecycleManagerSessionDidTimeout:)]) {
|
|
290
280
|
[self.delegate lifecycleManagerSessionDidTimeout:backgroundDurationSec];
|
|
291
281
|
}
|
|
292
|
-
// Note: The delegate's handleSessionTimeout will read totalBackgroundTimeMs
|
|
293
|
-
// and then call resetBackgroundTime for the new session
|
|
294
282
|
} else {
|
|
295
|
-
// SHORT BACKGROUND: Just accumulate and resume
|
|
296
283
|
self.accumulatedBackgroundTimeMs += bgDurationMs;
|
|
297
284
|
RJLogInfo(@"[RJ-LIFECYCLE] Short background: Added %.0fms, total=%.0fms - resuming session",
|
|
298
285
|
bgDurationMs, self.accumulatedBackgroundTimeMs);
|
|
299
286
|
}
|
|
300
287
|
}
|
|
301
288
|
|
|
302
|
-
// Call didBecomeActive for normal resume handling (video capture, etc.)
|
|
303
289
|
if ([self.delegate respondsToSelector:@selector(lifecycleManagerDidBecomeActive)]) {
|
|
304
290
|
[self.delegate lifecycleManagerDidBecomeActive];
|
|
305
291
|
}
|
package/ios/Core/Rejourney.mm
CHANGED
|
@@ -249,22 +249,33 @@ RCT_EXPORT_MODULE()
|
|
|
249
249
|
didDecideSample = YES;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
// Decouple video recording from session active state.
|
|
253
|
+
// We want to record video ONLY if:
|
|
254
|
+
// 1. Config says recording is enabled (remote toggles)
|
|
255
|
+
// 2. Sample rate allows it
|
|
256
|
+
//
|
|
257
|
+
// NOTE: Even if shouldRecordVideo is NO, the session remains active
|
|
258
|
+
// (isRecording=YES) and events are still observed and uploaded ("Data-Only
|
|
259
|
+
// Mode").
|
|
260
|
+
BOOL shouldRecordVideo = self.recordingEnabledByConfig && self.sessionSampled;
|
|
261
|
+
self.remoteRecordingEnabled = shouldRecordVideo;
|
|
254
262
|
|
|
255
263
|
if (self.captureEngine) {
|
|
256
|
-
self.captureEngine.uploadsEnabled =
|
|
257
|
-
if (!
|
|
264
|
+
self.captureEngine.uploadsEnabled = shouldRecordVideo;
|
|
265
|
+
if (!shouldRecordVideo && self.captureEngine.isRecording) {
|
|
266
|
+
// Stop video capture pipeline to save resources, but keep session alive
|
|
258
267
|
[self.captureEngine stopSession];
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
if (didDecideSample && self.recordingEnabledByConfig &&
|
|
263
272
|
!self.sessionSampled) {
|
|
264
|
-
|
|
273
|
+
RJLogInfo(@"Session sampled out for video (%ld%%) - entering Data-Only "
|
|
274
|
+
@"Mode (Events enabled, Video disabled)",
|
|
275
|
+
(long)clampedRate);
|
|
265
276
|
}
|
|
266
277
|
|
|
267
|
-
return
|
|
278
|
+
return shouldRecordVideo;
|
|
268
279
|
}
|
|
269
280
|
|
|
270
281
|
- (void)ensureFullyInitialized {
|
|
@@ -676,10 +687,7 @@ RCT_EXPORT_METHOD(stopSession : (RCTPromiseResolveBlock)
|
|
|
676
687
|
|
|
677
688
|
NSString *sessionId = self.currentSessionId ?: @"";
|
|
678
689
|
|
|
679
|
-
|
|
680
|
-
// When a session ends while the app is in background (e.g. max duration),
|
|
681
|
-
// we must include the ongoing background duration, otherwise the backend
|
|
682
|
-
// will think the entire wall-clock session duration is playable.
|
|
690
|
+
|
|
683
691
|
NSTimeInterval totalBgTimeMs = 0;
|
|
684
692
|
if (self.lifecycleManager) {
|
|
685
693
|
totalBgTimeMs = self.lifecycleManager.totalBackgroundTimeMs;
|
|
@@ -1037,7 +1045,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
1037
1045
|
if ([gestureType hasPrefix:@"scroll"]) {
|
|
1038
1046
|
static NSTimeInterval lastScrollLogTime = 0;
|
|
1039
1047
|
NSTimeInterval now = CACurrentMediaTime();
|
|
1040
|
-
if (now - lastScrollLogTime < 0.5) {
|
|
1048
|
+
if (now - lastScrollLogTime < 0.5) {
|
|
1041
1049
|
return;
|
|
1042
1050
|
}
|
|
1043
1051
|
lastScrollLogTime = now;
|
|
@@ -1125,7 +1133,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
1125
1133
|
registerDeviceWithProjectKey:publicKey
|
|
1126
1134
|
bundleId:bundleId
|
|
1127
1135
|
platform:@"ios"
|
|
1128
|
-
sdkVersion
|
|
1136
|
+
sdkVersion:RJSDKVersion
|
|
1129
1137
|
apiUrl:apiUrl
|
|
1130
1138
|
completion:^(BOOL success, NSString *credId,
|
|
1131
1139
|
NSError *error) {
|
|
@@ -1329,12 +1337,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
1329
1337
|
return;
|
|
1330
1338
|
}
|
|
1331
1339
|
|
|
1332
|
-
// Handle security errors (403 = forbidden/mismatch, 404 = not found)
|
|
1333
1340
|
if ((error.code == 403 || error.code == 404) &&
|
|
1334
1341
|
[error.domain isEqualToString:@"RJDeviceAuth"]) {
|
|
1335
1342
|
|
|
1336
1343
|
if (!isRetry) {
|
|
1337
|
-
// First failure - try re-registration
|
|
1338
1344
|
RJLogDebug(@"Device auth invalid (%ld), attempting re-registration...",
|
|
1339
1345
|
(long)error.code);
|
|
1340
1346
|
NSString *bundleId =
|
|
@@ -1345,7 +1351,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
1345
1351
|
registerDeviceWithProjectKey:publicKey
|
|
1346
1352
|
bundleId:bundleId
|
|
1347
1353
|
platform:@"ios"
|
|
1348
|
-
sdkVersion
|
|
1354
|
+
sdkVersion:RJSDKVersion
|
|
1349
1355
|
apiUrl:apiUrl
|
|
1350
1356
|
completion:^(BOOL retrySuccess, NSString *credId,
|
|
1351
1357
|
NSError *retryError) {
|
|
@@ -2112,7 +2118,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2112
2118
|
}
|
|
2113
2119
|
|
|
2114
2120
|
if (self.captureEngine) {
|
|
2115
|
-
|
|
2121
|
+
|
|
2116
2122
|
RJLogInfo(@"[RJ-TERMINATE] Stopping capture engine synchronously");
|
|
2117
2123
|
[self.captureEngine stopSessionSync];
|
|
2118
2124
|
RJLogInfo(@"[RJ-TERMINATE] Capture engine stopped");
|
|
@@ -2146,9 +2152,9 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2146
2152
|
|
|
2147
2153
|
if (self.uploadManager && events.count > 0) {
|
|
2148
2154
|
@try {
|
|
2149
|
-
[self.uploadManager
|
|
2155
|
+
[self.uploadManager persistTerminationEvents:events ?: @[]];
|
|
2150
2156
|
} @catch (NSException *e) {
|
|
2151
|
-
RJLogWarning(@"Terminate
|
|
2157
|
+
RJLogWarning(@"Terminate persistence failed: %@", e);
|
|
2152
2158
|
}
|
|
2153
2159
|
}
|
|
2154
2160
|
} @catch (NSException *exception) {
|
|
@@ -2212,13 +2218,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2212
2218
|
[self handleSessionTimeout:backgroundDuration currentTime:currentTime];
|
|
2213
2219
|
}
|
|
2214
2220
|
|
|
2215
|
-
|
|
2216
|
-
/// 1. Capture final background time from lifecycle manager
|
|
2217
|
-
/// 2. Stop timers and capture engine for old session
|
|
2218
|
-
/// 3. Synchronously end the old session with correct background time
|
|
2219
|
-
/// 4. Create new session ID and reset all state
|
|
2220
|
-
/// 5. Start capture for new session
|
|
2221
|
-
/// 6. Trigger immediate upload to register new session
|
|
2222
2221
|
- (void)handleSessionTimeout:(NSTimeInterval)backgroundDuration
|
|
2223
2222
|
currentTime:(NSTimeInterval)currentTime {
|
|
2224
2223
|
RJLogInfo(
|
|
@@ -2232,8 +2231,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2232
2231
|
RJLogInfo(@"[RJ-SESSION-TIMEOUT] === ENDING OLD SESSION: %@ ===",
|
|
2233
2232
|
oldSessionId);
|
|
2234
2233
|
|
|
2235
|
-
// ========== STEP 1: Capture background time BEFORE any state changes
|
|
2236
|
-
// ==========
|
|
2237
2234
|
NSTimeInterval totalBackgroundMs = 0;
|
|
2238
2235
|
if (self.lifecycleManager) {
|
|
2239
2236
|
totalBackgroundMs = self.lifecycleManager.totalBackgroundTimeMs;
|
|
@@ -2242,7 +2239,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2242
2239
|
totalBackgroundMs);
|
|
2243
2240
|
}
|
|
2244
2241
|
|
|
2245
|
-
// ========== STEP 2: Stop all capture/timers for old session ==========
|
|
2246
2242
|
[self stopBatchUploadTimer];
|
|
2247
2243
|
[self stopDurationLimitTimer];
|
|
2248
2244
|
|
|
@@ -2255,14 +2251,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2255
2251
|
}
|
|
2256
2252
|
}
|
|
2257
2253
|
|
|
2258
|
-
// ========== STEP 3: End old session SYNCHRONOUSLY with correct background
|
|
2259
|
-
// time ==========
|
|
2260
2254
|
if (wasRecording && self.uploadManager && oldSessionId.length > 0 &&
|
|
2261
2255
|
![oldSessionId isEqualToString:@"none"]) {
|
|
2262
|
-
// Set background time on upload manager
|
|
2263
2256
|
self.uploadManager.totalBackgroundTimeMs = totalBackgroundMs;
|
|
2264
2257
|
|
|
2265
|
-
// Get current events for final upload
|
|
2266
2258
|
__block NSArray<NSDictionary *> *finalEvents = nil;
|
|
2267
2259
|
[self performStateSync:^{
|
|
2268
2260
|
finalEvents = [self.sessionEvents copy];
|
|
@@ -2272,7 +2264,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2272
2264
|
@"bgTime=%.0fms",
|
|
2273
2265
|
(unsigned long)finalEvents.count, totalBackgroundMs);
|
|
2274
2266
|
|
|
2275
|
-
// Synchronous upload and session end
|
|
2276
2267
|
if (finalEvents.count > 0) {
|
|
2277
2268
|
[self.uploadManager synchronousUploadWithEvents:finalEvents];
|
|
2278
2269
|
} else {
|
|
@@ -2282,7 +2273,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2282
2273
|
RJLogInfo(@"[RJ-SESSION-TIMEOUT] Old session %@ ended", oldSessionId);
|
|
2283
2274
|
}
|
|
2284
2275
|
|
|
2285
|
-
// ========== STEP 4: Reset ALL state for new session ==========
|
|
2286
2276
|
RJLogInfo(@"[RJ-SESSION-TIMEOUT] === STARTING NEW SESSION ===");
|
|
2287
2277
|
|
|
2288
2278
|
__block NSString *newSessionId = nil;
|
|
@@ -2336,7 +2326,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2336
2326
|
self.eventBuffer = [[RJEventBuffer alloc] initWithSessionId:newSessionId
|
|
2337
2327
|
pendingRootPath:pendingPath];
|
|
2338
2328
|
|
|
2339
|
-
// ========== STEP 5: Start capture for new session ==========
|
|
2340
2329
|
[self resetSamplingDecision];
|
|
2341
2330
|
self.remoteRecordingEnabled = self.recordingEnabledByConfig;
|
|
2342
2331
|
if (self.captureEngine) {
|
|
@@ -2367,8 +2356,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
2367
2356
|
[self startBatchUploadTimer];
|
|
2368
2357
|
[self startDurationLimitTimer];
|
|
2369
2358
|
|
|
2370
|
-
// ========== STEP 6: Log session_start and trigger immediate upload
|
|
2371
|
-
// ==========
|
|
2372
2359
|
NSMutableDictionary *sessionStartEvent = [NSMutableDictionary dictionary];
|
|
2373
2360
|
sessionStartEvent[@"type"] = RJEventTypeSessionStart;
|
|
2374
2361
|
sessionStartEvent[@"timestamp"] = @([RJWindowUtils currentTimestampMillis]);
|
|
@@ -857,10 +857,8 @@ static const NSInteger RJ_AUTH_MAX_CONSECUTIVE_FAILURES = 10;
|
|
|
857
857
|
return;
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
-
// Check cooldown after consecutive failures to prevent flooding
|
|
861
860
|
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
862
861
|
if (self.consecutiveFailures > 0 && self.lastFailedRegistrationTime > 0) {
|
|
863
|
-
// Exponential backoff: 5s, 10s, 20s, 40s... up to 5 minutes
|
|
864
862
|
NSTimeInterval cooldown = MIN(
|
|
865
863
|
RJ_AUTH_COOLDOWN_BASE_SECONDS * pow(2, self.consecutiveFailures - 1),
|
|
866
864
|
RJ_AUTH_COOLDOWN_MAX_SECONDS
|
|
@@ -180,6 +180,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
180
180
|
*/
|
|
181
181
|
- (BOOL)synchronousUploadWithEvents:(NSArray<NSDictionary *> *)events;
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Persists events to disk for termination, skipping network upload.
|
|
185
|
+
* Use this in appWillTerminate to avoid watchdog kills.
|
|
186
|
+
*
|
|
187
|
+
* @param events Array of event dictionaries.
|
|
188
|
+
*/
|
|
189
|
+
- (void)persistTerminationEvents:(NSArray<NSDictionary *> *)events;
|
|
190
|
+
|
|
183
191
|
/**
|
|
184
192
|
* Uploads a crash report to the dashboard.
|
|
185
193
|
*
|
|
@@ -67,6 +67,7 @@ static NSString *RJRedactedURLForLogFromString(NSString *urlString) {
|
|
|
67
67
|
|
|
68
68
|
@interface RJUploadManager ()
|
|
69
69
|
|
|
70
|
+
@property(nonatomic, assign) BOOL keyboardVisible;
|
|
70
71
|
@property(nonatomic, strong) dispatch_queue_t uploadQueue;
|
|
71
72
|
|
|
72
73
|
@property(nonatomic, strong, nullable) NSTimer *batchUploadTimer;
|
|
@@ -1820,6 +1821,50 @@ static NSString *RJRedactedURLForLogFromString(NSString *urlString) {
|
|
|
1820
1821
|
return YES;
|
|
1821
1822
|
}
|
|
1822
1823
|
|
|
1824
|
+
- (void)persistTerminationEvents:(NSArray<NSDictionary *> *)events {
|
|
1825
|
+
if (events.count == 0 && self.sessionId.length == 0)
|
|
1826
|
+
return;
|
|
1827
|
+
|
|
1828
|
+
self.eventBatchNumber += 1;
|
|
1829
|
+
NSInteger currentBatch = self.eventBatchNumber;
|
|
1830
|
+
|
|
1831
|
+
RJLogInfo(@"[RJ-UPLOAD] persistTerminationEvents starting: sessionId=%@, "
|
|
1832
|
+
@"batch=%ld, "
|
|
1833
|
+
@"eventCount=%lu",
|
|
1834
|
+
self.sessionId ?: @"<nil>", (long)currentBatch,
|
|
1835
|
+
(unsigned long)events.count);
|
|
1836
|
+
|
|
1837
|
+
NSDictionary *payload = [self buildEventPayloadWithEvents:events
|
|
1838
|
+
batchNumber:currentBatch
|
|
1839
|
+
isFinal:YES];
|
|
1840
|
+
if (!payload)
|
|
1841
|
+
return;
|
|
1842
|
+
|
|
1843
|
+
NSError *jsonError = nil;
|
|
1844
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload
|
|
1845
|
+
options:0
|
|
1846
|
+
error:&jsonError];
|
|
1847
|
+
if (jsonError || !jsonData)
|
|
1848
|
+
return;
|
|
1849
|
+
|
|
1850
|
+
NSError *gzipError = nil;
|
|
1851
|
+
NSData *compressed = RJGzipData(jsonData, &gzipError);
|
|
1852
|
+
if (gzipError || !compressed)
|
|
1853
|
+
return;
|
|
1854
|
+
|
|
1855
|
+
[self persistPendingUploadWithContentType:@"events"
|
|
1856
|
+
batchNumber:currentBatch
|
|
1857
|
+
keyframe:NO
|
|
1858
|
+
gzipped:compressed
|
|
1859
|
+
eventCount:events.count
|
|
1860
|
+
frameCount:0];
|
|
1861
|
+
|
|
1862
|
+
[self markSessionActiveForRecovery];
|
|
1863
|
+
|
|
1864
|
+
RJLogInfo(@"[RJ-CHECKPOINT] Persisted termination events to disk, skipping "
|
|
1865
|
+
@"sync upload");
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1823
1868
|
- (BOOL)uploadEventsBatchSync:(NSArray<NSDictionary *> *)events
|
|
1824
1869
|
isFinal:(BOOL)isFinal {
|
|
1825
1870
|
self.eventBatchNumber += 1;
|