@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.
- package/.claude/agents/codebase-analyzer.md +33 -0
- package/.claude/agents/codebase-locator.md +42 -0
- package/.claude/agents/codebase-pattern-finder.md +40 -0
- package/.claude/commands/apply-pr-reviews.md +253 -0
- package/.claude/commands/create-jira-workitem.md +27 -0
- package/.claude/commands/create-pr.md +138 -0
- package/.claude/commands/create-public-release-notes.md +145 -0
- package/.claude/commands/create-rca.md +286 -0
- package/.claude/commands/debug-sdk.md +66 -0
- package/.claude/commands/describe-pr.md +40 -0
- package/.claude/commands/new-api.md +60 -0
- package/.claude/commands/new-feature.md +75 -0
- package/.claude/commands/pr-review.md +85 -0
- package/.claude/commands/research-codebase.md +41 -0
- package/.claude/commands/review.md +73 -0
- package/.claude/memory/MEMORY.md +1 -0
- package/.claude/memory/feedback_pr_title_format.md +10 -0
- package/.claude/rules/react-native-typescript.md +46 -0
- package/CHANGELOG.md +12 -0
- package/CLAUDE.md +125 -0
- package/android/native.gradle +1 -1
- package/android/proguard-rules.txt +1 -1
- package/android/src/main/java/ai/luciq/reactlibrary/LuciqScreenLoadingFrameTracker.java +88 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +193 -10
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +36 -12
- package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
- package/dist/components/LuciqCaptureScreenLoading.d.ts +8 -0
- package/dist/components/LuciqCaptureScreenLoading.js +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modules/APM.d.ts +19 -0
- package/dist/modules/APM.js +38 -0
- package/dist/modules/Luciq.d.ts +1 -1
- package/dist/modules/Luciq.js +179 -12
- package/dist/modules/NetworkLogger.d.ts +0 -5
- package/dist/modules/NetworkLogger.js +9 -1
- package/dist/modules/apm/ScreenLoadingManager.d.ts +99 -0
- package/dist/modules/apm/ScreenLoadingManager.js +296 -0
- package/dist/native/NativeAPM.d.ts +9 -0
- package/dist/native/NativeLuciq.d.ts +1 -1
- package/dist/utils/FeatureFlags.d.ts +6 -0
- package/dist/utils/FeatureFlags.js +35 -0
- package/dist/utils/LuciqUtils.d.ts +25 -0
- package/dist/utils/LuciqUtils.js +50 -0
- package/dist/utils/RouteMatcher.d.ts +30 -0
- package/dist/utils/RouteMatcher.js +67 -0
- package/dist/utils/XhrNetworkInterceptor.js +85 -53
- package/ios/RNLuciq/LuciqAPMBridge.m +82 -0
- package/ios/RNLuciq/LuciqReactBridge.m +1 -1
- package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.h +11 -0
- package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.m +121 -0
- package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +14 -0
- package/ios/native.rb +1 -1
- package/package.json +4 -2
- package/scripts/get-github-app-token.sh +70 -0
- package/scripts/notify-github.sh +17 -8
- package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
- package/src/index.ts +4 -0
- package/src/modules/APM.ts +42 -0
- package/src/modules/Luciq.ts +210 -12
- package/src/modules/NetworkLogger.ts +26 -1
- package/src/modules/apm/ScreenLoadingManager.ts +364 -0
- package/src/native/NativeAPM.ts +22 -0
- package/src/native/NativeLuciq.ts +1 -1
- package/src/utils/FeatureFlags.ts +44 -0
- package/src/utils/LuciqUtils.ts +64 -0
- package/src/utils/RouteMatcher.ts +83 -0
- package/src/utils/XhrNetworkInterceptor.ts +128 -55
|
@@ -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;
|
package/src/modules/APM.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/modules/Luciq.ts
CHANGED
|
@@ -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 } from '../utils/FeatureFlags';
|
|
13
|
+
import { registerFeatureFlagsListener, initFeatureFlagsCache } 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,10 +89,22 @@ 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) => {
|
|
92
|
+
initFeatureFlagsCache();
|
|
93
|
+
|
|
84
94
|
if (Platform.OS === 'android') {
|
|
85
95
|
// Add android feature flags listener for android
|
|
86
96
|
registerFeatureFlagsListener();
|
|
87
97
|
addOnFeatureUpdatedListener(config);
|
|
98
|
+
|
|
99
|
+
// Enable the JS XHR interceptor synchronously so cold-start requests
|
|
100
|
+
// (fired before LCQ_ON_FEATURES_UPDATED_CALLBACK arrives) are captured.
|
|
101
|
+
handleNetworkInterceptionMode(config);
|
|
102
|
+
|
|
103
|
+
setApmNetworkFlagsIfChanged({
|
|
104
|
+
isNativeInterceptionFeatureEnabled: isNativeInterceptionFeatureEnabled,
|
|
105
|
+
hasAPMNetworkPlugin: hasAPMNetworkPlugin,
|
|
106
|
+
shouldEnableNativeInterception: shouldEnableNativeInterception,
|
|
107
|
+
});
|
|
88
108
|
} else {
|
|
89
109
|
isNativeInterceptionFeatureEnabled = NativeNetworkLogger.isNativeInterceptionEnabled();
|
|
90
110
|
|
|
@@ -116,7 +136,7 @@ export const init = (config: LuciqConfig) => {
|
|
|
116
136
|
reportCurrentViewForAndroid(firstScreen);
|
|
117
137
|
setTimeout(() => {
|
|
118
138
|
if (_currentScreen === firstScreen) {
|
|
119
|
-
NativeLuciq.reportScreenChange(firstScreen);
|
|
139
|
+
NativeLuciq.reportScreenChange(firstScreen, null);
|
|
120
140
|
_currentScreen = null;
|
|
121
141
|
}
|
|
122
142
|
}, 1000);
|
|
@@ -158,12 +178,14 @@ export const setWebViewUserInteractionsTrackingEnabled = (isEnabled: boolean) =>
|
|
|
158
178
|
* Handles app state changes and updates APM network flags if necessary.
|
|
159
179
|
*/
|
|
160
180
|
const handleAppStateChange = async (nextAppState: AppStateStatus, config: LuciqConfig) => {
|
|
161
|
-
// Checks if
|
|
181
|
+
// Checks if the app has come to the foreground
|
|
162
182
|
if (['inactive', 'background'].includes(_currentAppState) && nextAppState === 'active') {
|
|
163
183
|
const isUpdated = await fetchApmNetworkFlags();
|
|
164
184
|
if (isUpdated) {
|
|
165
185
|
refreshAPMNetworkConfigs(config);
|
|
166
186
|
}
|
|
187
|
+
// Refresh screen loading flags from native
|
|
188
|
+
await ScreenLoadingManager.refreshFlags();
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
_currentAppState = nextAppState;
|
|
@@ -756,6 +778,139 @@ export const onReportSubmitHandler = (handler?: (report: Report) => void) => {
|
|
|
756
778
|
NativeLuciq.setPreSendingHandler(handler);
|
|
757
779
|
};
|
|
758
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Helper to clear the state change timeout
|
|
783
|
+
*/
|
|
784
|
+
const _clearStateChangeTimeout = (): void => {
|
|
785
|
+
if (_stateChangeTimeout) {
|
|
786
|
+
clearTimeout(_stateChangeTimeout);
|
|
787
|
+
_stateChangeTimeout = undefined;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Handles React Navigation's __unsafe_action__ event
|
|
793
|
+
* This fires WHEN a navigation action is dispatched (the start of navigation)
|
|
794
|
+
*/
|
|
795
|
+
const _onNavigationAction = (event?: any): void => {
|
|
796
|
+
// Check for noop actions that shouldn't create spans
|
|
797
|
+
if (event?.data?.noop) {
|
|
798
|
+
Logger.log('[ScreenLoading] Navigation action is a noop, not starting span');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Skip non-navigation actions (like SET_PARAMS, OPEN_DRAWER, etc.)
|
|
803
|
+
const actionType = event?.data?.action?.type;
|
|
804
|
+
if (
|
|
805
|
+
actionType &&
|
|
806
|
+
['SET_PARAMS', 'OPEN_DRAWER', 'CLOSE_DRAWER', 'TOGGLE_DRAWER'].includes(actionType)
|
|
807
|
+
) {
|
|
808
|
+
Logger.log(`[ScreenLoading] Skipping non-navigation action: ${actionType}`);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// If there's an existing active span, it means navigation was interrupted
|
|
813
|
+
// Discard the previous span as it never completed
|
|
814
|
+
if (_activeNavigationSpanId) {
|
|
815
|
+
Logger.log('[ScreenLoading] Discarding incomplete previous navigation span');
|
|
816
|
+
// Mark the span as cancelled/error since state change never occurred
|
|
817
|
+
const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
|
|
818
|
+
if (span) {
|
|
819
|
+
ScreenLoadingManager.endSpan(_activeNavigationSpanId);
|
|
820
|
+
}
|
|
821
|
+
_activeNavigationSpanId = null;
|
|
822
|
+
_clearStateChangeTimeout();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Create a new span for this navigation action
|
|
826
|
+
// We don't know the destination screen yet, so use a placeholder name
|
|
827
|
+
if (ScreenLoadingManager.isFeatureEnabled()) {
|
|
828
|
+
const span = ScreenLoadingManager.createSpan('NavigationPending', false);
|
|
829
|
+
if (span) {
|
|
830
|
+
_activeNavigationSpanId = span.spanId;
|
|
831
|
+
Logger.log(`[ScreenLoading] Started span ${span.spanId} on navigation dispatch`);
|
|
832
|
+
|
|
833
|
+
// Set a safety timeout to discard the span if state never changes
|
|
834
|
+
// This prevents memory leaks from incomplete navigations
|
|
835
|
+
_stateChangeTimeout = setTimeout(() => {
|
|
836
|
+
if (_activeNavigationSpanId === span.spanId) {
|
|
837
|
+
Logger.warn(
|
|
838
|
+
`[ScreenLoading] Navigation span ${span.spanId} timed out - state never changed`,
|
|
839
|
+
);
|
|
840
|
+
ScreenLoadingManager.endSpan(span.spanId);
|
|
841
|
+
_activeNavigationSpanId = null;
|
|
842
|
+
}
|
|
843
|
+
}, STATE_CHANGE_TIMEOUT_MS);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Handles React Navigation's state event
|
|
850
|
+
* This fires AFTER the navigation state has changed (the screen is mounted)
|
|
851
|
+
*/
|
|
852
|
+
const _onNavigationStateChange = (): void => {
|
|
853
|
+
if (!_navigationRef?.current) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const previousRouteName = _currentRoute;
|
|
858
|
+
const currentRoute = _navigationRef.current.getCurrentRoute();
|
|
859
|
+
const currentRouteName = currentRoute?.name || null;
|
|
860
|
+
|
|
861
|
+
// If no route or same route, ignore
|
|
862
|
+
if (!currentRouteName || previousRouteName === currentRouteName) {
|
|
863
|
+
// Still need to clean up the span if one was created
|
|
864
|
+
if (_activeNavigationSpanId) {
|
|
865
|
+
Logger.log('[ScreenLoading] Navigation resulted in same route, discarding span');
|
|
866
|
+
ScreenLoadingManager.endSpan(_activeNavigationSpanId);
|
|
867
|
+
_activeNavigationSpanId = null;
|
|
868
|
+
_clearStateChangeTimeout();
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Capture the span ID BEFORE clearing it so we can pass it to reportScreenChange
|
|
874
|
+
let spanIdForReport: string | null = _activeNavigationSpanId;
|
|
875
|
+
|
|
876
|
+
// Complete the active navigation span if one exists
|
|
877
|
+
if (_activeNavigationSpanId) {
|
|
878
|
+
// Now that we know the actual route name, check if it's excluded
|
|
879
|
+
if (ScreenLoadingManager.isRouteExcluded(currentRouteName)) {
|
|
880
|
+
Logger.log(`[ScreenLoading] Route "${currentRouteName}" is excluded, discarding span`);
|
|
881
|
+
ScreenLoadingManager.discardSpan(_activeNavigationSpanId);
|
|
882
|
+
spanIdForReport = null;
|
|
883
|
+
_activeNavigationSpanId = null;
|
|
884
|
+
_clearStateChangeTimeout();
|
|
885
|
+
} else {
|
|
886
|
+
const span = ScreenLoadingManager.getActiveSpan(_activeNavigationSpanId);
|
|
887
|
+
if (span) {
|
|
888
|
+
// Update the span name from placeholder to actual screen name
|
|
889
|
+
span.screenName = currentRouteName;
|
|
890
|
+
|
|
891
|
+
// End the span - the native frame tracker will provide the actual render timestamp
|
|
892
|
+
ScreenLoadingManager.endSpan(_activeNavigationSpanId)
|
|
893
|
+
.then(() => {
|
|
894
|
+
Logger.log(`[ScreenLoading] Completed span for navigation to ${currentRouteName}`);
|
|
895
|
+
})
|
|
896
|
+
.catch((error) => {
|
|
897
|
+
Logger.warn('[ScreenLoading] Failed to end navigation span:', error);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Clear the active span and timeout
|
|
902
|
+
_activeNavigationSpanId = null;
|
|
903
|
+
_clearStateChangeTimeout();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Update the current route for the rest of Luciq's tracking
|
|
908
|
+
_currentRoute = currentRouteName;
|
|
909
|
+
|
|
910
|
+
// Report to native
|
|
911
|
+
NativeLuciq.reportScreenChange(currentRouteName, spanIdForReport);
|
|
912
|
+
};
|
|
913
|
+
|
|
759
914
|
export const onNavigationStateChange = (
|
|
760
915
|
prevState: NavigationStateV4,
|
|
761
916
|
currentState: NavigationStateV4,
|
|
@@ -765,17 +920,33 @@ export const onNavigationStateChange = (
|
|
|
765
920
|
const prevScreen = LuciqUtils.getActiveRouteName(prevState);
|
|
766
921
|
|
|
767
922
|
if (prevScreen !== currentScreen) {
|
|
923
|
+
// Start Screen Loading measurement for v4
|
|
924
|
+
let screenLoadingSpanId: string | null = null;
|
|
925
|
+
if (ScreenLoadingManager.isFeatureEnabled()) {
|
|
926
|
+
const span = ScreenLoadingManager.createSpan(currentScreen || 'Unknown', false);
|
|
927
|
+
if (span) {
|
|
928
|
+
screenLoadingSpanId = span.spanId;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
768
932
|
reportCurrentViewForAndroid(currentScreen);
|
|
769
933
|
if (_currentScreen != null && _currentScreen !== firstScreen) {
|
|
770
|
-
NativeLuciq.reportScreenChange(_currentScreen);
|
|
934
|
+
NativeLuciq.reportScreenChange(_currentScreen, screenLoadingSpanId);
|
|
771
935
|
_currentScreen = null;
|
|
772
936
|
}
|
|
773
937
|
_currentScreen = currentScreen;
|
|
774
938
|
setTimeout(() => {
|
|
775
939
|
if (currentScreen && _currentScreen === currentScreen) {
|
|
776
|
-
NativeLuciq.reportScreenChange(currentScreen);
|
|
940
|
+
NativeLuciq.reportScreenChange(currentScreen, screenLoadingSpanId);
|
|
777
941
|
_currentScreen = null;
|
|
778
942
|
}
|
|
943
|
+
|
|
944
|
+
// End Screen Loading measurement for v4
|
|
945
|
+
if (screenLoadingSpanId) {
|
|
946
|
+
ScreenLoadingManager.endSpan(screenLoadingSpanId).catch((error) => {
|
|
947
|
+
Logger.warn('[ScreenLoading] Failed to end span:', error);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
779
950
|
}, 1000);
|
|
780
951
|
}
|
|
781
952
|
};
|
|
@@ -785,17 +956,28 @@ export const onStateChange = (state?: NavigationStateV5) => {
|
|
|
785
956
|
return;
|
|
786
957
|
}
|
|
787
958
|
|
|
959
|
+
// Delegate to the new state change handler for Screen Loading
|
|
960
|
+
// This handles reportScreenChange when setNavigationListener was called
|
|
961
|
+
_onNavigationStateChange();
|
|
962
|
+
|
|
963
|
+
// When setNavigationListener is used, _onNavigationStateChange already handles
|
|
964
|
+
// reportScreenChange properly - skip legacy logic to avoid duplicate calls
|
|
965
|
+
if (_navigationRef?.current) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Fallback: Legacy screen tracking for users who only use onStateChange without setNavigationListener
|
|
788
970
|
const currentScreen = LuciqUtils.getFullRoute(state);
|
|
789
971
|
reportCurrentViewForAndroid(currentScreen);
|
|
790
972
|
if (_currentScreen !== null && _currentScreen !== firstScreen) {
|
|
791
|
-
NativeLuciq.reportScreenChange(_currentScreen);
|
|
973
|
+
NativeLuciq.reportScreenChange(_currentScreen, null);
|
|
792
974
|
_currentScreen = null;
|
|
793
975
|
}
|
|
794
976
|
|
|
795
977
|
_currentScreen = currentScreen;
|
|
796
978
|
setTimeout(() => {
|
|
797
979
|
if (_currentScreen === currentScreen) {
|
|
798
|
-
NativeLuciq.reportScreenChange(currentScreen);
|
|
980
|
+
NativeLuciq.reportScreenChange(currentScreen, null);
|
|
799
981
|
_currentScreen = null;
|
|
800
982
|
}
|
|
801
983
|
}, 1000);
|
|
@@ -809,13 +991,29 @@ export const onStateChange = (state?: NavigationStateV5) => {
|
|
|
809
991
|
export const setNavigationListener = (
|
|
810
992
|
navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>,
|
|
811
993
|
) => {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
994
|
+
// Store the navigationRef for Screen Loading tracking
|
|
995
|
+
_navigationRef = navigationRef;
|
|
996
|
+
|
|
997
|
+
if (!navigationRef?.current) {
|
|
998
|
+
Logger.warn('[Luciq] Navigation ref not available, cannot set listeners');
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Register the __unsafe_action__ listener for span creation
|
|
1003
|
+
// This listener fires on navigation dispatch (start of navigation)
|
|
1004
|
+
navigationRef.current.addListener('__unsafe_action__', _onNavigationAction);
|
|
1005
|
+
|
|
1006
|
+
// NOTE: We do NOT register a 'state' listener here because the user is expected
|
|
1007
|
+
// to pass Luciq.onStateChange to NavigationContainer's onStateChange prop.
|
|
1008
|
+
// Registering both would cause duplicate reportScreenChange calls.
|
|
1009
|
+
|
|
1010
|
+
Logger.log('[Luciq] Registered Screen Loading listener (__unsafe_action__)');
|
|
1011
|
+
|
|
1012
|
+
// return stateListener;
|
|
815
1013
|
};
|
|
816
1014
|
|
|
817
1015
|
export const reportScreenChange = (screenName: string) => {
|
|
818
|
-
NativeLuciq.reportScreenChange(screenName);
|
|
1016
|
+
NativeLuciq.reportScreenChange(screenName, null);
|
|
819
1017
|
};
|
|
820
1018
|
|
|
821
1019
|
/**
|
|
@@ -879,7 +1077,7 @@ export const componentDidAppearListener = (event: ComponentDidAppearEvent) => {
|
|
|
879
1077
|
return;
|
|
880
1078
|
}
|
|
881
1079
|
if (_lastScreen !== event.componentName) {
|
|
882
|
-
NativeLuciq.reportScreenChange(event.componentName);
|
|
1080
|
+
NativeLuciq.reportScreenChange(event.componentName, null);
|
|
883
1081
|
_lastScreen = event.componentName;
|
|
884
1082
|
}
|
|
885
1083
|
};
|
|
@@ -39,10 +39,17 @@ 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
|
+
|
|
42
44
|
export const setEnabled = (isEnabled: boolean) => {
|
|
43
45
|
if (isEnabled) {
|
|
44
46
|
xhr.enableInterception();
|
|
45
47
|
xhr.setOnDoneCallback(async (network) => {
|
|
48
|
+
Logger.debug(
|
|
49
|
+
NET_TAG,
|
|
50
|
+
`[NetworkLogger] onDoneCallback received: ${network.method} ${network.url}, status=${network.responseCode}`,
|
|
51
|
+
);
|
|
52
|
+
|
|
46
53
|
// eslint-disable-next-line no-new-func
|
|
47
54
|
const predicate = Function('network', 'return ' + _requestFilterExpression);
|
|
48
55
|
|
|
@@ -50,12 +57,17 @@ export const setEnabled = (isEnabled: boolean) => {
|
|
|
50
57
|
const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeLuciq.getNetworkBodyMaxSize();
|
|
51
58
|
try {
|
|
52
59
|
if (_networkDataObfuscationHandler) {
|
|
60
|
+
Logger.debug(NET_TAG, `[NetworkLogger] Running obfuscation handler for ${network.url}`);
|
|
53
61
|
network = await _networkDataObfuscationHandler(network);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
if (__DEV__) {
|
|
57
65
|
const urlPort = getPortFromUrl(network.url);
|
|
58
66
|
if (urlPort === LuciqRNConfig.metroDevServerPort) {
|
|
67
|
+
Logger.debug(
|
|
68
|
+
NET_TAG,
|
|
69
|
+
`[NetworkLogger] Skipping Metro dev server request: ${network.url}`,
|
|
70
|
+
);
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
}
|
|
@@ -97,10 +109,23 @@ export const setEnabled = (isEnabled: boolean) => {
|
|
|
97
109
|
);
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
Logger.debug(
|
|
113
|
+
NET_TAG,
|
|
114
|
+
`[NetworkLogger] Reporting network log to native: ${network.method} ${network.url}`,
|
|
115
|
+
);
|
|
100
116
|
reportNetworkLog(network);
|
|
101
117
|
} catch (e) {
|
|
102
|
-
Logger.error(
|
|
118
|
+
Logger.error(
|
|
119
|
+
NET_TAG,
|
|
120
|
+
`[NetworkLogger] Error processing network log for ${network.url}:`,
|
|
121
|
+
e,
|
|
122
|
+
);
|
|
103
123
|
}
|
|
124
|
+
} else {
|
|
125
|
+
Logger.debug(
|
|
126
|
+
NET_TAG,
|
|
127
|
+
`[NetworkLogger] Request filtered out by predicate: ${network.method} ${network.url}, expression="${_requestFilterExpression}"`,
|
|
128
|
+
);
|
|
104
129
|
}
|
|
105
130
|
});
|
|
106
131
|
} else {
|