@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.
Files changed (39) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
  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/ios/Capture/RJViewHierarchyScanner.m +68 -51
  15. package/ios/Core/RJLifecycleManager.m +0 -14
  16. package/ios/Core/Rejourney.mm +24 -37
  17. package/ios/Network/RJDeviceAuthManager.m +0 -2
  18. package/ios/Network/RJUploadManager.h +8 -0
  19. package/ios/Network/RJUploadManager.m +45 -0
  20. package/ios/Privacy/RJPrivacyMask.m +5 -31
  21. package/ios/Rejourney.h +0 -14
  22. package/ios/Touch/RJTouchInterceptor.m +21 -15
  23. package/ios/Utils/RJEventBuffer.m +57 -69
  24. package/ios/Utils/RJWindowUtils.m +87 -86
  25. package/lib/commonjs/index.js +42 -30
  26. package/lib/commonjs/sdk/autoTracking.js +0 -3
  27. package/lib/commonjs/sdk/networkInterceptor.js +0 -11
  28. package/lib/commonjs/sdk/utils.js +73 -14
  29. package/lib/module/index.js +42 -30
  30. package/lib/module/sdk/autoTracking.js +0 -3
  31. package/lib/module/sdk/networkInterceptor.js +0 -11
  32. package/lib/module/sdk/utils.js +73 -14
  33. package/lib/typescript/sdk/utils.d.ts +31 -1
  34. package/package.json +16 -4
  35. package/src/index.ts +40 -19
  36. package/src/sdk/autoTracking.ts +0 -2
  37. package/src/sdk/constants.ts +13 -13
  38. package/src/sdk/networkInterceptor.ts +0 -9
  39. package/src/sdk/utils.ts +76 -14
@@ -23,110 +23,111 @@
23
23
 
24
24
  @implementation RJWindowUtils
25
25
 
26
+ static __weak UIWindow *_cachedKeyWindow = nil;
27
+ static NSTimeInterval _lastCacheTime = 0;
28
+ static const NSTimeInterval kKeyWindowCacheTTL = 0.5; // Cache for 500ms
29
+
26
30
  + (UIWindow *)keyWindow {
27
- if (@available(iOS 13.0, *)) {
28
- // IMPORTANT:
29
- // "Key window" can temporarily become a system window such as:
30
- // - UITextEffectsWindow
31
- // - UIRemoteKeyboardWindow / UIInputWindowController-hosted windows
32
- // Capturing those can trigger keyboard autolayout warnings/spikes even if the
33
- // user never focused a text field. Prefer a normal-level app window.
34
- //
35
- // ALSO IMPORTANT:
36
- // Some frameworks (including React Native/Expo modals/overlays) may present
37
- // content in a separate UIWindow with a windowLevel > UIWindowLevelNormal.
38
- // If we only consider "normal" level, we may capture the wrong window and
39
- // miss TextInputs entirely (privacy masking failure).
40
- //
41
- // We therefore prefer the TOP-MOST visible non-system window, while still
42
- // excluding keyboard/text-effects windows.
43
-
44
- UIWindow *bestKeyApp = nil;
45
- CGFloat bestKeyAppLevel = -CGFLOAT_MAX;
46
- UIWindow *bestApp = nil;
47
- CGFloat bestAppLevel = -CGFLOAT_MAX;
48
- UIWindow *anyKey = nil;
49
-
50
- for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
51
- if (![scene isKindOfClass:[UIWindowScene class]]) {
52
- continue;
53
- }
54
- UIWindowScene *windowScene = (UIWindowScene *)scene;
55
- if (windowScene.activationState != UISceneActivationStateForegroundActive) {
56
- continue;
57
- }
58
-
59
- for (UIWindow *window in windowScene.windows) {
60
- if (!window || window.isHidden || window.alpha <= 0.01) {
61
- continue;
62
- }
63
-
64
- NSString *cls = NSStringFromClass([window class]);
65
- BOOL isSystemInputWindow =
66
- ([cls containsString:@"Keyboard"] ||
67
- [cls containsString:@"TextEffects"] ||
68
- [cls containsString:@"InputWindow"] ||
69
- [cls containsString:@"RemoteKeyboard"]);
70
-
71
- BOOL hasRoot = (window.rootViewController != nil);
72
- BOOL isAppCandidate = (!isSystemInputWindow && hasRoot);
73
- CGFloat level = window.windowLevel;
74
-
75
- if (window.isKeyWindow) {
76
- if (!anyKey) {
77
- anyKey = window;
78
- }
79
- if (isAppCandidate && level > bestKeyAppLevel) {
80
- bestKeyAppLevel = level;
81
- bestKeyApp = window;
82
- }
83
- }
84
-
85
- if (isAppCandidate && level > bestAppLevel) {
86
- bestAppLevel = level;
87
- bestApp = window;
88
- }
89
- }
90
- }
31
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
32
+ if (_cachedKeyWindow && (now - _lastCacheTime < kKeyWindowCacheTTL)) {
33
+ return _cachedKeyWindow;
34
+ }
35
+
36
+ UIWindow *window = [self findKeyWindowInternal];
37
+ _cachedKeyWindow = window;
38
+ _lastCacheTime = now;
39
+ return window;
40
+ }
41
+
42
+ + (UIWindow *)findKeyWindowInternal {
43
+ if (@available(iOS 13.0, *)) {
44
+ UIWindow *bestKeyApp = nil;
45
+ CGFloat bestKeyAppLevel = -CGFLOAT_MAX;
46
+ UIWindow *bestApp = nil;
47
+ CGFloat bestAppLevel = -CGFLOAT_MAX;
48
+ UIWindow *anyKey = nil;
91
49
 
92
- if (bestKeyApp) {
93
- return bestKeyApp;
50
+ for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
51
+ if (![scene isKindOfClass:[UIWindowScene class]]) {
52
+ continue;
53
+ }
54
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
55
+ if (windowScene.activationState !=
56
+ UISceneActivationStateForegroundActive) {
57
+ continue;
58
+ }
59
+
60
+ for (UIWindow *window in windowScene.windows) {
61
+ if (!window || window.isHidden || window.alpha <= 0.01) {
62
+ continue;
94
63
  }
95
- if (bestApp) {
96
- return bestApp;
64
+
65
+ NSString *cls = NSStringFromClass([window class]);
66
+ BOOL isSystemInputWindow = ([cls containsString:@"Keyboard"] ||
67
+ [cls containsString:@"TextEffects"] ||
68
+ [cls containsString:@"InputWindow"] ||
69
+ [cls containsString:@"RemoteKeyboard"]);
70
+
71
+ BOOL hasRoot = (window.rootViewController != nil);
72
+ BOOL isAppCandidate = (!isSystemInputWindow && hasRoot);
73
+ CGFloat level = window.windowLevel;
74
+
75
+ if (window.isKeyWindow) {
76
+ if (!anyKey) {
77
+ anyKey = window;
78
+ }
79
+ if (isAppCandidate && level > bestKeyAppLevel) {
80
+ bestKeyAppLevel = level;
81
+ bestKeyApp = window;
82
+ }
97
83
  }
98
- if (anyKey) {
99
- return anyKey;
84
+
85
+ if (isAppCandidate && level > bestAppLevel) {
86
+ bestAppLevel = level;
87
+ bestApp = window;
100
88
  }
101
- } else {
89
+ }
90
+ }
91
+
92
+ if (bestKeyApp) {
93
+ return bestKeyApp;
94
+ }
95
+ if (bestApp) {
96
+ return bestApp;
97
+ }
98
+ if (anyKey) {
99
+ return anyKey;
100
+ }
101
+ } else {
102
102
  #pragma clang diagnostic push
103
103
  #pragma clang diagnostic ignored "-Wdeprecated-declarations"
104
- return [UIApplication sharedApplication].keyWindow;
104
+ return [UIApplication sharedApplication].keyWindow;
105
105
  #pragma clang diagnostic pop
106
- }
107
- return nil;
106
+ }
107
+ return nil;
108
108
  }
109
109
 
110
110
  + (NSString *)accessibilityLabelForView:(UIView *)view {
111
- UIView *current = view;
112
- while (current) {
113
- if (current.accessibilityLabel.length > 0) {
114
- return current.accessibilityLabel;
115
- }
116
- current = current.superview;
111
+ UIView *current = view;
112
+ while (current) {
113
+ if (current.accessibilityLabel.length > 0) {
114
+ return current.accessibilityLabel;
117
115
  }
118
- return nil;
116
+ current = current.superview;
117
+ }
118
+ return nil;
119
119
  }
120
120
 
121
121
  + (NSString *)generateSessionId {
122
- NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
123
- NSString *timestampStr = [NSString stringWithFormat:@"%.0f", timestamp * 1000];
124
- NSString *randomHex = [NSString stringWithFormat:@"%08X", arc4random()];
125
- return [NSString stringWithFormat:@"session_%@_%@", timestampStr, randomHex];
122
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
123
+ NSString *timestampStr =
124
+ [NSString stringWithFormat:@"%.0f", timestamp * 1000];
125
+ NSString *randomHex = [NSString stringWithFormat:@"%08X", arc4random()];
126
+ return [NSString stringWithFormat:@"session_%@_%@", timestampStr, randomHex];
126
127
  }
127
128
 
128
129
  + (NSTimeInterval)currentTimestampMillis {
129
- return [[NSDate date] timeIntervalSince1970] * 1000;
130
+ return [[NSDate date] timeIntervalSince1970] * 1000;
130
131
  }
131
132
 
132
133
  @end
@@ -194,7 +194,12 @@ function getLogger() {
194
194
  logRecordingStart: () => {},
195
195
  logRecordingRemoteDisabled: () => {},
196
196
  logInvalidProjectKey: () => {},
197
- logPackageMismatch: () => {}
197
+ logPackageMismatch: () => {},
198
+ logNetworkRequest: () => {},
199
+ logFrustration: () => {},
200
+ logError: () => {},
201
+ logUploadStats: () => {},
202
+ logLifecycleEvent: () => {}
198
203
  };
199
204
  }
200
205
  try {
@@ -219,7 +224,12 @@ function getLogger() {
219
224
  logRecordingStart: () => {},
220
225
  logRecordingRemoteDisabled: () => {},
221
226
  logInvalidProjectKey: () => {},
222
- logPackageMismatch: () => {}
227
+ logPackageMismatch: () => {},
228
+ logNetworkRequest: () => {},
229
+ logFrustration: () => {},
230
+ logError: () => {},
231
+ logUploadStats: () => {},
232
+ logLifecycleEvent: () => {}
223
233
  };
224
234
  }
225
235
  }
@@ -276,6 +286,7 @@ function getAutoTracking() {
276
286
  let _isInitialized = false;
277
287
  let _isRecording = false;
278
288
  let _initializationFailed = false;
289
+ let _metricsInterval = null;
279
290
  let _appStateSubscription = null;
280
291
  let _authErrorSubscription = null;
281
292
  let _currentAppState = 'active'; // Default to active, will be updated on init
@@ -478,18 +489,12 @@ const Rejourney = {
478
489
  const apiUrl = _storedConfig.apiUrl || 'https://api.rejourney.co';
479
490
  const publicKey = _storedConfig.publicRouteKey || '';
480
491
  getLogger().debug(`Calling native startSession (apiUrl=${apiUrl})`);
481
-
482
- // Use user identity if set, otherwise use anonymous device ID
483
492
  const deviceId = await getAutoTracking().ensurePersistentAnonymousId();
484
-
485
- // Try to load persisted user identity if not already set in memory
486
493
  if (!_userIdentity) {
487
494
  _userIdentity = await loadPersistedUserIdentity();
488
495
  }
489
496
  const userId = _userIdentity || deviceId;
490
497
  getLogger().debug(`userId=${userId.substring(0, 8)}...`);
491
-
492
- // Start native session
493
498
  const result = await nativeModule.startSession(userId, apiUrl, publicKey);
494
499
  getLogger().debug('Native startSession returned:', JSON.stringify(result));
495
500
  if (!result?.success) {
@@ -502,10 +507,27 @@ const Rejourney = {
502
507
  }
503
508
  _isRecording = true;
504
509
  getLogger().debug(`✅ Session started: ${result.sessionId}`);
505
- // Use lifecycle log for session start - only shown in dev builds
506
510
  getLogger().logSessionStart(result.sessionId);
507
-
508
- // Initialize auto tracking features
511
+ // Start polling for upload stats in dev mode
512
+ if (__DEV__) {
513
+ _metricsInterval = setInterval(async () => {
514
+ if (!_isRecording) {
515
+ if (_metricsInterval) clearInterval(_metricsInterval);
516
+ return;
517
+ }
518
+ try {
519
+ const native = getRejourneyNative();
520
+ if (native) {
521
+ const metrics = await native.getSDKMetrics();
522
+ if (metrics) {
523
+ getLogger().logUploadStats(metrics);
524
+ }
525
+ }
526
+ } catch (e) {
527
+ getLogger().debug('Failed to fetch metrics:', e);
528
+ }
529
+ }, 10000); // Poll more frequently in dev (10s) for better feedback
530
+ }
509
531
  getAutoTracking().initAutoTracking({
510
532
  rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
511
533
  rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
@@ -523,7 +545,7 @@ const Rejourney = {
523
545
  x,
524
546
  y
525
547
  });
526
- // logger.debug(`Rage tap detected: ${count} taps at (${x}, ${y})`);
548
+ getLogger().logFrustration(`Rage tap (${count} taps)`);
527
549
  },
528
550
  // Error callback - log as error event
529
551
  onError: error => {
@@ -532,17 +554,10 @@ const Rejourney = {
532
554
  stack: error.stack,
533
555
  name: error.name
534
556
  });
535
- // logger.debug(`Error captured: ${error.message}`);
557
+ getLogger().logError(error.message);
536
558
  },
537
- // Screen change callback - log screen change
538
- onScreen: (_screenName, _previousScreen) => {
539
- // Native module already handles screen changes
540
- // This is just for metrics tracking
541
- // logger.debug(`Screen changed: ${previousScreen} -> ${screenName}`);
542
- }
559
+ onScreen: (_screenName, _previousScreen) => {}
543
560
  });
544
-
545
- // Collect and log device info
546
561
  if (_storedConfig?.collectDeviceInfo !== false) {
547
562
  try {
548
563
  const deviceInfo = await getAutoTracking().collectDeviceInfo();
@@ -551,13 +566,10 @@ const Rejourney = {
551
566
  getLogger().warn('Failed to collect device info:', deviceError);
552
567
  }
553
568
  }
554
-
555
- // Setup automatic network interception
556
569
  if (_storedConfig?.autoTrackNetwork !== false) {
557
570
  try {
558
571
  const ignoreUrls = [apiUrl, '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
559
572
  getNetworkInterceptor().initNetworkInterceptor(request => {
560
- this.logNetworkRequest(request);
561
573
  getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
562
574
  }, {
563
575
  ignoreUrls,
@@ -589,6 +601,10 @@ const Rejourney = {
589
601
  getAutoTracking().cleanupAutoTracking();
590
602
  getAutoTracking().resetMetrics();
591
603
  await safeNativeCall('stopSession', () => getRejourneyNative().stopSession(), undefined);
604
+ if (_metricsInterval) {
605
+ clearInterval(_metricsInterval);
606
+ _metricsInterval = null;
607
+ }
592
608
  _isRecording = false;
593
609
  getLogger().logSessionEnd('current');
594
610
  } catch (error) {
@@ -605,7 +621,6 @@ const Rejourney = {
605
621
  */
606
622
  logEvent(name, properties) {
607
623
  safeNativeCallSync('logEvent', () => {
608
- // Fire and forget - don't await
609
624
  getRejourneyNative().logEvent(name, properties || {}).catch(() => {});
610
625
  }, undefined);
611
626
  },
@@ -733,7 +748,6 @@ const Rejourney = {
733
748
  * @returns Path to export file (not implemented)
734
749
  */
735
750
  async exportSession(_sessionId) {
736
- // Return empty string - actual export should be done from dashboard server
737
751
  getLogger().warn('exportSession not implemented - export from dashboard server');
738
752
  return '';
739
753
  },
@@ -955,8 +969,6 @@ const Rejourney = {
955
969
  errorMessage: request.errorMessage,
956
970
  cached: request.cached
957
971
  };
958
-
959
- // Fire and forget - don't await, this is low priority
960
972
  getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
961
973
  }, undefined);
962
974
  },
@@ -1053,10 +1065,10 @@ function handleAppStateChange(nextAppState) {
1053
1065
  try {
1054
1066
  if (_currentAppState.match(/active/) && nextAppState === 'background') {
1055
1067
  // App going to background - native module handles this automatically
1056
- getLogger().debug('App moving to background');
1068
+ getLogger().logLifecycleEvent('App moving to background');
1057
1069
  } else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
1058
1070
  // App coming back to foreground
1059
- getLogger().debug('App returning to foreground');
1071
+ getLogger().logLifecycleEvent('App returning to foreground');
1060
1072
  }
1061
1073
  _currentAppState = nextAppState;
1062
1074
  } catch (error) {
@@ -479,9 +479,6 @@ function setupNavigationTracking() {
479
479
  if (__DEV__) {
480
480
  _utils.logger.debug('Setting up navigation tracking...');
481
481
  }
482
-
483
- // Delay to ensure navigation is initialized - Expo Router needs more time
484
- // We retry a few times with increasing delays
485
482
  let attempts = 0;
486
483
  const maxAttempts = 5;
487
484
  const trySetup = () => {
@@ -126,8 +126,6 @@ function queueRequest(request) {
126
126
  function flushPendingRequests() {
127
127
  flushTimer = null;
128
128
  if (!logCallback || pendingCount === 0) return;
129
-
130
- // Process all pending requests
131
129
  while (pendingCount > 0) {
132
130
  const request = pendingRequests[pendingHead];
133
131
  pendingRequests[pendingHead] = null; // Allow GC
@@ -195,14 +193,9 @@ function interceptFetch() {
195
193
  if (!shouldSampleRequest(path)) {
196
194
  return originalFetch(input, init);
197
195
  }
198
-
199
- // Capture start time (only synchronous work)
200
196
  const startTime = Date.now();
201
197
  const method = (init?.method || 'GET').toUpperCase();
202
-
203
- // Call original fetch
204
198
  return originalFetch(input, init).then(response => {
205
- // Success - queue the log asynchronously
206
199
  queueRequest({
207
200
  requestId: `f${startTime}`,
208
201
  method,
@@ -253,8 +246,6 @@ function interceptXHR() {
253
246
  if (!config.enabled || !logCallback || !data || shouldIgnoreUrl(data.u)) {
254
247
  return originalXHRSend.call(this, body);
255
248
  }
256
-
257
- // Check sampling
258
249
  const {
259
250
  path
260
251
  } = parseUrlFast(data.u);
@@ -303,8 +294,6 @@ function initNetworkInterceptor(callback, options) {
303
294
  */
304
295
  function disableNetworkInterceptor() {
305
296
  config.enabled = false;
306
-
307
- // Flush any pending requests
308
297
  if (flushTimer) {
309
298
  clearTimeout(flushTimer);
310
299
  flushTimer = null;
@@ -243,7 +243,6 @@ let LogLevel = exports.LogLevel = /*#__PURE__*/function (LogLevel) {
243
243
  */
244
244
  class Logger {
245
245
  prefix = '[Rejourney]';
246
- debugMode = false;
247
246
 
248
247
  /**
249
248
  * Minimum log level to display.
@@ -264,7 +263,6 @@ class Logger {
264
263
  this.minimumLogLevel = level;
265
264
  }
266
265
  setDebugMode(enabled) {
267
- this.debugMode = enabled;
268
266
  this.minimumLogLevel = enabled ? LogLevel.DEBUG : typeof __DEV__ !== 'undefined' && __DEV__ ? LogLevel.ERROR : LogLevel.SILENT;
269
267
  }
270
268
 
@@ -285,14 +283,26 @@ class Logger {
285
283
  /** Log a warning message */
286
284
  warn(...args) {
287
285
  if (this.minimumLogLevel <= LogLevel.WARNING) {
288
- console.warn(this.prefix, ...args);
286
+ if (this.minimumLogLevel <= LogLevel.DEBUG) {
287
+ // Explicit Debug Mode: Show YellowBox
288
+ console.warn(this.prefix, ...args);
289
+ } else {
290
+ // Default Dev Mode: Log to console only, avoid YellowBox
291
+ console.log(this.prefix, '[WARN]', ...args);
292
+ }
289
293
  }
290
294
  }
291
295
 
292
296
  /** Log an error message */
293
297
  error(...args) {
294
298
  if (this.minimumLogLevel <= LogLevel.ERROR) {
295
- console.error(this.prefix, ...args);
299
+ if (this.minimumLogLevel <= LogLevel.DEBUG) {
300
+ // Explicit Debug Mode: Show RedBox
301
+ console.error(this.prefix, ...args);
302
+ } else {
303
+ // Default Dev Mode: Log to console only, avoid RedBox
304
+ console.log(this.prefix, '[ERROR]', ...args);
305
+ }
296
306
  }
297
307
  }
298
308
  notice(...args) {
@@ -306,9 +316,7 @@ class Logger {
306
316
  * Only shown in development builds - this is the minimal "SDK started" log.
307
317
  */
308
318
  logInitSuccess(version) {
309
- if (this.debugMode) {
310
- this.info(`✓ SDK initialized (v${version})`);
311
- }
319
+ this.notice(`✓ SDK initialized (v${version})`);
312
320
  }
313
321
 
314
322
  /**
@@ -324,9 +332,7 @@ class Logger {
324
332
  * Only shown in development builds.
325
333
  */
326
334
  logSessionStart(sessionId) {
327
- if (this.debugMode) {
328
- this.info(`Session started: ${sessionId}`);
329
- }
335
+ this.notice(`Session started: ${sessionId}`);
330
336
  }
331
337
 
332
338
  /**
@@ -334,12 +340,10 @@ class Logger {
334
340
  * Only shown in development builds.
335
341
  */
336
342
  logSessionEnd(sessionId) {
337
- if (this.debugMode) {
338
- this.info(`Session ended: ${sessionId}`);
339
- }
343
+ this.notice(`Session ended: ${sessionId}`);
340
344
  }
341
345
  logObservabilityStart() {
342
- this.notice('Starting Rejourney observability');
346
+ this.notice('💧 Starting Rejourney observability');
343
347
  }
344
348
  logRecordingStart() {
345
349
  this.notice('Starting recording');
@@ -353,6 +357,61 @@ class Logger {
353
357
  logPackageMismatch() {
354
358
  this.notice('Bundle ID / package name mismatch');
355
359
  }
360
+
361
+ /**
362
+ * Log network request details
363
+ */
364
+ logNetworkRequest(request) {
365
+ const statusIcon = request.error || request.statusCode && request.statusCode >= 400 ? '🔴' : '🟢';
366
+ const method = request.method || 'GET';
367
+ // Shorten URL to just path if possible
368
+ let url = request.url || '';
369
+ try {
370
+ if (url.startsWith('http')) {
371
+ const urlObj = new URL(url);
372
+ url = urlObj.pathname;
373
+ }
374
+ } catch {
375
+ // Keep full URL if parsing fails
376
+ }
377
+ const duration = request.duration ? `(${Math.round(request.duration)}ms)` : '';
378
+ const status = request.statusCode ? `${request.statusCode}` : 'ERR';
379
+ this.notice(`${statusIcon} [NET] ${status} ${method} ${url} ${duration} ${request.error ? `Error: ${request.error}` : ''}`);
380
+ }
381
+
382
+ /**
383
+ * Log frustration event (rage taps, etc)
384
+ */
385
+ logFrustration(kind) {
386
+ this.notice(`🤬 Frustration detected: ${kind}`);
387
+ }
388
+
389
+ /**
390
+ * Log error captured by SDK
391
+ */
392
+ logError(message) {
393
+ this.notice(`X Error captured: ${message}`);
394
+ }
395
+
396
+ /**
397
+ * Log lifecycle event (Background/Foreground)
398
+ * Visible in development builds.
399
+ */
400
+ logLifecycleEvent(event) {
401
+ this.notice(`🔄 Lifecycle: ${event}`);
402
+ }
403
+
404
+ /**
405
+ * Log upload statistics
406
+ */
407
+ logUploadStats(metrics) {
408
+ const success = metrics.uploadSuccessCount;
409
+ const failed = metrics.uploadFailureCount;
410
+ const bytes = formatBytes(metrics.totalBytesUploaded);
411
+
412
+ // Always show in dev mode for reassurance, even if 0
413
+ this.notice(`📡 Upload Stats: ${success} success, ${failed} failed (${bytes} uploaded)`);
414
+ }
356
415
  }
357
416
  const logger = exports.logger = new Logger();
358
417
  //# sourceMappingURL=utils.js.map