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

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 (67) 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 +184 -19
  25. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +7 -29
  26. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +14 -34
  27. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +0 -7
  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 +170 -13
  36. package/dist/modules/NetworkLogger.d.ts +5 -0
  37. package/dist/modules/NetworkLogger.js +1 -9
  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 +0 -6
  43. package/dist/utils/FeatureFlags.js +0 -35
  44. package/dist/utils/LuciqUtils.d.ts +25 -0
  45. package/dist/utils/LuciqUtils.js +44 -6
  46. package/dist/utils/RouteMatcher.d.ts +30 -0
  47. package/dist/utils/RouteMatcher.js +67 -0
  48. package/dist/utils/XhrNetworkInterceptor.js +53 -85
  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 +5 -1
  56. package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
  57. package/src/index.ts +4 -0
  58. package/src/modules/APM.ts +42 -0
  59. package/src/modules/Luciq.ts +198 -14
  60. package/src/modules/NetworkLogger.ts +1 -26
  61. package/src/modules/apm/ScreenLoadingManager.ts +364 -0
  62. package/src/native/NativeAPM.ts +22 -0
  63. package/src/native/NativeLuciq.ts +1 -1
  64. package/src/utils/FeatureFlags.ts +0 -44
  65. package/src/utils/LuciqUtils.ts +49 -15
  66. package/src/utils/RouteMatcher.ts +83 -0
  67. package/src/utils/XhrNetworkInterceptor.ts +55 -128
@@ -0,0 +1,210 @@
1
+ import React, { useState, useRef, useEffect, useLayoutEffect, useContext } from 'react';
2
+ import { View, ViewProps } from 'react-native';
3
+ import { ScreenLoadingManager } from '../modules/apm/ScreenLoadingManager';
4
+ import { Logger } from '../utils/logger';
5
+ import { nowMicros, toEpochMicros } from '../utils/LuciqUtils';
6
+
7
+ // Context to handle nested components
8
+ const ScreenLoadingContext = React.createContext<boolean>(false);
9
+
10
+ export interface LuciqScreenLoadingProps extends ViewProps {
11
+ screenName: string;
12
+ record?: boolean;
13
+ onMeasured?: (ttid: number) => void;
14
+ }
15
+
16
+ export function LuciqCaptureScreenLoading(props: LuciqScreenLoadingProps) {
17
+ const { screenName, record, onMeasured, onLayout, children, ...viewProps } = props;
18
+
19
+ const isNested = useContext(ScreenLoadingContext);
20
+
21
+ // Refs for timestamps (these don't need to trigger re-renders)
22
+ const constructorTimestampRef = useRef<number>(nowMicros()); // microseconds
23
+ const renderStartTimestampRef = useRef<number | undefined>(undefined);
24
+ const renderEndTimestampRef = useRef<number | undefined>(undefined);
25
+ const mountTimestampRef = useRef<number | undefined>(undefined);
26
+
27
+ // Guards to ensure single execution
28
+ const initializedRef = useRef(false);
29
+ const hasFirstRenderCompletedRef = useRef(false);
30
+ const attributesRecordedRef = useRef(false);
31
+ const initialSpanIdRef = useRef<string | null>(null);
32
+
33
+ // Capture render start timestamp ONLY on first render
34
+ if (!hasFirstRenderCompletedRef.current) {
35
+ renderStartTimestampRef.current = nowMicros();
36
+ }
37
+
38
+ // Initialize span - runs once like constructor (lazy initialization)
39
+ if (!initializedRef.current) {
40
+ initializedRef.current = true;
41
+ // Initialize span if conditions are met
42
+ try {
43
+ if (record !== false && ScreenLoadingManager.isFeatureEnabled()) {
44
+ const span = ScreenLoadingManager.createSpan(
45
+ screenName,
46
+ true,
47
+ constructorTimestampRef.current,
48
+ );
49
+ if (span) {
50
+ initialSpanIdRef.current = span.spanId;
51
+ Logger.log(`[LuciqScreenLoading] Span ${span.spanId} created in constructor`);
52
+ }
53
+ }
54
+ } catch (error) {
55
+ Logger.error('[LuciqScreenLoading] Failed to create span:', error);
56
+ }
57
+ }
58
+
59
+ const [spanId, setSpanId] = useState<string | null>(initialSpanIdRef.current);
60
+ const [isMeasured, setIsMeasured] = useState(false);
61
+
62
+ // Ref to avoid stale closure in useLayoutEffect
63
+ const onMeasuredRef = useRef(onMeasured);
64
+ useEffect(() => {
65
+ onMeasuredRef.current = onMeasured;
66
+ }, [onMeasured]);
67
+
68
+ // Refs to track latest values for cleanup (componentWillUnmount)
69
+ const spanIdRef = useRef<string | null>(spanId);
70
+ const isMeasuredRef = useRef(isMeasured);
71
+
72
+ // Keep refs in sync with state
73
+ useEffect(() => {
74
+ spanIdRef.current = spanId;
75
+ }, [spanId]);
76
+
77
+ useEffect(() => {
78
+ isMeasuredRef.current = isMeasured;
79
+ }, [isMeasured]);
80
+
81
+ // Handle nested component detection
82
+ useEffect(() => {
83
+ // Check if we're nested and should ignore this component
84
+ if (isNested && initialSpanIdRef.current) {
85
+ Logger.log(
86
+ `[LuciqScreenLoading] Nested component detected, ignoring span ${initialSpanIdRef.current}`,
87
+ );
88
+ // Cancel the span
89
+ setSpanId(null);
90
+ }
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, []); // Empty deps = componentDidMount
93
+
94
+ // Record lifecycle timestamps after first render completes (synchronous)
95
+ // useLayoutEffect fires synchronously after DOM mutations but before browser paint
96
+ useLayoutEffect(() => {
97
+ // Skip if no span, already recorded, or nested
98
+ if (!spanId || attributesRecordedRef.current || isNested) {
99
+ return;
100
+ }
101
+
102
+ // endSpan is async (native frame timestamp fetch), fire-and-forget from useLayoutEffect
103
+ ScreenLoadingManager.endSpan(spanId)
104
+ .then(() => {
105
+ const completedSpan = ScreenLoadingManager.getActiveSpan(spanId);
106
+ if (completedSpan?.ttid && onMeasuredRef.current) {
107
+ onMeasuredRef.current(completedSpan.ttid / 1000);
108
+ }
109
+ })
110
+ .catch((error) => {
111
+ Logger.warn('[LuciqScreenLoading] Failed to end span:', error);
112
+ });
113
+
114
+ attributesRecordedRef.current = true;
115
+ mountTimestampRef.current = nowMicros();
116
+
117
+ try {
118
+ // Record all timestamps
119
+ ScreenLoadingManager.addSpanAttribute(
120
+ spanId,
121
+ 'cnst_mus_st',
122
+ toEpochMicros(constructorTimestampRef.current),
123
+ );
124
+
125
+ if (renderStartTimestampRef.current) {
126
+ ScreenLoadingManager.addSpanAttribute(
127
+ spanId,
128
+ 'rnd_mus_st',
129
+ toEpochMicros(renderStartTimestampRef.current),
130
+ );
131
+ }
132
+
133
+ ScreenLoadingManager.addSpanAttribute(
134
+ spanId,
135
+ 'mnt_mus_st',
136
+ toEpochMicros(mountTimestampRef.current),
137
+ );
138
+
139
+ // Record all durations
140
+ if (renderStartTimestampRef.current) {
141
+ // Constructor duration: time from component init to first render start
142
+ const constructorDuration =
143
+ renderStartTimestampRef.current - constructorTimestampRef.current;
144
+ ScreenLoadingManager.addSpanAttribute(spanId, 'cnst_mus', constructorDuration);
145
+ }
146
+
147
+ if (renderEndTimestampRef.current && renderStartTimestampRef.current) {
148
+ // Render duration: time spent creating JSX
149
+ const renderDuration = renderEndTimestampRef.current - renderStartTimestampRef.current;
150
+ ScreenLoadingManager.addSpanAttribute(spanId, 'rnd_mus', renderDuration);
151
+ }
152
+
153
+ if (mountTimestampRef.current && renderEndTimestampRef.current) {
154
+ // Mount duration: time from render complete to effect execution
155
+ const mountDuration = mountTimestampRef.current - renderEndTimestampRef.current;
156
+ ScreenLoadingManager.addSpanAttribute(spanId, 'mnt_mus', mountDuration);
157
+ }
158
+
159
+ Logger.log(`[LuciqScreenLoading] Lifecycle measurements for span ${spanId}:`, {
160
+ constructor_us: renderStartTimestampRef.current
161
+ ? renderStartTimestampRef.current - constructorTimestampRef.current
162
+ : undefined,
163
+ render_us:
164
+ renderEndTimestampRef.current && renderStartTimestampRef.current
165
+ ? renderEndTimestampRef.current - renderStartTimestampRef.current
166
+ : undefined,
167
+ mount_us:
168
+ mountTimestampRef.current && renderEndTimestampRef.current
169
+ ? mountTimestampRef.current - renderEndTimestampRef.current
170
+ : undefined,
171
+ });
172
+ } catch (error) {
173
+ Logger.error(`[LuciqScreenLoading] Failed to record attributes for span ${spanId}:`, error);
174
+ }
175
+
176
+ // End the span — mark as measured synchronously to guard against unmount race
177
+ setIsMeasured(true);
178
+
179
+ // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ }, [spanId]); // Run when spanId is set
181
+
182
+ // componentWillUnmount equivalent
183
+ useEffect(() => {
184
+ return () => {
185
+ // Cleanup on unmount if not measured
186
+ if (spanIdRef.current && !isMeasuredRef.current) {
187
+ ScreenLoadingManager.endSpan(spanIdRef.current).catch((error) => {
188
+ Logger.warn('[LuciqScreenLoading] Failed to end span on unmount:', error);
189
+ });
190
+ }
191
+ };
192
+ }, []); // Empty deps = only runs cleanup on unmount
193
+
194
+ // Create the JSX result
195
+ const result = (
196
+ <ScreenLoadingContext.Provider value={spanId !== null}>
197
+ <View {...viewProps} onLayout={onLayout}>
198
+ {children}
199
+ </View>
200
+ </ScreenLoadingContext.Provider>
201
+ );
202
+
203
+ // Capture render end timestamp ONLY on first render (after JSX creation)
204
+ if (!hasFirstRenderCompletedRef.current) {
205
+ renderEndTimestampRef.current = nowMicros();
206
+ hasFirstRenderCompletedRef.current = true;
207
+ }
208
+
209
+ return result;
210
+ }
package/src/index.ts CHANGED
@@ -45,4 +45,8 @@ export type {
45
45
  ThemeConfig,
46
46
  };
47
47
 
48
+ // Screen Loading Component
49
+ export { LuciqCaptureScreenLoading } from './components/LuciqCaptureScreenLoading';
50
+ export type { LuciqScreenLoadingProps } from './components/LuciqCaptureScreenLoading';
51
+
48
52
  export default Luciq;
@@ -7,6 +7,13 @@ import {
7
7
  addCompletedCustomSpan as addCompletedCustomSpanInternal,
8
8
  } from '../utils/CustomSpansManager';
9
9
  import type { CustomSpan } from '../models/CustomSpan';
10
+ import { ScreenLoadingManager } from './apm/ScreenLoadingManager';
11
+ import { Logger } from '../utils/logger';
12
+
13
+ // Initialize Screen Loading on module load
14
+ ScreenLoadingManager.initialize().catch((error) => {
15
+ Logger.error('[APM] Failed to initialize Screen Loading:', error);
16
+ });
10
17
 
11
18
  /**
12
19
  * Enables or disables APM
@@ -195,3 +202,38 @@ export const addCompletedCustomSpan = async (
195
202
  ): Promise<void> => {
196
203
  return addCompletedCustomSpanInternal(name, startDate, endDate);
197
204
  };
205
+
206
+ /**
207
+ * Enables or disables Screen Loading feature
208
+ * @param isEnabled
209
+ */
210
+ export const setScreenLoadingEnabled = (isEnabled: boolean) => {
211
+ try {
212
+ NativeAPM.setScreenLoadingEnabled(isEnabled);
213
+ } catch (error) {
214
+ Logger.error('[APM] Failed to set screen loading enabled:', error);
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Extends the currently running screen loading trace with a new end timestamp.
220
+ */
221
+ export const endScreenLoading = () => {
222
+ ScreenLoadingManager.endScreenLoading();
223
+ };
224
+
225
+ /**
226
+ * Exclude specific routes from automatic screen loading measurement
227
+ * @param routes Array of route names to exclude
228
+ */
229
+ export function excludeScreenLoadingRoutes(routes: string[]): void {
230
+ ScreenLoadingManager.excludeRoutes(routes);
231
+ }
232
+
233
+ /**
234
+ * Include previously excluded routes back into screen loading measurement
235
+ * @param routes Array of route names to include (or empty to clear all exclusions)
236
+ */
237
+ export function includeScreenLoadingRoutes(routes?: string[]): void {
238
+ ScreenLoadingManager.includeRoutes(routes);
239
+ }
@@ -10,7 +10,7 @@ import type { NavigationAction, NavigationState as NavigationStateV4 } from 'rea
10
10
  import type { LuciqConfig } from '../models/LuciqConfig';
11
11
  import Report from '../models/Report';
12
12
  import { emitter, NativeEvents, NativeLuciq } from '../native/NativeLuciq';
13
- import { registerFeatureFlagsListener, initFeatureFlagsCache } from '../utils/FeatureFlags';
13
+ import { registerFeatureFlagsListener } from '../utils/FeatureFlags';
14
14
  import {
15
15
  AutoMaskingType,
16
16
  ColorTheme,
@@ -29,6 +29,7 @@ import LuciqUtils, {
29
29
  } from '../utils/LuciqUtils';
30
30
  import * as NetworkLogger from './NetworkLogger';
31
31
  import { captureUnhandledRejections } from '../utils/UnhandledRejectionTracking';
32
+ import { ScreenLoadingManager } from './apm/ScreenLoadingManager';
32
33
  import type { ReproConfig } from '../models/ReproConfig';
33
34
  import type { FeatureFlag } from '../models/FeatureFlag';
34
35
  import { addAppStateListener } from '../utils/AppStatesHandler';
@@ -48,6 +49,13 @@ let isNativeInterceptionFeatureEnabled = false; // Checks the value of "cp_nativ
48
49
  let hasAPMNetworkPlugin = false; // Android only: checks if the APM plugin is installed.
49
50
  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)
50
51
 
52
+ // Screen Loading tracking variables
53
+ let _navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList> | null = null;
54
+ let _currentRoute: string | null = null;
55
+ let _activeNavigationSpanId: string | null = null;
56
+ let _stateChangeTimeout: ReturnType<typeof setTimeout> | undefined;
57
+ const STATE_CHANGE_TIMEOUT_MS = 2000; // Safety timeout if state never changes
58
+
51
59
  /**
52
60
  * Enables or disables Luciq functionality.
53
61
  * @param isEnabled A boolean to enable/disable Luciq.
@@ -81,8 +89,6 @@ function reportCurrentViewForAndroid(screenName: string | null) {
81
89
  * @param config SDK configurations. See {@link LuciqConfig} for more info.
82
90
  */
83
91
  export const init = (config: LuciqConfig) => {
84
- initFeatureFlagsCache();
85
-
86
92
  if (Platform.OS === 'android') {
87
93
  // Add android feature flags listener for android
88
94
  registerFeatureFlagsListener();
@@ -118,7 +124,7 @@ export const init = (config: LuciqConfig) => {
118
124
  reportCurrentViewForAndroid(firstScreen);
119
125
  setTimeout(() => {
120
126
  if (_currentScreen === firstScreen) {
121
- NativeLuciq.reportScreenChange(firstScreen);
127
+ NativeLuciq.reportScreenChange(firstScreen, null);
122
128
  _currentScreen = null;
123
129
  }
124
130
  }, 1000);
@@ -160,12 +166,14 @@ export const setWebViewUserInteractionsTrackingEnabled = (isEnabled: boolean) =>
160
166
  * Handles app state changes and updates APM network flags if necessary.
161
167
  */
162
168
  const handleAppStateChange = async (nextAppState: AppStateStatus, config: LuciqConfig) => {
163
- // Checks if the app has come to the foreground
169
+ // Checks if the app has come to the foreground
164
170
  if (['inactive', 'background'].includes(_currentAppState) && nextAppState === 'active') {
165
171
  const isUpdated = await fetchApmNetworkFlags();
166
172
  if (isUpdated) {
167
173
  refreshAPMNetworkConfigs(config);
168
174
  }
175
+ // Refresh screen loading flags from native
176
+ await ScreenLoadingManager.refreshFlags();
169
177
  }
170
178
 
171
179
  _currentAppState = nextAppState;
@@ -758,6 +766,139 @@ export const onReportSubmitHandler = (handler?: (report: Report) => void) => {
758
766
  NativeLuciq.setPreSendingHandler(handler);
759
767
  };
760
768
 
769
+ /**
770
+ * Helper to clear the state change timeout
771
+ */
772
+ const _clearStateChangeTimeout = (): void => {
773
+ if (_stateChangeTimeout) {
774
+ clearTimeout(_stateChangeTimeout);
775
+ _stateChangeTimeout = undefined;
776
+ }
777
+ };
778
+
779
+ /**
780
+ * Handles React Navigation's __unsafe_action__ event
781
+ * This fires WHEN a navigation action is dispatched (the start of navigation)
782
+ */
783
+ const _onNavigationAction = (event?: any): void => {
784
+ // Check for noop actions that shouldn't create spans
785
+ if (event?.data?.noop) {
786
+ Logger.log('[ScreenLoading] Navigation action is a noop, not starting span');
787
+ return;
788
+ }
789
+
790
+ // Skip non-navigation actions (like SET_PARAMS, OPEN_DRAWER, etc.)
791
+ const actionType = event?.data?.action?.type;
792
+ if (
793
+ actionType &&
794
+ ['SET_PARAMS', 'OPEN_DRAWER', 'CLOSE_DRAWER', 'TOGGLE_DRAWER'].includes(actionType)
795
+ ) {
796
+ Logger.log(`[ScreenLoading] Skipping non-navigation action: ${actionType}`);
797
+ return;
798
+ }
799
+
800
+ // If there's an existing active span, it means navigation was interrupted
801
+ // Discard the previous span as it never completed
802
+ if (_activeNavigationSpanId) {
803
+ Logger.log('[ScreenLoading] Discarding incomplete previous navigation span');
804
+ // Mark the span as cancelled/error since state change never occurred
805
+ const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
806
+ if (span) {
807
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId);
808
+ }
809
+ _activeNavigationSpanId = null;
810
+ _clearStateChangeTimeout();
811
+ }
812
+
813
+ // Create a new span for this navigation action
814
+ // We don't know the destination screen yet, so use a placeholder name
815
+ if (ScreenLoadingManager.isFeatureEnabled()) {
816
+ const span = ScreenLoadingManager.createSpan('NavigationPending', false);
817
+ if (span) {
818
+ _activeNavigationSpanId = span.spanId;
819
+ Logger.log(`[ScreenLoading] Started span ${span.spanId} on navigation dispatch`);
820
+
821
+ // Set a safety timeout to discard the span if state never changes
822
+ // This prevents memory leaks from incomplete navigations
823
+ _stateChangeTimeout = setTimeout(() => {
824
+ if (_activeNavigationSpanId === span.spanId) {
825
+ Logger.warn(
826
+ `[ScreenLoading] Navigation span ${span.spanId} timed out - state never changed`,
827
+ );
828
+ ScreenLoadingManager.endSpan(span.spanId);
829
+ _activeNavigationSpanId = null;
830
+ }
831
+ }, STATE_CHANGE_TIMEOUT_MS);
832
+ }
833
+ }
834
+ };
835
+
836
+ /**
837
+ * Handles React Navigation's state event
838
+ * This fires AFTER the navigation state has changed (the screen is mounted)
839
+ */
840
+ const _onNavigationStateChange = (): void => {
841
+ if (!_navigationRef?.current) {
842
+ return;
843
+ }
844
+
845
+ const previousRouteName = _currentRoute;
846
+ const currentRoute = _navigationRef.current.getCurrentRoute();
847
+ const currentRouteName = currentRoute?.name || null;
848
+
849
+ // If no route or same route, ignore
850
+ if (!currentRouteName || previousRouteName === currentRouteName) {
851
+ // Still need to clean up the span if one was created
852
+ if (_activeNavigationSpanId) {
853
+ Logger.log('[ScreenLoading] Navigation resulted in same route, discarding span');
854
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId);
855
+ _activeNavigationSpanId = null;
856
+ _clearStateChangeTimeout();
857
+ }
858
+ return;
859
+ }
860
+
861
+ // Capture the span ID BEFORE clearing it so we can pass it to reportScreenChange
862
+ let spanIdForReport: string | null = _activeNavigationSpanId;
863
+
864
+ // Complete the active navigation span if one exists
865
+ if (_activeNavigationSpanId) {
866
+ // Now that we know the actual route name, check if it's excluded
867
+ if (ScreenLoadingManager.isRouteExcluded(currentRouteName)) {
868
+ Logger.log(`[ScreenLoading] Route "${currentRouteName}" is excluded, discarding span`);
869
+ ScreenLoadingManager.discardSpan(_activeNavigationSpanId);
870
+ spanIdForReport = null;
871
+ _activeNavigationSpanId = null;
872
+ _clearStateChangeTimeout();
873
+ } else {
874
+ const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
875
+ if (span) {
876
+ // Update the span name from placeholder to actual screen name
877
+ span.screenName = currentRouteName;
878
+
879
+ // End the span - the native frame tracker will provide the actual render timestamp
880
+ ScreenLoadingManager.endSpan(_activeNavigationSpanId)
881
+ .then(() => {
882
+ Logger.log(`[ScreenLoading] Completed span for navigation to ${currentRouteName}`);
883
+ })
884
+ .catch((error) => {
885
+ Logger.warn('[ScreenLoading] Failed to end navigation span:', error);
886
+ });
887
+ }
888
+
889
+ // Clear the active span and timeout
890
+ _activeNavigationSpanId = null;
891
+ _clearStateChangeTimeout();
892
+ }
893
+ }
894
+
895
+ // Update the current route for the rest of Luciq's tracking
896
+ _currentRoute = currentRouteName;
897
+
898
+ // Report to native
899
+ NativeLuciq.reportScreenChange(currentRouteName, spanIdForReport);
900
+ };
901
+
761
902
  export const onNavigationStateChange = (
762
903
  prevState: NavigationStateV4,
763
904
  currentState: NavigationStateV4,
@@ -767,17 +908,33 @@ export const onNavigationStateChange = (
767
908
  const prevScreen = LuciqUtils.getActiveRouteName(prevState);
768
909
 
769
910
  if (prevScreen !== currentScreen) {
911
+ // Start Screen Loading measurement for v4
912
+ let screenLoadingSpanId: string | null = null;
913
+ if (ScreenLoadingManager.isFeatureEnabled()) {
914
+ const span = ScreenLoadingManager.createSpan(currentScreen || 'Unknown', false);
915
+ if (span) {
916
+ screenLoadingSpanId = span.spanId;
917
+ }
918
+ }
919
+
770
920
  reportCurrentViewForAndroid(currentScreen);
771
921
  if (_currentScreen != null && _currentScreen !== firstScreen) {
772
- NativeLuciq.reportScreenChange(_currentScreen);
922
+ NativeLuciq.reportScreenChange(_currentScreen, screenLoadingSpanId);
773
923
  _currentScreen = null;
774
924
  }
775
925
  _currentScreen = currentScreen;
776
926
  setTimeout(() => {
777
927
  if (currentScreen && _currentScreen === currentScreen) {
778
- NativeLuciq.reportScreenChange(currentScreen);
928
+ NativeLuciq.reportScreenChange(currentScreen, screenLoadingSpanId);
779
929
  _currentScreen = null;
780
930
  }
931
+
932
+ // End Screen Loading measurement for v4
933
+ if (screenLoadingSpanId) {
934
+ ScreenLoadingManager.endSpan(screenLoadingSpanId).catch((error) => {
935
+ Logger.warn('[ScreenLoading] Failed to end span:', error);
936
+ });
937
+ }
781
938
  }, 1000);
782
939
  }
783
940
  };
@@ -787,17 +944,28 @@ export const onStateChange = (state?: NavigationStateV5) => {
787
944
  return;
788
945
  }
789
946
 
947
+ // Delegate to the new state change handler for Screen Loading
948
+ // This handles reportScreenChange when setNavigationListener was called
949
+ _onNavigationStateChange();
950
+
951
+ // When setNavigationListener is used, _onNavigationStateChange already handles
952
+ // reportScreenChange properly - skip legacy logic to avoid duplicate calls
953
+ if (_navigationRef?.current) {
954
+ return;
955
+ }
956
+
957
+ // Fallback: Legacy screen tracking for users who only use onStateChange without setNavigationListener
790
958
  const currentScreen = LuciqUtils.getFullRoute(state);
791
959
  reportCurrentViewForAndroid(currentScreen);
792
960
  if (_currentScreen !== null && _currentScreen !== firstScreen) {
793
- NativeLuciq.reportScreenChange(_currentScreen);
961
+ NativeLuciq.reportScreenChange(_currentScreen, null);
794
962
  _currentScreen = null;
795
963
  }
796
964
 
797
965
  _currentScreen = currentScreen;
798
966
  setTimeout(() => {
799
967
  if (_currentScreen === currentScreen) {
800
- NativeLuciq.reportScreenChange(currentScreen);
968
+ NativeLuciq.reportScreenChange(currentScreen, null);
801
969
  _currentScreen = null;
802
970
  }
803
971
  }, 1000);
@@ -811,13 +979,29 @@ export const onStateChange = (state?: NavigationStateV5) => {
811
979
  export const setNavigationListener = (
812
980
  navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>,
813
981
  ) => {
814
- return navigationRef.addListener('state', () => {
815
- onStateChange(navigationRef.getRootState());
816
- });
982
+ // Store the navigationRef for Screen Loading tracking
983
+ _navigationRef = navigationRef;
984
+
985
+ if (!navigationRef?.current) {
986
+ Logger.warn('[Luciq] Navigation ref not available, cannot set listeners');
987
+ return;
988
+ }
989
+
990
+ // Register the __unsafe_action__ listener for span creation
991
+ // This listener fires on navigation dispatch (start of navigation)
992
+ navigationRef.current.addListener('__unsafe_action__', _onNavigationAction);
993
+
994
+ // NOTE: We do NOT register a 'state' listener here because the user is expected
995
+ // to pass Luciq.onStateChange to NavigationContainer's onStateChange prop.
996
+ // Registering both would cause duplicate reportScreenChange calls.
997
+
998
+ Logger.log('[Luciq] Registered Screen Loading listener (__unsafe_action__)');
999
+
1000
+ // return stateListener;
817
1001
  };
818
1002
 
819
1003
  export const reportScreenChange = (screenName: string) => {
820
- NativeLuciq.reportScreenChange(screenName);
1004
+ NativeLuciq.reportScreenChange(screenName, null);
821
1005
  };
822
1006
 
823
1007
  /**
@@ -881,7 +1065,7 @@ export const componentDidAppearListener = (event: ComponentDidAppearEvent) => {
881
1065
  return;
882
1066
  }
883
1067
  if (_lastScreen !== event.componentName) {
884
- NativeLuciq.reportScreenChange(event.componentName);
1068
+ NativeLuciq.reportScreenChange(event.componentName, null);
885
1069
  _lastScreen = event.componentName;
886
1070
  }
887
1071
  };
@@ -39,17 +39,10 @@ function getPortFromUrl(url: string) {
39
39
  * It is enabled by default.
40
40
  * @param isEnabled
41
41
  */
42
- const NET_TAG = 'LCQ-RN-NET:';
43
-
44
42
  export const setEnabled = (isEnabled: boolean) => {
45
43
  if (isEnabled) {
46
44
  xhr.enableInterception();
47
45
  xhr.setOnDoneCallback(async (network) => {
48
- Logger.debug(
49
- NET_TAG,
50
- `[NetworkLogger] onDoneCallback received: ${network.method} ${network.url}, status=${network.responseCode}`,
51
- );
52
-
53
46
  // eslint-disable-next-line no-new-func
54
47
  const predicate = Function('network', 'return ' + _requestFilterExpression);
55
48
 
@@ -57,17 +50,12 @@ export const setEnabled = (isEnabled: boolean) => {
57
50
  const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeLuciq.getNetworkBodyMaxSize();
58
51
  try {
59
52
  if (_networkDataObfuscationHandler) {
60
- Logger.debug(NET_TAG, `[NetworkLogger] Running obfuscation handler for ${network.url}`);
61
53
  network = await _networkDataObfuscationHandler(network);
62
54
  }
63
55
 
64
56
  if (__DEV__) {
65
57
  const urlPort = getPortFromUrl(network.url);
66
58
  if (urlPort === LuciqRNConfig.metroDevServerPort) {
67
- Logger.debug(
68
- NET_TAG,
69
- `[NetworkLogger] Skipping Metro dev server request: ${network.url}`,
70
- );
71
59
  return;
72
60
  }
73
61
  }
@@ -109,23 +97,10 @@ export const setEnabled = (isEnabled: boolean) => {
109
97
  );
110
98
  }
111
99
 
112
- Logger.debug(
113
- NET_TAG,
114
- `[NetworkLogger] Reporting network log to native: ${network.method} ${network.url}`,
115
- );
116
100
  reportNetworkLog(network);
117
101
  } catch (e) {
118
- Logger.error(
119
- NET_TAG,
120
- `[NetworkLogger] Error processing network log for ${network.url}:`,
121
- e,
122
- );
102
+ Logger.error(e);
123
103
  }
124
- } else {
125
- Logger.debug(
126
- NET_TAG,
127
- `[NetworkLogger] Request filtered out by predicate: ${network.method} ${network.url}, expression="${_requestFilterExpression}"`,
128
- );
129
104
  }
130
105
  });
131
106
  } else {