@luciq/react-native 19.4.0 → 19.6.0-51917-SNAPSHOT

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 (69) hide show
  1. package/.claude/agents/codebase-analyzer.md +33 -0
  2. package/.claude/agents/codebase-locator.md +42 -0
  3. package/.claude/agents/codebase-pattern-finder.md +40 -0
  4. package/.claude/commands/apply-pr-reviews.md +253 -0
  5. package/.claude/commands/create-jira-workitem.md +27 -0
  6. package/.claude/commands/create-pr.md +138 -0
  7. package/.claude/commands/create-public-release-notes.md +145 -0
  8. package/.claude/commands/create-rca.md +286 -0
  9. package/.claude/commands/debug-sdk.md +66 -0
  10. package/.claude/commands/describe-pr.md +40 -0
  11. package/.claude/commands/new-api.md +60 -0
  12. package/.claude/commands/new-feature.md +75 -0
  13. package/.claude/commands/pr-review.md +85 -0
  14. package/.claude/commands/research-codebase.md +41 -0
  15. package/.claude/commands/review.md +73 -0
  16. package/.claude/memory/MEMORY.md +1 -0
  17. package/.claude/memory/feedback_pr_title_format.md +10 -0
  18. package/.claude/rules/react-native-typescript.md +46 -0
  19. package/CHANGELOG.md +12 -0
  20. package/CLAUDE.md +125 -0
  21. package/android/native.gradle +1 -1
  22. package/android/proguard-rules.txt +1 -1
  23. package/android/src/main/java/ai/luciq/reactlibrary/LuciqScreenLoadingFrameTracker.java +88 -0
  24. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +193 -10
  25. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  26. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +36 -12
  27. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  28. package/dist/components/LuciqCaptureScreenLoading.d.ts +8 -0
  29. package/dist/components/LuciqCaptureScreenLoading.js +154 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/modules/APM.d.ts +19 -0
  33. package/dist/modules/APM.js +38 -0
  34. package/dist/modules/Luciq.d.ts +1 -1
  35. package/dist/modules/Luciq.js +179 -12
  36. package/dist/modules/NetworkLogger.d.ts +0 -5
  37. package/dist/modules/NetworkLogger.js +9 -1
  38. package/dist/modules/apm/ScreenLoadingManager.d.ts +99 -0
  39. package/dist/modules/apm/ScreenLoadingManager.js +296 -0
  40. package/dist/native/NativeAPM.d.ts +9 -0
  41. package/dist/native/NativeLuciq.d.ts +1 -1
  42. package/dist/utils/FeatureFlags.d.ts +6 -0
  43. package/dist/utils/FeatureFlags.js +35 -0
  44. package/dist/utils/LuciqUtils.d.ts +25 -0
  45. package/dist/utils/LuciqUtils.js +50 -0
  46. package/dist/utils/RouteMatcher.d.ts +30 -0
  47. package/dist/utils/RouteMatcher.js +67 -0
  48. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  49. package/ios/RNLuciq/LuciqAPMBridge.m +82 -0
  50. package/ios/RNLuciq/LuciqReactBridge.m +1 -1
  51. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.h +11 -0
  52. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.m +121 -0
  53. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +14 -0
  54. package/ios/native.rb +1 -1
  55. package/package.json +4 -2
  56. package/scripts/get-github-app-token.sh +70 -0
  57. package/scripts/notify-github.sh +17 -8
  58. package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
  59. package/src/index.ts +4 -0
  60. package/src/modules/APM.ts +42 -0
  61. package/src/modules/Luciq.ts +210 -12
  62. package/src/modules/NetworkLogger.ts +26 -1
  63. package/src/modules/apm/ScreenLoadingManager.ts +364 -0
  64. package/src/native/NativeAPM.ts +22 -0
  65. package/src/native/NativeLuciq.ts +1 -1
  66. package/src/utils/FeatureFlags.ts +44 -0
  67. package/src/utils/LuciqUtils.ts +64 -0
  68. package/src/utils/RouteMatcher.ts +83 -0
  69. package/src/utils/XhrNetworkInterceptor.ts +128 -55
@@ -1,11 +1,12 @@
1
1
  import { AppState, findNodeHandle, Platform } from 'react-native';
2
2
  import Report from '../models/Report';
3
3
  import { emitter, NativeEvents, NativeLuciq } from '../native/NativeLuciq';
4
- import { registerFeatureFlagsListener } from '../utils/FeatureFlags';
4
+ import { registerFeatureFlagsListener, initFeatureFlagsCache } from '../utils/FeatureFlags';
5
5
  import { LogLevel, NetworkInterceptionMode, ReproStepsMode, StringKey, } from '../utils/Enums';
6
6
  import LuciqUtils, { checkNetworkRequestHandlers, resetNativeObfuscationListener, setApmNetworkFlagsIfChanged, stringifyIfNotString, } from '../utils/LuciqUtils';
7
7
  import * as NetworkLogger from './NetworkLogger';
8
8
  import { captureUnhandledRejections } from '../utils/UnhandledRejectionTracking';
9
+ import { ScreenLoadingManager } from './apm/ScreenLoadingManager';
9
10
  import { addAppStateListener } from '../utils/AppStatesHandler';
10
11
  import { NativeNetworkLogger } from '../native/NativeNetworkLogger';
11
12
  import LuciqConstants from '../utils/LuciqConstants';
@@ -19,6 +20,12 @@ let _currentAppState = AppState.currentState;
19
20
  let isNativeInterceptionFeatureEnabled = false; // Checks the value of "cp_native_interception_enabled" backend flag.
20
21
  let hasAPMNetworkPlugin = false; // Android only: checks if the APM plugin is installed.
21
22
  let shouldEnableNativeInterception = false; // For Android: used to disable APM logging inside reportNetworkLog() -> NativeAPM.networkLogAndroid(), For iOS: used to control native interception (true == enabled , false == disabled)
23
+ // Screen Loading tracking variables
24
+ let _navigationRef = null;
25
+ let _currentRoute = null;
26
+ let _activeNavigationSpanId = null;
27
+ let _stateChangeTimeout;
28
+ const STATE_CHANGE_TIMEOUT_MS = 2000; // Safety timeout if state never changes
22
29
  /**
23
30
  * Enables or disables Luciq functionality.
24
31
  * @param isEnabled A boolean to enable/disable Luciq.
@@ -50,10 +57,19 @@ function reportCurrentViewForAndroid(screenName) {
50
57
  * @param config SDK configurations. See {@link LuciqConfig} for more info.
51
58
  */
52
59
  export const init = (config) => {
60
+ initFeatureFlagsCache();
53
61
  if (Platform.OS === 'android') {
54
62
  // Add android feature flags listener for android
55
63
  registerFeatureFlagsListener();
56
64
  addOnFeatureUpdatedListener(config);
65
+ // Enable the JS XHR interceptor synchronously so cold-start requests
66
+ // (fired before LCQ_ON_FEATURES_UPDATED_CALLBACK arrives) are captured.
67
+ handleNetworkInterceptionMode(config);
68
+ setApmNetworkFlagsIfChanged({
69
+ isNativeInterceptionFeatureEnabled: isNativeInterceptionFeatureEnabled,
70
+ hasAPMNetworkPlugin: hasAPMNetworkPlugin,
71
+ shouldEnableNativeInterception: shouldEnableNativeInterception,
72
+ });
57
73
  }
58
74
  else {
59
75
  isNativeInterceptionFeatureEnabled = NativeNetworkLogger.isNativeInterceptionEnabled();
@@ -78,7 +94,7 @@ export const init = (config) => {
78
94
  reportCurrentViewForAndroid(firstScreen);
79
95
  setTimeout(() => {
80
96
  if (_currentScreen === firstScreen) {
81
- NativeLuciq.reportScreenChange(firstScreen);
97
+ NativeLuciq.reportScreenChange(firstScreen, null);
82
98
  _currentScreen = null;
83
99
  }
84
100
  }, 1000);
@@ -115,12 +131,14 @@ export const setWebViewUserInteractionsTrackingEnabled = (isEnabled) => {
115
131
  * Handles app state changes and updates APM network flags if necessary.
116
132
  */
117
133
  const handleAppStateChange = async (nextAppState, config) => {
118
- // Checks if the app has come to the foreground
134
+ // Checks if the app has come to the foreground
119
135
  if (['inactive', 'background'].includes(_currentAppState) && nextAppState === 'active') {
120
136
  const isUpdated = await fetchApmNetworkFlags();
121
137
  if (isUpdated) {
122
138
  refreshAPMNetworkConfigs(config);
123
139
  }
140
+ // Refresh screen loading flags from native
141
+ await ScreenLoadingManager.refreshFlags();
124
142
  }
125
143
  _currentAppState = nextAppState;
126
144
  };
@@ -646,21 +664,150 @@ export const onReportSubmitHandler = (handler) => {
646
664
  });
647
665
  NativeLuciq.setPreSendingHandler(handler);
648
666
  };
667
+ /**
668
+ * Helper to clear the state change timeout
669
+ */
670
+ const _clearStateChangeTimeout = () => {
671
+ if (_stateChangeTimeout) {
672
+ clearTimeout(_stateChangeTimeout);
673
+ _stateChangeTimeout = undefined;
674
+ }
675
+ };
676
+ /**
677
+ * Handles React Navigation's __unsafe_action__ event
678
+ * This fires WHEN a navigation action is dispatched (the start of navigation)
679
+ */
680
+ const _onNavigationAction = (event) => {
681
+ // Check for noop actions that shouldn't create spans
682
+ if (event?.data?.noop) {
683
+ Logger.log('[ScreenLoading] Navigation action is a noop, not starting span');
684
+ return;
685
+ }
686
+ // Skip non-navigation actions (like SET_PARAMS, OPEN_DRAWER, etc.)
687
+ const actionType = event?.data?.action?.type;
688
+ if (actionType &&
689
+ ['SET_PARAMS', 'OPEN_DRAWER', 'CLOSE_DRAWER', 'TOGGLE_DRAWER'].includes(actionType)) {
690
+ Logger.log(`[ScreenLoading] Skipping non-navigation action: ${actionType}`);
691
+ return;
692
+ }
693
+ // If there's an existing active span, it means navigation was interrupted
694
+ // Discard the previous span as it never completed
695
+ if (_activeNavigationSpanId) {
696
+ Logger.log('[ScreenLoading] Discarding incomplete previous navigation span');
697
+ // Mark the span as cancelled/error since state change never occurred
698
+ const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
699
+ if (span) {
700
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId);
701
+ }
702
+ _activeNavigationSpanId = null;
703
+ _clearStateChangeTimeout();
704
+ }
705
+ // Create a new span for this navigation action
706
+ // We don't know the destination screen yet, so use a placeholder name
707
+ if (ScreenLoadingManager.isFeatureEnabled()) {
708
+ const span = ScreenLoadingManager.createSpan('NavigationPending', false);
709
+ if (span) {
710
+ _activeNavigationSpanId = span.spanId;
711
+ Logger.log(`[ScreenLoading] Started span ${span.spanId} on navigation dispatch`);
712
+ // Set a safety timeout to discard the span if state never changes
713
+ // This prevents memory leaks from incomplete navigations
714
+ _stateChangeTimeout = setTimeout(() => {
715
+ if (_activeNavigationSpanId === span.spanId) {
716
+ Logger.warn(`[ScreenLoading] Navigation span ${span.spanId} timed out - state never changed`);
717
+ ScreenLoadingManager.endSpan(span.spanId);
718
+ _activeNavigationSpanId = null;
719
+ }
720
+ }, STATE_CHANGE_TIMEOUT_MS);
721
+ }
722
+ }
723
+ };
724
+ /**
725
+ * Handles React Navigation's state event
726
+ * This fires AFTER the navigation state has changed (the screen is mounted)
727
+ */
728
+ const _onNavigationStateChange = () => {
729
+ if (!_navigationRef?.current) {
730
+ return;
731
+ }
732
+ const previousRouteName = _currentRoute;
733
+ const currentRoute = _navigationRef.current.getCurrentRoute();
734
+ const currentRouteName = currentRoute?.name || null;
735
+ // If no route or same route, ignore
736
+ if (!currentRouteName || previousRouteName === currentRouteName) {
737
+ // Still need to clean up the span if one was created
738
+ if (_activeNavigationSpanId) {
739
+ Logger.log('[ScreenLoading] Navigation resulted in same route, discarding span');
740
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId);
741
+ _activeNavigationSpanId = null;
742
+ _clearStateChangeTimeout();
743
+ }
744
+ return;
745
+ }
746
+ // Capture the span ID BEFORE clearing it so we can pass it to reportScreenChange
747
+ let spanIdForReport = _activeNavigationSpanId;
748
+ // Complete the active navigation span if one exists
749
+ if (_activeNavigationSpanId) {
750
+ // Now that we know the actual route name, check if it's excluded
751
+ if (ScreenLoadingManager.isRouteExcluded(currentRouteName)) {
752
+ Logger.log(`[ScreenLoading] Route "${currentRouteName}" is excluded, discarding span`);
753
+ ScreenLoadingManager.discardSpan(_activeNavigationSpanId);
754
+ spanIdForReport = null;
755
+ _activeNavigationSpanId = null;
756
+ _clearStateChangeTimeout();
757
+ }
758
+ else {
759
+ const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
760
+ if (span) {
761
+ // Update the span name from placeholder to actual screen name
762
+ span.screenName = currentRouteName;
763
+ // End the span - the native frame tracker will provide the actual render timestamp
764
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId)
765
+ .then(() => {
766
+ Logger.log(`[ScreenLoading] Completed span for navigation to ${currentRouteName}`);
767
+ })
768
+ .catch((error) => {
769
+ Logger.warn('[ScreenLoading] Failed to end navigation span:', error);
770
+ });
771
+ }
772
+ // Clear the active span and timeout
773
+ _activeNavigationSpanId = null;
774
+ _clearStateChangeTimeout();
775
+ }
776
+ }
777
+ // Update the current route for the rest of Luciq's tracking
778
+ _currentRoute = currentRouteName;
779
+ // Report to native
780
+ NativeLuciq.reportScreenChange(currentRouteName, spanIdForReport);
781
+ };
649
782
  export const onNavigationStateChange = (prevState, currentState, _action) => {
650
783
  const currentScreen = LuciqUtils.getActiveRouteName(currentState);
651
784
  const prevScreen = LuciqUtils.getActiveRouteName(prevState);
652
785
  if (prevScreen !== currentScreen) {
786
+ // Start Screen Loading measurement for v4
787
+ let screenLoadingSpanId = null;
788
+ if (ScreenLoadingManager.isFeatureEnabled()) {
789
+ const span = ScreenLoadingManager.createSpan(currentScreen || 'Unknown', false);
790
+ if (span) {
791
+ screenLoadingSpanId = span.spanId;
792
+ }
793
+ }
653
794
  reportCurrentViewForAndroid(currentScreen);
654
795
  if (_currentScreen != null && _currentScreen !== firstScreen) {
655
- NativeLuciq.reportScreenChange(_currentScreen);
796
+ NativeLuciq.reportScreenChange(_currentScreen, screenLoadingSpanId);
656
797
  _currentScreen = null;
657
798
  }
658
799
  _currentScreen = currentScreen;
659
800
  setTimeout(() => {
660
801
  if (currentScreen && _currentScreen === currentScreen) {
661
- NativeLuciq.reportScreenChange(currentScreen);
802
+ NativeLuciq.reportScreenChange(currentScreen, screenLoadingSpanId);
662
803
  _currentScreen = null;
663
804
  }
805
+ // End Screen Loading measurement for v4
806
+ if (screenLoadingSpanId) {
807
+ ScreenLoadingManager.endSpan(screenLoadingSpanId).catch((error) => {
808
+ Logger.warn('[ScreenLoading] Failed to end span:', error);
809
+ });
810
+ }
664
811
  }, 1000);
665
812
  }
666
813
  };
@@ -668,16 +815,25 @@ export const onStateChange = (state) => {
668
815
  if (!state) {
669
816
  return;
670
817
  }
818
+ // Delegate to the new state change handler for Screen Loading
819
+ // This handles reportScreenChange when setNavigationListener was called
820
+ _onNavigationStateChange();
821
+ // When setNavigationListener is used, _onNavigationStateChange already handles
822
+ // reportScreenChange properly - skip legacy logic to avoid duplicate calls
823
+ if (_navigationRef?.current) {
824
+ return;
825
+ }
826
+ // Fallback: Legacy screen tracking for users who only use onStateChange without setNavigationListener
671
827
  const currentScreen = LuciqUtils.getFullRoute(state);
672
828
  reportCurrentViewForAndroid(currentScreen);
673
829
  if (_currentScreen !== null && _currentScreen !== firstScreen) {
674
- NativeLuciq.reportScreenChange(_currentScreen);
830
+ NativeLuciq.reportScreenChange(_currentScreen, null);
675
831
  _currentScreen = null;
676
832
  }
677
833
  _currentScreen = currentScreen;
678
834
  setTimeout(() => {
679
835
  if (_currentScreen === currentScreen) {
680
- NativeLuciq.reportScreenChange(currentScreen);
836
+ NativeLuciq.reportScreenChange(currentScreen, null);
681
837
  _currentScreen = null;
682
838
  }
683
839
  }, 1000);
@@ -688,12 +844,23 @@ export const onStateChange = (state) => {
688
844
  *
689
845
  */
690
846
  export const setNavigationListener = (navigationRef) => {
691
- return navigationRef.addListener('state', () => {
692
- onStateChange(navigationRef.getRootState());
693
- });
847
+ // Store the navigationRef for Screen Loading tracking
848
+ _navigationRef = navigationRef;
849
+ if (!navigationRef?.current) {
850
+ Logger.warn('[Luciq] Navigation ref not available, cannot set listeners');
851
+ return;
852
+ }
853
+ // Register the __unsafe_action__ listener for span creation
854
+ // This listener fires on navigation dispatch (start of navigation)
855
+ navigationRef.current.addListener('__unsafe_action__', _onNavigationAction);
856
+ // NOTE: We do NOT register a 'state' listener here because the user is expected
857
+ // to pass Luciq.onStateChange to NavigationContainer's onStateChange prop.
858
+ // Registering both would cause duplicate reportScreenChange calls.
859
+ Logger.log('[Luciq] Registered Screen Loading listener (__unsafe_action__)');
860
+ // return stateListener;
694
861
  };
695
862
  export const reportScreenChange = (screenName) => {
696
- NativeLuciq.reportScreenChange(screenName);
863
+ NativeLuciq.reportScreenChange(screenName, null);
697
864
  };
698
865
  /**
699
866
  * Add feature flags to the next report.
@@ -749,7 +916,7 @@ export const componentDidAppearListener = (event) => {
749
916
  return;
750
917
  }
751
918
  if (_lastScreen !== event.componentName) {
752
- NativeLuciq.reportScreenChange(event.componentName);
919
+ NativeLuciq.reportScreenChange(event.componentName, null);
753
920
  _lastScreen = event.componentName;
754
921
  }
755
922
  };
@@ -3,11 +3,6 @@ import { NetworkData, ProgressCallback } from '../utils/XhrNetworkInterceptor';
3
3
  import { NetworkListenerType } from '../native/NativeNetworkLogger';
4
4
  export type { NetworkData };
5
5
  export type NetworkDataObfuscationHandler = (data: NetworkData) => Promise<NetworkData>;
6
- /**
7
- * Sets whether network logs should be sent with bug reports.
8
- * It is enabled by default.
9
- * @param isEnabled
10
- */
11
6
  export declare const setEnabled: (isEnabled: boolean) => void;
12
7
  /**
13
8
  * @internal
@@ -20,21 +20,25 @@ function getPortFromUrl(url) {
20
20
  * It is enabled by default.
21
21
  * @param isEnabled
22
22
  */
23
+ const NET_TAG = 'LCQ-RN-NET:';
23
24
  export const setEnabled = (isEnabled) => {
24
25
  if (isEnabled) {
25
26
  xhr.enableInterception();
26
27
  xhr.setOnDoneCallback(async (network) => {
28
+ Logger.debug(NET_TAG, `[NetworkLogger] onDoneCallback received: ${network.method} ${network.url}, status=${network.responseCode}`);
27
29
  // eslint-disable-next-line no-new-func
28
30
  const predicate = Function('network', 'return ' + _requestFilterExpression);
29
31
  if (!predicate(network)) {
30
32
  const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeLuciq.getNetworkBodyMaxSize();
31
33
  try {
32
34
  if (_networkDataObfuscationHandler) {
35
+ Logger.debug(NET_TAG, `[NetworkLogger] Running obfuscation handler for ${network.url}`);
33
36
  network = await _networkDataObfuscationHandler(network);
34
37
  }
35
38
  if (__DEV__) {
36
39
  const urlPort = getPortFromUrl(network.url);
37
40
  if (urlPort === LuciqRNConfig.metroDevServerPort) {
41
+ Logger.debug(NET_TAG, `[NetworkLogger] Skipping Metro dev server request: ${network.url}`);
38
42
  return;
39
43
  }
40
44
  }
@@ -54,12 +58,16 @@ export const setEnabled = (isEnabled) => {
54
58
  network.responseBody = `Body is omitted because content type ${network.contentType} isn't supported`;
55
59
  Logger.warn(`LCQ-RN: The response body for the network request with URL ${network.url} has been omitted because the content type ${network.contentType} isn't supported.`);
56
60
  }
61
+ Logger.debug(NET_TAG, `[NetworkLogger] Reporting network log to native: ${network.method} ${network.url}`);
57
62
  reportNetworkLog(network);
58
63
  }
59
64
  catch (e) {
60
- Logger.error(e);
65
+ Logger.error(NET_TAG, `[NetworkLogger] Error processing network log for ${network.url}:`, e);
61
66
  }
62
67
  }
68
+ else {
69
+ Logger.debug(NET_TAG, `[NetworkLogger] Request filtered out by predicate: ${network.method} ${network.url}, expression="${_requestFilterExpression}"`);
70
+ }
63
71
  });
64
72
  }
65
73
  else {
@@ -0,0 +1,99 @@
1
+ export interface ScreenLoadingSpan {
2
+ spanId: string;
3
+ screenName: string;
4
+ startTimestamp: number;
5
+ endTimestamp?: number;
6
+ ttid?: number;
7
+ status: 'pending' | 'measuring' | 'completed' | 'error';
8
+ isManual: boolean;
9
+ attributes: Map<string, number>;
10
+ }
11
+ /**
12
+ * Automatic Screen Loading Measurement
13
+ *
14
+ * Start point: `__unsafe_action__` navigation event (below).
15
+ * - Fires when a navigation action is dispatched, before the target screen mounts.
16
+ * - `ScreenLoadingManager.createSpan()` records `nowMicros()` as the span start.
17
+ *
18
+ * End point: `_onNavigationStateChange()` (called from `onStateChange`).
19
+ * - Fires after navigation state has settled and the new screen is mounted.
20
+ * - `ScreenLoadingManager.endSpan()` fetches the native frame timestamp from
21
+ * CADisplayLink (iOS) / Choreographer (Android) to mark actual render completion.
22
+ * - The TTID is: native frame timestamp − span start.
23
+ *
24
+ *
25
+ * Manual Screen Loading Measurement
26
+ *
27
+ * Start point: Component instantiation (lazy init block before first render).
28
+ * - `nowMicros()` is captured as `constructorTimestampRef` and passed to
29
+ * `ScreenLoadingManager.createSpan()` as the span's start timestamp.
30
+ *
31
+ * End point: `useLayoutEffect` (fires synchronously after React commits DOM
32
+ * mutations, before the browser paints).
33
+ * - `ScreenLoadingManager.endSpan()` fetches the native frame timestamp
34
+ * from CADisplayLink (iOS) / Choreographer (Android) to mark the actual
35
+ * render completion. The TTID is: native frame timestamp − span start.
36
+ *
37
+ * Both approaches share the same `endSpan()` path so TTID values are comparable.
38
+ */
39
+ declare class ScreenLoadingManagerClass {
40
+ private activeSpans;
41
+ private isInitialized;
42
+ private isEnabled;
43
+ private isEndScreenLoadingEnabled;
44
+ private isFrameTrackingInitialized;
45
+ private activeSpanId;
46
+ private maxConcurrentSpans;
47
+ private excludedRoutes;
48
+ initialize(): Promise<void>;
49
+ refreshFlags(): Promise<void>;
50
+ /**
51
+ * Exclude specific routes from automatic screen loading measurement
52
+ * @param routes Array of route names to exclude
53
+ */
54
+ excludeRoutes(routes: string[]): void;
55
+ /**
56
+ * Include previously excluded routes back into screen loading measurement
57
+ * @param routes Array of route names to include (or empty to clear all exclusions)
58
+ */
59
+ includeRoutes(routes?: string[]): void;
60
+ /**
61
+ * Check if a route is excluded from measurement
62
+ */
63
+ isRouteExcluded(routeName: string): boolean;
64
+ /**
65
+ * Create a new screen loading span
66
+ * @param screenName Name of the screen
67
+ * @param isManual Whether the span is manual (not automatically created)
68
+ * @param startTimestampParam Optional start timestamp in microseconds (defaults to nowMicros())
69
+ * @returns The created span or null if the feature is not enabled
70
+ */
71
+ createSpan(screenName: string, isManual?: boolean, startTimestampParam?: number): ScreenLoadingSpan | null;
72
+ /**
73
+ * End a screen loading span
74
+ * @param spanId The ID of the span to end
75
+ */
76
+ endSpan(spanId: string): Promise<void>;
77
+ /**
78
+ * Log a screen loading span
79
+ * @param span The span to log
80
+ */
81
+ private logScreenLoading;
82
+ /**
83
+ * End a screen loading span using the current timestamp and active span ID
84
+ */
85
+ endScreenLoading(): void;
86
+ /**
87
+ * Discard a span without logging or syncing it to native.
88
+ * Used when a span should be silently dropped (e.g., excluded route resolved after creation).
89
+ */
90
+ discardSpan(spanId: string): void;
91
+ getActiveSpan(spanId: string): ScreenLoadingSpan | undefined;
92
+ getAllActiveSpans(): ScreenLoadingSpan[];
93
+ addSpanAttribute(spanId: string, key: string, value: number): void;
94
+ private cleanupOldestSpans;
95
+ isFeatureEnabled(): boolean;
96
+ isEndScreenLoadingFeatureEnabled(): boolean;
97
+ }
98
+ export declare const ScreenLoadingManager: ScreenLoadingManagerClass;
99
+ export {};