@rejourneyco/react-native 1.0.1 → 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.
Files changed (65) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +72 -391
  2. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
  3. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
  4. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
  5. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
  6. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
  7. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
  8. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
  9. package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
  10. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
  11. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
  12. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
  13. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
  14. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
  15. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
  16. package/ios/Capture/RJCaptureEngine.m +3 -34
  17. package/ios/Capture/RJVideoEncoder.m +0 -26
  18. package/ios/Capture/RJViewHierarchyScanner.m +68 -51
  19. package/ios/Core/RJLifecycleManager.m +0 -14
  20. package/ios/Core/Rejourney.mm +53 -129
  21. package/ios/Network/RJDeviceAuthManager.m +0 -2
  22. package/ios/Network/RJUploadManager.h +8 -0
  23. package/ios/Network/RJUploadManager.m +45 -0
  24. package/ios/Privacy/RJPrivacyMask.m +5 -31
  25. package/ios/Rejourney.h +0 -14
  26. package/ios/Touch/RJTouchInterceptor.m +21 -15
  27. package/ios/Utils/RJEventBuffer.m +57 -69
  28. package/ios/Utils/RJPerfTiming.m +0 -5
  29. package/ios/Utils/RJWindowUtils.m +87 -87
  30. package/lib/commonjs/components/Mask.js +1 -6
  31. package/lib/commonjs/index.js +46 -117
  32. package/lib/commonjs/sdk/autoTracking.js +39 -313
  33. package/lib/commonjs/sdk/constants.js +2 -13
  34. package/lib/commonjs/sdk/errorTracking.js +1 -29
  35. package/lib/commonjs/sdk/metricsTracking.js +3 -24
  36. package/lib/commonjs/sdk/navigation.js +3 -42
  37. package/lib/commonjs/sdk/networkInterceptor.js +7 -60
  38. package/lib/commonjs/sdk/utils.js +73 -19
  39. package/lib/module/components/Mask.js +1 -6
  40. package/lib/module/index.js +45 -121
  41. package/lib/module/sdk/autoTracking.js +39 -314
  42. package/lib/module/sdk/constants.js +2 -13
  43. package/lib/module/sdk/errorTracking.js +1 -29
  44. package/lib/module/sdk/index.js +0 -2
  45. package/lib/module/sdk/metricsTracking.js +3 -24
  46. package/lib/module/sdk/navigation.js +3 -42
  47. package/lib/module/sdk/networkInterceptor.js +7 -60
  48. package/lib/module/sdk/utils.js +73 -19
  49. package/lib/typescript/NativeRejourney.d.ts +1 -0
  50. package/lib/typescript/sdk/autoTracking.d.ts +4 -4
  51. package/lib/typescript/sdk/utils.d.ts +31 -1
  52. package/lib/typescript/types/index.d.ts +0 -1
  53. package/package.json +17 -11
  54. package/src/NativeRejourney.ts +2 -0
  55. package/src/components/Mask.tsx +0 -3
  56. package/src/index.ts +43 -92
  57. package/src/sdk/autoTracking.ts +51 -284
  58. package/src/sdk/constants.ts +13 -13
  59. package/src/sdk/errorTracking.ts +1 -17
  60. package/src/sdk/index.ts +0 -2
  61. package/src/sdk/metricsTracking.ts +5 -33
  62. package/src/sdk/navigation.ts +8 -29
  63. package/src/sdk/networkInterceptor.ts +9 -42
  64. package/src/sdk/utils.ts +76 -19
  65. package/src/types/index.ts +0 -29
@@ -46,6 +46,7 @@
46
46
  #import <UIKit/UIKit.h>
47
47
  #import <mach/mach_time.h>
48
48
  #import <sys/sysctl.h>
49
+ #import <sys/utsname.h>
49
50
 
50
51
  static uint64_t _rj_constructorMachTime = 0;
51
52
  static NSTimeInterval _rj_constructorWallTimeMs = 0;
@@ -248,22 +249,33 @@ RCT_EXPORT_MODULE()
248
249
  didDecideSample = YES;
249
250
  }
250
251
 
251
- BOOL shouldRecord = self.recordingEnabledByConfig && self.sessionSampled;
252
- self.remoteRecordingEnabled = shouldRecord;
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;
253
262
 
254
263
  if (self.captureEngine) {
255
- self.captureEngine.uploadsEnabled = shouldRecord;
256
- if (!shouldRecord && self.captureEngine.isRecording) {
264
+ self.captureEngine.uploadsEnabled = shouldRecordVideo;
265
+ if (!shouldRecordVideo && self.captureEngine.isRecording) {
266
+ // Stop video capture pipeline to save resources, but keep session alive
257
267
  [self.captureEngine stopSession];
258
268
  }
259
269
  }
260
270
 
261
271
  if (didDecideSample && self.recordingEnabledByConfig &&
262
272
  !self.sessionSampled) {
263
- RJLogWarning(@"Session skipped by sample rate (%ld%%)", (long)clampedRate);
273
+ RJLogInfo(@"Session sampled out for video (%ld%%) - entering Data-Only "
274
+ @"Mode (Events enabled, Video disabled)",
275
+ (long)clampedRate);
264
276
  }
265
277
 
266
- return shouldRecord;
278
+ return shouldRecordVideo;
267
279
  }
268
280
 
269
281
  - (void)ensureFullyInitialized {
@@ -608,6 +620,34 @@ RCT_EXPORT_METHOD(debugTriggerANR : (double)durationMs) {
608
620
  });
609
621
  }
610
622
 
623
+ RCT_EXPORT_METHOD(getDeviceInfo : (RCTPromiseResolveBlock)
624
+ resolve reject : (RCTPromiseRejectBlock)reject) {
625
+ NSMutableDictionary *info = [NSMutableDictionary new];
626
+
627
+ // Model
628
+ struct utsname systemInfo;
629
+ uname(&systemInfo);
630
+ NSString *modelCode = [NSString stringWithCString:systemInfo.machine
631
+ encoding:NSUTF8StringEncoding];
632
+ info[@"model"] = modelCode ?: [[UIDevice currentDevice] model];
633
+
634
+ info[@"brand"] = @"Apple";
635
+ info[@"systemName"] = [[UIDevice currentDevice] systemName];
636
+ info[@"systemVersion"] = [[UIDevice currentDevice] systemVersion];
637
+ info[@"bundleId"] = [[NSBundle mainBundle] bundleIdentifier] ?: @"";
638
+ info[@"appVersion"] =
639
+ [[NSBundle mainBundle]
640
+ objectForInfoDictionaryKey:@"CFBundleShortVersionString"]
641
+ ?: @"";
642
+ info[@"buildNumber"] =
643
+ [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]
644
+ ?: @"";
645
+ info[@"isTablet"] = @([[UIDevice currentDevice] userInterfaceIdiom] ==
646
+ UIUserInterfaceIdiomPad);
647
+
648
+ resolve(info);
649
+ }
650
+
611
651
  RCT_EXPORT_METHOD(getSessionId : (RCTPromiseResolveBlock)
612
652
  resolve reject : (RCTPromiseRejectBlock)reject) {
613
653
  NSString *sessionId = self.currentSessionId;
@@ -647,10 +687,7 @@ RCT_EXPORT_METHOD(stopSession : (RCTPromiseResolveBlock)
647
687
 
648
688
  NSString *sessionId = self.currentSessionId ?: @"";
649
689
 
650
- // CRITICAL: Compute background time at the moment we end the session.
651
- // When a session ends while the app is in background (e.g. max duration),
652
- // we must include the ongoing background duration, otherwise the backend
653
- // will think the entire wall-clock session duration is playable.
690
+
654
691
  NSTimeInterval totalBgTimeMs = 0;
655
692
  if (self.lifecycleManager) {
656
693
  totalBgTimeMs = self.lifecycleManager.totalBackgroundTimeMs;
@@ -947,7 +984,6 @@ RCT_EXPORT_METHOD(setUserIdentity : (NSString *)userId resolve : (
947
984
  @try {
948
985
  NSString *safeUserId = userId.length > 0 ? userId : @"anonymous";
949
986
 
950
- // KEY CHANGE: Persist directly to NSUserDefaults (Native Storage)
951
987
  [[NSUserDefaults standardUserDefaults] setObject:safeUserId
952
988
  forKey:@"rj_user_identity"];
953
989
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -960,13 +996,11 @@ RCT_EXPORT_METHOD(setUserIdentity : (NSString *)userId resolve : (
960
996
  RJLogDebug(@"User identity updated and persisted: %@", safeUserId);
961
997
 
962
998
  if (self.isRecording) {
963
- // Log event for tracking
964
999
  NSMutableDictionary *event = [NSMutableDictionary new];
965
1000
  event[@"type"] = @"user_identity_changed";
966
1001
  event[@"timestamp"] = @([RJWindowUtils currentTimestampMillis]);
967
1002
  event[@"userId"] = safeUserId;
968
1003
 
969
- // Helper to log event safely
970
1004
  if (self.eventBuffer) {
971
1005
  [self.eventBuffer appendEvent:event];
972
1006
  }
@@ -1008,17 +1042,15 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1008
1042
  return;
1009
1043
  }
1010
1044
 
1011
- // Throttle scroll events to avoid spamming the main thread/logs
1012
1045
  if ([gestureType hasPrefix:@"scroll"]) {
1013
1046
  static NSTimeInterval lastScrollLogTime = 0;
1014
1047
  NSTimeInterval now = CACurrentMediaTime();
1015
- if (now - lastScrollLogTime < 0.5) { // 500ms throttle
1048
+ if (now - lastScrollLogTime < 0.5) {
1016
1049
  return;
1017
1050
  }
1018
1051
  lastScrollLogTime = now;
1019
1052
  }
1020
1053
 
1021
- // Move ALL processing to the background state queue to unblock Main Thread
1022
1054
  dispatch_async(self.stateQueue, ^{
1023
1055
  @try {
1024
1056
  NSMutableDictionary *details =
@@ -1030,18 +1062,8 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1030
1062
  @"targetLabel" : targetLabel ?: [NSNull null]
1031
1063
  }];
1032
1064
 
1033
- // Log internal event (already on stateQueue, so safe)
1034
- // We call a simpler version that assumes we are already on background or
1035
- // handles it Actually logEventInternal dispatches TO stateQueue. Since we
1036
- // are ON stateQueue, we can call a direct helper or just logEventInternal
1037
- // (it will just dispatch_async again which is fine) But to be cleaner,
1038
- // let's just use logEventInternal but ensuring we don't do main thread
1039
- // dictionary work above. We moved the dictionary creation HERE
1040
- // (background).
1041
-
1042
1065
  [self logEventInternal:RJEventTypeGesture details:[details copy]];
1043
1066
 
1044
- // Notify engine on main thread (it's lightweight)
1045
1067
  dispatch_async(dispatch_get_main_queue(), ^{
1046
1068
  if (self.captureEngine) {
1047
1069
  [self.captureEngine notifyGesture:gestureType];
@@ -1111,7 +1133,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1111
1133
  registerDeviceWithProjectKey:publicKey
1112
1134
  bundleId:bundleId
1113
1135
  platform:@"ios"
1114
- sdkVersion:@"1.0.0"
1136
+ sdkVersion:RJSDKVersion
1115
1137
  apiUrl:apiUrl
1116
1138
  completion:^(BOOL success, NSString *credId,
1117
1139
  NSError *error) {
@@ -1123,11 +1145,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1123
1145
  RJLogError(@"Device registration failed: %@",
1124
1146
  error);
1125
1147
 
1126
- // SECURITY: Handle specific error codes
1127
- // 403 = Bundle ID mismatch or forbidden - PERMANENT
1128
- // failure 404 = Project not found - could be
1129
- // temporary, retry Other = Network/transient -
1130
- // retry
1131
1148
  if (error.code == 403) {
1132
1149
  RJLogError(@"SECURITY: Bundle ID mismatch or "
1133
1150
  @"access forbidden. "
@@ -1136,9 +1153,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1136
1153
  strongSelf.authPermanentlyFailed = YES;
1137
1154
  [strongSelf handleAuthenticationFailure:error];
1138
1155
  } else {
1139
- // For 404 and other errors, schedule retry with
1140
- // exponential backoff Recording continues locally
1141
- // - events queued for later upload
1142
1156
  [strongSelf scheduleAuthRetryWithError:error
1143
1157
  publicKey:publicKey
1144
1158
  apiUrl:apiUrl];
@@ -1146,7 +1160,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1146
1160
  } else {
1147
1161
  RJLogDebug(@"Device registered: %@", credId);
1148
1162
 
1149
- // Auth succeeded - reset retry state
1150
1163
  [strongSelf resetAuthRetryState];
1151
1164
 
1152
1165
  [strongSelf
@@ -1161,21 +1174,16 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1161
1174
  - (void)handleAuthenticationFailure:(NSError *)error {
1162
1175
  RJLogError(@"Authentication failure - stopping recording. Error: %@", error);
1163
1176
 
1164
- // Stop recording to prevent data accumulation that can't be uploaded
1165
1177
  dispatch_async(dispatch_get_main_queue(), ^{
1166
1178
  if (self.isRecording) {
1167
1179
  self.isRecording = NO;
1168
1180
 
1169
- // Stop capture engine
1170
1181
  if (self.captureEngine) {
1171
1182
  [self.captureEngine stopSession];
1172
1183
  }
1173
1184
 
1174
- // Clear auth data so next attempt starts fresh
1175
1185
  [[RJDeviceAuthManager sharedManager] clearAllAuthData];
1176
1186
 
1177
- // Notify JS layer about the failure (if bridge is available)
1178
- // This allows the app to handle the error (e.g., show user message)
1179
1187
  @try {
1180
1188
  if (self.bridge) {
1181
1189
  [self.bridge enqueueJSCall:@"RCTDeviceEventEmitter"
@@ -1202,22 +1210,18 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1202
1210
  - (void)scheduleAuthRetryWithError:(NSError *)error
1203
1211
  publicKey:(NSString *)publicKey
1204
1212
  apiUrl:(NSString *)apiUrl {
1205
- // Check if already permanently failed (403 security error)
1206
1213
  if (self.authPermanentlyFailed) {
1207
1214
  RJLogWarning(@"Auth permanently failed - not scheduling retry");
1208
1215
  return;
1209
1216
  }
1210
1217
 
1211
- // Increment retry count
1212
1218
  self.authRetryCount++;
1213
1219
 
1214
- // Check max retries
1215
1220
  if (self.authRetryCount > RJ_MAX_AUTH_RETRIES) {
1216
1221
  RJLogError(@"Auth failed after %ld retries. Recording continues locally, "
1217
1222
  @"events will be uploaded when auth succeeds.",
1218
1223
  (long)RJ_MAX_AUTH_RETRIES);
1219
1224
 
1220
- // Notify JS but DON'T stop recording - events queue locally
1221
1225
  dispatch_async(dispatch_get_main_queue(), ^{
1222
1226
  @try {
1223
1227
  if (self.bridge) {
@@ -1234,18 +1238,15 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1234
1238
  completion:nil];
1235
1239
  }
1236
1240
  } @catch (NSException *exception) {
1237
- // Ignore JS notification failures
1238
1241
  }
1239
1242
  });
1240
1243
 
1241
- // Schedule a much longer retry (5 minutes) to try again later
1242
1244
  [self scheduleBackgroundAuthRetryAfter:300.0
1243
1245
  publicKey:publicKey
1244
1246
  apiUrl:apiUrl];
1245
1247
  return;
1246
1248
  }
1247
1249
 
1248
- // Calculate exponential backoff delay: 2, 4, 8, 16, 32... capped at 60s
1249
1250
  NSTimeInterval delay =
1250
1251
  MIN(RJ_AUTH_RETRY_BASE_DELAY * pow(2, self.authRetryCount - 1),
1251
1252
  RJ_AUTH_RETRY_MAX_DELAY);
@@ -1263,7 +1264,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1263
1264
  - (void)scheduleBackgroundAuthRetryAfter:(NSTimeInterval)delay
1264
1265
  publicKey:(NSString *)publicKey
1265
1266
  apiUrl:(NSString *)apiUrl {
1266
- // Cancel any existing retry timer
1267
1267
  if (self.authRetryTimer) {
1268
1268
  [self.authRetryTimer invalidate];
1269
1269
  self.authRetryTimer = nil;
@@ -1296,14 +1296,11 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1296
1296
 
1297
1297
  RJLogInfo(@"Retrying auth (attempt %ld)...", (long)(self.authRetryCount + 1));
1298
1298
 
1299
- // After 2 failed attempts, clear cached auth data and re-register fresh
1300
- // This handles expired/corrupted tokens or server-side revocations
1301
1299
  if (self.authRetryCount >= 2) {
1302
1300
  RJLogInfo(@"Clearing cached auth data and re-registering fresh...");
1303
1301
  [[RJDeviceAuthManager sharedManager] clearAllAuthData];
1304
1302
  }
1305
1303
 
1306
- // Re-attempt device auth setup
1307
1304
  [self setupDeviceAuthWithPublicKey:publicKey apiUrl:apiUrl];
1308
1305
  }
1309
1306
 
@@ -1340,12 +1337,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1340
1337
  return;
1341
1338
  }
1342
1339
 
1343
- // Handle security errors (403 = forbidden/mismatch, 404 = not found)
1344
1340
  if ((error.code == 403 || error.code == 404) &&
1345
1341
  [error.domain isEqualToString:@"RJDeviceAuth"]) {
1346
1342
 
1347
1343
  if (!isRetry) {
1348
- // First failure - try re-registration
1349
1344
  RJLogDebug(@"Device auth invalid (%ld), attempting re-registration...",
1350
1345
  (long)error.code);
1351
1346
  NSString *bundleId =
@@ -1356,7 +1351,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1356
1351
  registerDeviceWithProjectKey:publicKey
1357
1352
  bundleId:bundleId
1358
1353
  platform:@"ios"
1359
- sdkVersion:@"1.0.0"
1354
+ sdkVersion:RJSDKVersion
1360
1355
  apiUrl:apiUrl
1361
1356
  completion:^(BOOL retrySuccess, NSString *credId,
1362
1357
  NSError *retryError) {
@@ -1372,8 +1367,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1372
1367
  apiUrl
1373
1368
  isRetry:YES];
1374
1369
  } else {
1375
- // Re-registration also failed - this is a
1376
- // security error
1377
1370
  RJLogError(@"Re-registration failed: %@",
1378
1371
  retryError);
1379
1372
  if (retryError.code == 403 ||
@@ -1384,12 +1377,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1384
1377
  }
1385
1378
  }];
1386
1379
  } else {
1387
- // Already retried - this is a persistent security error
1388
1380
  RJLogError(@"Token fetch failed after retry: %@", error);
1389
1381
  [strongSelf handleAuthenticationFailure:error];
1390
1382
  }
1391
1383
  } else {
1392
- // Network or other transient errors
1393
1384
  RJLogWarning(@"Failed to get upload token (transient): %@", error);
1394
1385
  }
1395
1386
  }];
@@ -1557,7 +1548,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1557
1548
  if (self.isShuttingDown || !eventType)
1558
1549
  return;
1559
1550
 
1560
- // Log gesture events specifically for debugging
1561
1551
  if ([eventType isEqualToString:@"gesture"]) {
1562
1552
  NSString *gestureType = details[@"gestureType"] ?: @"unknown";
1563
1553
  RJLogInfo(
@@ -1774,9 +1764,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1774
1764
  });
1775
1765
  }
1776
1766
 
1777
- /// Flush data with option to end session or keep it alive for resumption.
1778
- /// @param isFinal If YES, ends the session on backend. If NO, just uploads
1779
- /// pending data.
1780
1767
  - (void)flushDataWithCompletion:(RJCompletionHandler)completion
1781
1768
  isFinal:(BOOL)isFinal {
1782
1769
 
@@ -1834,7 +1821,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1834
1821
  return;
1835
1822
  }
1836
1823
 
1837
- // For non-final flushes, skip promotion evaluation and just upload
1838
1824
  if (!isFinal) {
1839
1825
  RJLogInfo(
1840
1826
  @"[RJ-FLUSH] Non-final background flush - uploading %lu "
@@ -1915,13 +1901,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1915
1901
  }
1916
1902
  }
1917
1903
 
1918
- /// Convenience method for final flush (ends session).
1919
1904
  - (void)flushAllDataWithCompletion:(RJCompletionHandler)completion {
1920
1905
  [self flushDataWithCompletion:completion isFinal:YES];
1921
1906
  }
1922
1907
 
1923
- /// Flush pending data for background transition without ending the session.
1924
- /// Used when app goes to background but may return quickly.
1925
1908
  - (void)flushDataForBackgroundWithCompletion:(RJCompletionHandler)completion {
1926
1909
  [self flushDataWithCompletion:completion isFinal:NO];
1927
1910
  }
@@ -1960,7 +1943,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1960
1943
  if (self.isRecording) {
1961
1944
  RJLogDebug(@"[KEYBOARD] Keyboard shown (height=%.0f)",
1962
1945
  keyboardFrame.size.height);
1963
- // Include keyboard height so web UI can render overlay
1964
1946
  [self logEventInternal:RJEventTypeKeyboardShow
1965
1947
  details:@{
1966
1948
  @"keyboardHeight" : @(keyboardFrame.size.height),
@@ -2020,9 +2002,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2020
2002
  @try {
2021
2003
  [self stopBatchUploadTimer];
2022
2004
 
2023
- // CRITICAL: Sync accumulated background time to uploadManager BEFORE any
2024
- // flush This ensures if the session ends during background, the correct
2025
- // time is included
2026
2005
  if (self.uploadManager && self.lifecycleManager) {
2027
2006
  NSTimeInterval currentBgTime =
2028
2007
  self.lifecycleManager.totalBackgroundTimeMs;
@@ -2032,14 +2011,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2032
2011
  currentBgTime);
2033
2012
  }
2034
2013
 
2035
- // Log background event FIRST and SYNCHRONOUSLY so it's included in the
2036
- // flush This must happen before we pause capture or start the flush
2037
2014
  NSMutableDictionary *bgEvent =
2038
2015
  [NSMutableDictionary dictionaryWithCapacity:3];
2039
2016
  bgEvent[@"type"] = RJEventTypeAppBackground;
2040
2017
  bgEvent[@"timestamp"] = @([RJWindowUtils currentTimestampMillis]);
2041
-
2042
- // Add directly to session events synchronously
2043
2018
  [self performStateSync:^{
2044
2019
  if (self.sessionEvents) {
2045
2020
  [self.sessionEvents addObject:bgEvent];
@@ -2049,17 +2024,12 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2049
2024
  }
2050
2025
  }];
2051
2026
 
2052
- // Also add to event buffer for persistence
2053
2027
  if (self.eventBuffer) {
2054
2028
  [self.eventBuffer appendEvent:bgEvent];
2055
2029
  }
2056
2030
 
2057
2031
  if (self.captureEngine) {
2058
2032
  @try {
2059
- // Use ASYNCHRONOUS pause for backgrounding.
2060
- // The encoder now implements internal UIBackgroundTask protection, so
2061
- // it will finish finalizing the MP4 on a background queue without
2062
- // locking the UI.
2063
2033
  RJLogInfo(@"[RJ-VIDEO] Pausing video capture for background (ASYNC)");
2064
2034
  [self.captureEngine pauseVideoCapture];
2065
2035
  RJLogInfo(@"[RJ-VIDEO] Video capture pause initiated");
@@ -2074,9 +2044,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2074
2044
  beginBackgroundTaskWithName:@"RejourneySessionFlush"];
2075
2045
  }
2076
2046
 
2077
- // Use non-final flush for background - session may resume if user returns
2078
- // quickly This avoids calling session/end which would prevent frame capture
2079
- // after returning
2080
2047
  RJLogInfo(@"[RJ-FLUSH] Starting non-final background flush (session will "
2081
2048
  @"resume if user returns)");
2082
2049
  [self flushDataForBackgroundWithCompletion:^(BOOL success) {
@@ -2129,13 +2096,9 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2129
2096
  @try {
2130
2097
  [self stopBatchUploadTimer];
2131
2098
 
2132
- // CRITICAL: Calculate and sync background time before ending session
2133
- // If we're being terminated while in background, we need to include
2134
- // the current background duration in the session's total background time
2135
2099
  if (self.uploadManager && self.lifecycleManager) {
2136
2100
  NSTimeInterval totalBgTime = self.lifecycleManager.totalBackgroundTimeMs;
2137
2101
 
2138
- // If we're currently in background, add the ongoing duration
2139
2102
  if (self.lifecycleManager.isInBackground &&
2140
2103
  self.lifecycleManager.backgroundEntryTime > 0) {
2141
2104
  NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
@@ -2155,15 +2118,12 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2155
2118
  }
2156
2119
 
2157
2120
  if (self.captureEngine) {
2158
- // Use SYNCHRONOUS stop to ensure segment is finished and upload is
2159
- // triggered before we try to upload events. This prevents video segments
2160
- // from being lost when app is terminated.
2121
+
2161
2122
  RJLogInfo(@"[RJ-TERMINATE] Stopping capture engine synchronously");
2162
2123
  [self.captureEngine stopSessionSync];
2163
2124
  RJLogInfo(@"[RJ-TERMINATE] Capture engine stopped");
2164
2125
  }
2165
2126
 
2166
- // Add terminated event SYNCHRONOUSLY before we copy events
2167
2127
  NSMutableDictionary *terminateEvent =
2168
2128
  [NSMutableDictionary dictionaryWithCapacity:3];
2169
2129
  terminateEvent[@"type"] = RJEventTypeAppTerminated;
@@ -2192,9 +2152,9 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2192
2152
 
2193
2153
  if (self.uploadManager && events.count > 0) {
2194
2154
  @try {
2195
- [self.uploadManager synchronousUploadWithEvents:events ?: @[]];
2155
+ [self.uploadManager persistTerminationEvents:events ?: @[]];
2196
2156
  } @catch (NSException *e) {
2197
- RJLogWarning(@"Terminate upload failed: %@", e);
2157
+ RJLogWarning(@"Terminate persistence failed: %@", e);
2198
2158
  }
2199
2159
  }
2200
2160
  } @catch (NSException *exception) {
@@ -2219,7 +2179,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2219
2179
  @try {
2220
2180
  NSTimeInterval bgTimeMs = self.lifecycleManager.totalBackgroundTimeMs;
2221
2181
 
2222
- // Keep local state in sync so stopSession/flush paths can rely on it.
2223
2182
  self.totalBackgroundTimeMs = bgTimeMs;
2224
2183
 
2225
2184
  if (self.uploadManager) {
@@ -2259,16 +2218,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2259
2218
  [self handleSessionTimeout:backgroundDuration currentTime:currentTime];
2260
2219
  }
2261
2220
 
2262
- /// Handle session timeout after extended background period.
2263
- /// This cleanly ends the old session and starts a fresh one.
2264
- ///
2265
- /// Flow:
2266
- /// 1. Capture final background time from lifecycle manager
2267
- /// 2. Stop timers and capture engine for old session
2268
- /// 3. Synchronously end the old session with correct background time
2269
- /// 4. Create new session ID and reset all state
2270
- /// 5. Start capture for new session
2271
- /// 6. Trigger immediate upload to register new session
2272
2221
  - (void)handleSessionTimeout:(NSTimeInterval)backgroundDuration
2273
2222
  currentTime:(NSTimeInterval)currentTime {
2274
2223
  RJLogInfo(
@@ -2282,8 +2231,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2282
2231
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] === ENDING OLD SESSION: %@ ===",
2283
2232
  oldSessionId);
2284
2233
 
2285
- // ========== STEP 1: Capture background time BEFORE any state changes
2286
- // ==========
2287
2234
  NSTimeInterval totalBackgroundMs = 0;
2288
2235
  if (self.lifecycleManager) {
2289
2236
  totalBackgroundMs = self.lifecycleManager.totalBackgroundTimeMs;
@@ -2292,7 +2239,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2292
2239
  totalBackgroundMs);
2293
2240
  }
2294
2241
 
2295
- // ========== STEP 2: Stop all capture/timers for old session ==========
2296
2242
  [self stopBatchUploadTimer];
2297
2243
  [self stopDurationLimitTimer];
2298
2244
 
@@ -2305,14 +2251,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2305
2251
  }
2306
2252
  }
2307
2253
 
2308
- // ========== STEP 3: End old session SYNCHRONOUSLY with correct background
2309
- // time ==========
2310
2254
  if (wasRecording && self.uploadManager && oldSessionId.length > 0 &&
2311
2255
  ![oldSessionId isEqualToString:@"none"]) {
2312
- // Set background time on upload manager
2313
2256
  self.uploadManager.totalBackgroundTimeMs = totalBackgroundMs;
2314
2257
 
2315
- // Get current events for final upload
2316
2258
  __block NSArray<NSDictionary *> *finalEvents = nil;
2317
2259
  [self performStateSync:^{
2318
2260
  finalEvents = [self.sessionEvents copy];
@@ -2322,18 +2264,15 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2322
2264
  @"bgTime=%.0fms",
2323
2265
  (unsigned long)finalEvents.count, totalBackgroundMs);
2324
2266
 
2325
- // Synchronous upload and session end
2326
2267
  if (finalEvents.count > 0) {
2327
2268
  [self.uploadManager synchronousUploadWithEvents:finalEvents];
2328
2269
  } else {
2329
- // Even with no events, end the session to record background time
2330
2270
  [self.uploadManager endSessionSync];
2331
2271
  }
2332
2272
 
2333
2273
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] Old session %@ ended", oldSessionId);
2334
2274
  }
2335
2275
 
2336
- // ========== STEP 4: Reset ALL state for new session ==========
2337
2276
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] === STARTING NEW SESSION ===");
2338
2277
 
2339
2278
  __block NSString *newSessionId = nil;
@@ -2343,8 +2282,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2343
2282
  self.sessionStartTime = currentTime;
2344
2283
  self.totalBackgroundTimeMs = 0;
2345
2284
  [self.sessionEvents removeAllObjects];
2346
-
2347
- // Persist for crash recovery
2348
2285
  [[NSUserDefaults standardUserDefaults]
2349
2286
  setObject:newSessionId
2350
2287
  forKey:@"rj_current_session_id"];
@@ -2353,7 +2290,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2353
2290
 
2354
2291
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] New session ID: %@", newSessionId);
2355
2292
 
2356
- // Reset upload manager for new session
2357
2293
  if (self.uploadManager) {
2358
2294
  @try {
2359
2295
  [self.uploadManager resetForNewSession];
@@ -2365,7 +2301,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2365
2301
  self.uploadManager.sessionStartTime = currentTime;
2366
2302
  self.uploadManager.totalBackgroundTimeMs = 0;
2367
2303
 
2368
- // Preserve user identity
2369
2304
  if (!self.userId) {
2370
2305
  self.userId = [[NSUserDefaults standardUserDefaults]
2371
2306
  stringForKey:@"rj_user_identity"];
@@ -2373,17 +2308,14 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2373
2308
  self.uploadManager.userId = self.userId ?: @"anonymous";
2374
2309
  }
2375
2310
 
2376
- // Reset lifecycle manager background tracking
2377
2311
  if (self.lifecycleManager) {
2378
2312
  [self.lifecycleManager resetBackgroundTime];
2379
2313
  self.lifecycleManager.isRecording = YES;
2380
2314
  }
2381
2315
 
2382
- // Reset event buffer for new session - recreate since sessionId is readonly
2383
2316
  if (self.eventBuffer) {
2384
2317
  [self.eventBuffer clearAllEvents];
2385
2318
  }
2386
- // Create new event buffer for new session (sessionId is readonly)
2387
2319
  NSString *pendingPath = self.eventBuffer.pendingRootPath;
2388
2320
  if (pendingPath.length == 0) {
2389
2321
  pendingPath =
@@ -2394,7 +2326,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2394
2326
  self.eventBuffer = [[RJEventBuffer alloc] initWithSessionId:newSessionId
2395
2327
  pendingRootPath:pendingPath];
2396
2328
 
2397
- // ========== STEP 5: Start capture for new session ==========
2398
2329
  [self resetSamplingDecision];
2399
2330
  self.remoteRecordingEnabled = self.recordingEnabledByConfig;
2400
2331
  if (self.captureEngine) {
@@ -2417,20 +2348,14 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2417
2348
  }
2418
2349
 
2419
2350
  self.isRecording = YES;
2420
-
2421
- // Verify touch tracking
2422
2351
  RJTouchInterceptor *touchInterceptor = [RJTouchInterceptor sharedInstance];
2423
2352
  if (touchInterceptor && !touchInterceptor.isTrackingEnabled) {
2424
2353
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] Re-enabling touch tracking");
2425
2354
  [self setupTouchTracking];
2426
2355
  }
2427
-
2428
- // Start timers for new session
2429
2356
  [self startBatchUploadTimer];
2430
2357
  [self startDurationLimitTimer];
2431
2358
 
2432
- // ========== STEP 6: Log session_start and trigger immediate upload
2433
- // ==========
2434
2359
  NSMutableDictionary *sessionStartEvent = [NSMutableDictionary dictionary];
2435
2360
  sessionStartEvent[@"type"] = RJEventTypeSessionStart;
2436
2361
  sessionStartEvent[@"timestamp"] = @([RJWindowUtils currentTimestampMillis]);
@@ -2447,7 +2372,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2447
2372
  [self.eventBuffer appendEvent:sessionStartEvent];
2448
2373
  }
2449
2374
 
2450
- // Immediate upload to register session with backend
2451
2375
  dispatch_after(
2452
2376
  dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
2453
2377
  dispatch_get_main_queue(), ^{
@@ -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
  *