@dreamhorizonorg/pulse-react-native 0.0.1 → 0.0.2
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/PulseReactNativeOtel.podspec +1 -1
- package/README.md +34 -879
- package/android/build.gradle +10 -15
- package/android/proguard-rules.pro +3 -99
- package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +87 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseOtelConstants.kt +1 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelLogger.kt +3 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +53 -3
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelPackage.kt +1 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelTracer.kt +24 -8
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesLogRecordProcessor.kt +21 -0
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesSpanProcessor.kt +30 -0
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenNameTracker.kt +17 -0
- package/app.plugin.js +1 -0
- package/ios/PulseReactNativeOtel.mm +7 -1
- package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
- package/lib/module/config.js +29 -9
- package/lib/module/config.js.map +1 -1
- package/lib/module/errorBoundary.js.map +1 -1
- package/lib/module/events.js +6 -0
- package/lib/module/events.js.map +1 -1
- package/lib/module/index.js +4 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigation/index.js +172 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/navigation.interface.js +2 -0
- package/lib/module/navigation/navigation.interface.js.map +1 -0
- package/lib/module/navigation/screen-interactive.js +101 -0
- package/lib/module/navigation/screen-interactive.js.map +1 -0
- package/lib/module/navigation/screen-load.js +68 -0
- package/lib/module/navigation/screen-load.js.map +1 -0
- package/lib/module/navigation/screen-session.js +60 -0
- package/lib/module/navigation/screen-session.js.map +1 -0
- package/lib/module/navigation/useNavigationTracking.js +33 -0
- package/lib/module/navigation/useNavigationTracking.js.map +1 -0
- package/lib/module/navigation/utils.js +17 -0
- package/lib/module/navigation/utils.js.map +1 -0
- package/lib/module/network-interceptor/graphql-helper.js +92 -0
- package/lib/module/network-interceptor/graphql-helper.js.map +1 -0
- package/lib/module/network-interceptor/request-tracker-xhr.js +2 -1
- package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
- package/lib/module/network-interceptor/span-helpers.js +24 -16
- package/lib/module/network-interceptor/span-helpers.js.map +1 -1
- package/lib/module/network-interceptor/url-helper.js +58 -2
- package/lib/module/network-interceptor/url-helper.js.map +1 -1
- package/lib/module/pulse.constants.js +42 -0
- package/lib/module/pulse.constants.js.map +1 -0
- package/lib/module/trace.js +17 -2
- package/lib/module/trace.js.map +1 -1
- package/lib/typescript/plugin/src/index.d.ts +5 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/types.d.ts +27 -0
- package/lib/typescript/plugin/src/types.d.ts.map +1 -0
- package/lib/typescript/plugin/src/utils.d.ts +10 -0
- package/lib/typescript/plugin/src/utils.d.ts.map +1 -0
- package/lib/typescript/plugin/src/withAndroidPulse.d.ts +4 -0
- package/lib/typescript/plugin/src/withAndroidPulse.d.ts.map +1 -0
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +8 -2
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
- package/lib/typescript/src/config.d.ts +8 -2
- package/lib/typescript/src/config.d.ts.map +1 -1
- package/lib/typescript/src/errorBoundary.d.ts.map +1 -1
- package/lib/typescript/src/events.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/index.d.ts +12 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/navigation.interface.d.ts +17 -0
- package/lib/typescript/src/navigation/navigation.interface.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-interactive.d.ts +16 -0
- package/lib/typescript/src/navigation/screen-interactive.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-load.d.ts +13 -0
- package/lib/typescript/src/navigation/screen-load.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-session.d.ts +15 -0
- package/lib/typescript/src/navigation/screen-session.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts +5 -0
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts.map +1 -0
- package/lib/typescript/src/navigation/utils.d.ts +8 -0
- package/lib/typescript/src/navigation/utils.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/graphql-helper.d.ts +8 -0
- package/lib/typescript/src/network-interceptor/graphql-helper.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts +1 -1
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/url-helper.d.ts +9 -0
- package/lib/typescript/src/network-interceptor/url-helper.d.ts.map +1 -1
- package/lib/typescript/src/pulse.constants.d.ts +35 -0
- package/lib/typescript/src/pulse.constants.d.ts.map +1 -0
- package/lib/typescript/src/pulse.interface.d.ts +2 -1
- package/lib/typescript/src/pulse.interface.d.ts.map +1 -1
- package/lib/typescript/src/trace.d.ts +7 -0
- package/lib/typescript/src/trace.d.ts.map +1 -1
- package/package.json +29 -9
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/index.js +10 -0
- package/plugin/build/types.d.ts +26 -0
- package/plugin/build/types.js +2 -0
- package/plugin/build/utils.d.ts +9 -0
- package/plugin/build/utils.js +102 -0
- package/plugin/build/withAndroidPulse.d.ts +3 -0
- package/plugin/build/withAndroidPulse.js +53 -0
- package/scripts/pulse-cli.js +82 -0
- package/scripts/uploadService.js +122 -0
- package/scripts/utils.js +125 -0
- package/src/NativePulseReactNativeOtel.ts +11 -2
- package/src/config.ts +37 -8
- package/src/errorBoundary.tsx +11 -5
- package/src/events.ts +7 -0
- package/src/global.d.ts +0 -1
- package/src/index.tsx +6 -3
- package/src/navigation/index.ts +306 -0
- package/src/navigation/navigation.interface.ts +19 -0
- package/src/navigation/screen-interactive.ts +149 -0
- package/src/navigation/screen-load.ts +103 -0
- package/src/navigation/screen-session.ts +87 -0
- package/src/navigation/useNavigationTracking.ts +50 -0
- package/src/navigation/utils.ts +19 -0
- package/src/network-interceptor/graphql-helper.ts +110 -0
- package/src/network-interceptor/request-tracker-xhr.ts +3 -1
- package/src/network-interceptor/span-helpers.ts +27 -18
- package/src/network-interceptor/url-helper.ts +67 -1
- package/src/pulse.constants.ts +38 -0
- package/src/pulse.interface.ts +6 -1
- package/src/trace.ts +25 -2
- package/LICENSE +0 -20
- package/lib/module/network-interceptor/request-tracker-fetch.js +0 -72
- package/lib/module/network-interceptor/request-tracker-fetch.js.map +0 -1
- package/lib/module/reactNavigation.js +0 -100
- package/lib/module/reactNavigation.js.map +0 -1
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts +0 -7
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts.map +0 -1
- package/lib/typescript/src/reactNavigation.d.ts +0 -10
- package/lib/typescript/src/reactNavigation.d.ts.map +0 -1
- package/src/network-interceptor/request-tracker-fetch.ts +0 -96
- package/src/reactNavigation.tsx +0 -146
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
NavigationContainer,
|
|
5
|
+
NavigationIntegrationOptions,
|
|
6
|
+
NavigationRoute,
|
|
7
|
+
} from './navigation.interface';
|
|
8
|
+
import { pushRecentRouteKey, LOG_TAGS } from './utils';
|
|
9
|
+
import { discardSpan } from '../trace';
|
|
10
|
+
import {
|
|
11
|
+
createScreenLoadTracker,
|
|
12
|
+
type ScreenLoadState,
|
|
13
|
+
INITIAL_SCREEN_LOAD_STATE,
|
|
14
|
+
} from './screen-load';
|
|
15
|
+
import {
|
|
16
|
+
createScreenInteractiveTracker,
|
|
17
|
+
markContentReady,
|
|
18
|
+
clearGlobalMarkContentReady,
|
|
19
|
+
type ScreenInteractiveState,
|
|
20
|
+
INITIAL_SCREEN_INTERACTIVE_STATE,
|
|
21
|
+
} from './screen-interactive';
|
|
22
|
+
import {
|
|
23
|
+
createScreenSessionTracker,
|
|
24
|
+
type ScreenSessionState,
|
|
25
|
+
INITIAL_SCREEN_SESSION_STATE,
|
|
26
|
+
} from './screen-session';
|
|
27
|
+
import { useNavigationTracking as useNavigationTrackingBase } from './useNavigationTracking';
|
|
28
|
+
import { isSupportedPlatform } from '../initialization';
|
|
29
|
+
import PulseReactNativeOtel from '../NativePulseReactNativeOtel';
|
|
30
|
+
import { getFeaturesFromRemoteConfig } from '../config';
|
|
31
|
+
|
|
32
|
+
export type { NavigationRoute, NavigationIntegrationOptions };
|
|
33
|
+
|
|
34
|
+
export interface ReactNavigationIntegration {
|
|
35
|
+
registerNavigationContainer: (
|
|
36
|
+
maybeNavigationContainer: unknown
|
|
37
|
+
) => () => void;
|
|
38
|
+
markContentReady: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createReactNavigationIntegration(
|
|
42
|
+
options?: NavigationIntegrationOptions
|
|
43
|
+
): ReactNavigationIntegration {
|
|
44
|
+
const features = getFeaturesFromRemoteConfig();
|
|
45
|
+
const screenSessionTracking =
|
|
46
|
+
features?.screen_session ?? options?.screenSessionTracking ?? true;
|
|
47
|
+
const screenNavigationTracking =
|
|
48
|
+
features?.rn_screen_load ?? options?.screenNavigationTracking ?? true;
|
|
49
|
+
const screenInteractiveTracking =
|
|
50
|
+
features?.rn_screen_interactive ??
|
|
51
|
+
options?.screenInteractiveTracking ??
|
|
52
|
+
false;
|
|
53
|
+
|
|
54
|
+
let navigationContainer: NavigationContainer | undefined;
|
|
55
|
+
let recentRouteKeys: string[] = [];
|
|
56
|
+
let isInitialized = false;
|
|
57
|
+
let appStateSubscription: { remove: () => void } | undefined;
|
|
58
|
+
|
|
59
|
+
const screenLoadState: ScreenLoadState = {
|
|
60
|
+
...INITIAL_SCREEN_LOAD_STATE,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const screenInteractiveState: ScreenInteractiveState = {
|
|
64
|
+
...INITIAL_SCREEN_INTERACTIVE_STATE,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const screenSessionState: ScreenSessionState = {
|
|
68
|
+
...INITIAL_SCREEN_SESSION_STATE,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const screenInteractiveTracker = createScreenInteractiveTracker(
|
|
72
|
+
screenInteractiveTracking,
|
|
73
|
+
screenInteractiveState,
|
|
74
|
+
navigationContainer
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const screenLoadTracker = createScreenLoadTracker(
|
|
78
|
+
screenNavigationTracking,
|
|
79
|
+
screenLoadState,
|
|
80
|
+
() => recentRouteKeys,
|
|
81
|
+
(key: string) => {
|
|
82
|
+
recentRouteKeys = pushRecentRouteKey(recentRouteKeys, key);
|
|
83
|
+
},
|
|
84
|
+
undefined
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const screenSessionTracker = createScreenSessionTracker(
|
|
88
|
+
screenSessionTracking,
|
|
89
|
+
screenSessionState
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const setCurrentScreenName = (screenName: string): void => {
|
|
93
|
+
if (isSupportedPlatform()) {
|
|
94
|
+
PulseReactNativeOtel.setCurrentScreenName(screenName);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onNavigationDispatch = (): void => {
|
|
99
|
+
try {
|
|
100
|
+
if (screenInteractiveTracking) {
|
|
101
|
+
screenInteractiveTracker.discardScreenInteractive(
|
|
102
|
+
'user navigated away'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
screenSessionTracking &&
|
|
108
|
+
screenSessionState.screenSessionSpan &&
|
|
109
|
+
navigationContainer
|
|
110
|
+
) {
|
|
111
|
+
const currentRoute = navigationContainer.getCurrentRoute();
|
|
112
|
+
screenSessionTracker.endScreenSession(currentRoute?.name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
screenLoadTracker.startNavigationSpan();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`${LOG_TAGS.NAVIGATION} Error in onNavigationDispatch:`,
|
|
119
|
+
error
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (screenLoadState.navigationSpan?.spanId) {
|
|
123
|
+
discardSpan(screenLoadState.navigationSpan.spanId);
|
|
124
|
+
screenLoadState.navigationSpan = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const onStateChange = (): void => {
|
|
130
|
+
try {
|
|
131
|
+
if (!navigationContainer) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const currentRoute = navigationContainer.getCurrentRoute();
|
|
136
|
+
if (!currentRoute) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (currentRoute.name) {
|
|
141
|
+
setCurrentScreenName(currentRoute.name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
screenLoadTracker.handleStateChange(currentRoute);
|
|
145
|
+
|
|
146
|
+
const appState = AppState.currentState as AppStateStatus;
|
|
147
|
+
if (
|
|
148
|
+
appState &&
|
|
149
|
+
screenSessionTracker.shouldStartSession(currentRoute, appState)
|
|
150
|
+
) {
|
|
151
|
+
screenSessionTracker.startScreenSession(currentRoute);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (screenInteractiveTracking) {
|
|
155
|
+
screenInteractiveTracker.startScreenInteractive(currentRoute);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`${LOG_TAGS.NAVIGATION} Error in onStateChange:`, error);
|
|
159
|
+
if (screenLoadState.navigationSpan?.spanId) {
|
|
160
|
+
discardSpan(screenLoadState.navigationSpan.spanId);
|
|
161
|
+
screenLoadState.navigationSpan = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleAppStateChange = (nextAppState: AppStateStatus): void => {
|
|
167
|
+
try {
|
|
168
|
+
screenSessionTracker.handleAppStateChange(
|
|
169
|
+
nextAppState,
|
|
170
|
+
navigationContainer
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.warn(
|
|
174
|
+
`${LOG_TAGS.NAVIGATION} Error in handleAppStateChange:`,
|
|
175
|
+
error
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const registerNavigationContainer = (
|
|
181
|
+
maybeNavigationContainer: unknown
|
|
182
|
+
): (() => void) => {
|
|
183
|
+
try {
|
|
184
|
+
let container: NavigationContainer | undefined;
|
|
185
|
+
if (
|
|
186
|
+
typeof maybeNavigationContainer === 'object' &&
|
|
187
|
+
maybeNavigationContainer !== null &&
|
|
188
|
+
'current' in maybeNavigationContainer
|
|
189
|
+
) {
|
|
190
|
+
container = maybeNavigationContainer.current as NavigationContainer;
|
|
191
|
+
} else {
|
|
192
|
+
container = maybeNavigationContainer as NavigationContainer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!container) {
|
|
196
|
+
console.warn(`${LOG_TAGS.NAVIGATION} Invalid navigation container ref`);
|
|
197
|
+
return () => {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isInitialized && navigationContainer === container) {
|
|
201
|
+
return () => {
|
|
202
|
+
if (screenSessionTracking && screenSessionState.screenSessionSpan) {
|
|
203
|
+
const currentRoute = container.getCurrentRoute();
|
|
204
|
+
screenSessionTracker.endScreenSession(currentRoute?.name);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
navigationContainer = container;
|
|
210
|
+
|
|
211
|
+
const updatedInteractiveTracker = createScreenInteractiveTracker(
|
|
212
|
+
screenInteractiveTracking,
|
|
213
|
+
screenInteractiveState,
|
|
214
|
+
navigationContainer
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
navigationContainer.addListener(
|
|
218
|
+
'__unsafe_action__',
|
|
219
|
+
onNavigationDispatch
|
|
220
|
+
);
|
|
221
|
+
navigationContainer.addListener('state', onStateChange);
|
|
222
|
+
|
|
223
|
+
const unmountCleanup = (): void => {
|
|
224
|
+
if (screenSessionTracking && screenSessionState.screenSessionSpan) {
|
|
225
|
+
const currentRoute = container.getCurrentRoute();
|
|
226
|
+
screenSessionTracker.endScreenSession(currentRoute?.name);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (screenInteractiveTracking) {
|
|
230
|
+
screenInteractiveTracker.discardScreenInteractive(
|
|
231
|
+
'navigation container unmounted'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
screenLoadTracker.endNavigationSpan();
|
|
236
|
+
|
|
237
|
+
if (navigationContainer === container) {
|
|
238
|
+
if (appStateSubscription) {
|
|
239
|
+
appStateSubscription.remove();
|
|
240
|
+
appStateSubscription = undefined;
|
|
241
|
+
}
|
|
242
|
+
navigationContainer = undefined;
|
|
243
|
+
isInitialized = false;
|
|
244
|
+
|
|
245
|
+
clearGlobalMarkContentReady(
|
|
246
|
+
updatedInteractiveTracker.markContentReady
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const currentRoute = container.getCurrentRoute();
|
|
252
|
+
if (currentRoute) {
|
|
253
|
+
screenLoadState.latestRoute = currentRoute;
|
|
254
|
+
recentRouteKeys = pushRecentRouteKey(recentRouteKeys, currentRoute.key);
|
|
255
|
+
if (currentRoute.name) {
|
|
256
|
+
setCurrentScreenName(currentRoute.name);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const appState = AppState.currentState as AppStateStatus;
|
|
260
|
+
if (
|
|
261
|
+
appState &&
|
|
262
|
+
screenSessionTracker.shouldStartSession(currentRoute, appState)
|
|
263
|
+
) {
|
|
264
|
+
screenSessionTracker.startScreenSession(currentRoute);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (screenInteractiveTracking) {
|
|
268
|
+
updatedInteractiveTracker.startScreenInteractive(currentRoute);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
appStateSubscription = AppState.addEventListener(
|
|
273
|
+
'change',
|
|
274
|
+
handleAppStateChange
|
|
275
|
+
);
|
|
276
|
+
isInitialized = true;
|
|
277
|
+
|
|
278
|
+
return unmountCleanup;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(
|
|
281
|
+
`${LOG_TAGS.NAVIGATION} Error registering container:`,
|
|
282
|
+
error
|
|
283
|
+
);
|
|
284
|
+
return () => {};
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
registerNavigationContainer,
|
|
290
|
+
markContentReady: screenInteractiveTracker.markContentReady,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { markContentReady };
|
|
295
|
+
|
|
296
|
+
export function useNavigationTracking(
|
|
297
|
+
navigationRef: RefObject<any>,
|
|
298
|
+
options?: NavigationIntegrationOptions
|
|
299
|
+
): () => void {
|
|
300
|
+
const { createNavigationIntegrationWithConfig } = require('../config');
|
|
301
|
+
return useNavigationTrackingBase(
|
|
302
|
+
navigationRef,
|
|
303
|
+
options,
|
|
304
|
+
createNavigationIntegrationWithConfig
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface NavigationRoute {
|
|
2
|
+
name: string;
|
|
3
|
+
key: string;
|
|
4
|
+
params?: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface NavigationContainer {
|
|
8
|
+
addListener: (
|
|
9
|
+
type: string,
|
|
10
|
+
listener: (event?: unknown) => void
|
|
11
|
+
) => { remove: () => void } | void;
|
|
12
|
+
getCurrentRoute: () => NavigationRoute | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NavigationIntegrationOptions {
|
|
16
|
+
screenSessionTracking?: boolean;
|
|
17
|
+
screenNavigationTracking?: boolean;
|
|
18
|
+
screenInteractiveTracking?: boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Pulse, type Span } from '../index';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { SPAN_NAMES, ATTRIBUTE_KEYS, PULSE_TYPES } from '../pulse.constants';
|
|
4
|
+
import { discardSpan } from '../trace';
|
|
5
|
+
import type {
|
|
6
|
+
NavigationRoute,
|
|
7
|
+
NavigationContainer,
|
|
8
|
+
} from './navigation.interface';
|
|
9
|
+
import { LOG_TAGS } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface ScreenInteractiveState {
|
|
12
|
+
screenInteractiveSpan: Span | undefined;
|
|
13
|
+
currentInteractiveRouteKey: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const INITIAL_SCREEN_INTERACTIVE_STATE: ScreenInteractiveState = {
|
|
17
|
+
screenInteractiveSpan: undefined,
|
|
18
|
+
currentInteractiveRouteKey: undefined,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let globalMarkContentReady: (() => void) | undefined;
|
|
22
|
+
|
|
23
|
+
export function createScreenInteractiveTracker(
|
|
24
|
+
enabled: boolean,
|
|
25
|
+
state: ScreenInteractiveState,
|
|
26
|
+
navigationContainer: NavigationContainer | undefined
|
|
27
|
+
) {
|
|
28
|
+
const discardScreenInteractive = (reason: string): void => {
|
|
29
|
+
if (state.screenInteractiveSpan) {
|
|
30
|
+
console.log(
|
|
31
|
+
`${LOG_TAGS.SCREEN_INTERACTIVE} screen_interactive span discarded: ${reason} (routeKey: ${state.currentInteractiveRouteKey})`
|
|
32
|
+
);
|
|
33
|
+
if (state.screenInteractiveSpan.spanId) {
|
|
34
|
+
discardSpan(state.screenInteractiveSpan.spanId);
|
|
35
|
+
}
|
|
36
|
+
state.screenInteractiveSpan = undefined;
|
|
37
|
+
state.currentInteractiveRouteKey = undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const startScreenInteractive = (route: NavigationRoute): void => {
|
|
42
|
+
if (!enabled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
state.screenInteractiveSpan &&
|
|
48
|
+
state.currentInteractiveRouteKey === route.key
|
|
49
|
+
) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (state.screenInteractiveSpan) {
|
|
54
|
+
discardScreenInteractive('previous span replaced by new navigation');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
state.screenInteractiveSpan = Pulse.startSpan(
|
|
58
|
+
SPAN_NAMES.SCREEN_INTERACTIVE,
|
|
59
|
+
{
|
|
60
|
+
attributes: {
|
|
61
|
+
[ATTRIBUTE_KEYS.PULSE_TYPE]: PULSE_TYPES.SCREEN_INTERACTIVE,
|
|
62
|
+
[ATTRIBUTE_KEYS.SCREEN_NAME]: route.name,
|
|
63
|
+
[ATTRIBUTE_KEYS.ROUTE_KEY]: route.key,
|
|
64
|
+
[ATTRIBUTE_KEYS.PLATFORM]: Platform.OS,
|
|
65
|
+
},
|
|
66
|
+
inheritContext: false,
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
state.currentInteractiveRouteKey = route.key;
|
|
70
|
+
console.log(`${LOG_TAGS.SCREEN_INTERACTIVE} ${route.name} started`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const endScreenInteractive = (routeName?: string): void => {
|
|
74
|
+
if (state.screenInteractiveSpan) {
|
|
75
|
+
state.screenInteractiveSpan.end();
|
|
76
|
+
if (routeName) {
|
|
77
|
+
console.log(`${LOG_TAGS.SCREEN_INTERACTIVE} ${routeName} ended`);
|
|
78
|
+
}
|
|
79
|
+
state.screenInteractiveSpan = undefined;
|
|
80
|
+
state.currentInteractiveRouteKey = undefined;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleMarkContentReady = (): void => {
|
|
85
|
+
try {
|
|
86
|
+
if (!enabled) {
|
|
87
|
+
console.warn(
|
|
88
|
+
`${LOG_TAGS.SCREEN_INTERACTIVE} markContentReady called but screenInteractiveTracking is disabled`
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!state.screenInteractiveSpan) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const currentRoute = navigationContainer?.getCurrentRoute();
|
|
98
|
+
if (!currentRoute) {
|
|
99
|
+
console.warn(
|
|
100
|
+
`${LOG_TAGS.SCREEN_INTERACTIVE} markContentReady called but no current route found`
|
|
101
|
+
);
|
|
102
|
+
discardScreenInteractive('no current route');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (currentRoute.key !== state.currentInteractiveRouteKey) {
|
|
107
|
+
console.warn(
|
|
108
|
+
`${LOG_TAGS.SCREEN_INTERACTIVE} markContentReady called for wrong screen. Expected routeKey: ${state.currentInteractiveRouteKey}, Current: ${currentRoute.key}`
|
|
109
|
+
);
|
|
110
|
+
discardScreenInteractive('route key mismatch');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
endScreenInteractive(currentRoute.name);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(
|
|
117
|
+
`${LOG_TAGS.SCREEN_INTERACTIVE} Error in markContentReady:`,
|
|
118
|
+
error
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
globalMarkContentReady = handleMarkContentReady;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
startScreenInteractive,
|
|
127
|
+
endScreenInteractive,
|
|
128
|
+
discardScreenInteractive,
|
|
129
|
+
markContentReady: handleMarkContentReady,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function markContentReady(): void {
|
|
134
|
+
if (globalMarkContentReady) {
|
|
135
|
+
globalMarkContentReady();
|
|
136
|
+
} else {
|
|
137
|
+
console.warn(
|
|
138
|
+
`${LOG_TAGS.NAVIGATION} markContentReady called but navigation integration not initialized`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function clearGlobalMarkContentReady(
|
|
144
|
+
markContentReadyFn: () => void
|
|
145
|
+
): void {
|
|
146
|
+
if (globalMarkContentReady === markContentReadyFn) {
|
|
147
|
+
globalMarkContentReady = undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Pulse, type Span } from '../index';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
SPAN_NAMES,
|
|
5
|
+
ATTRIBUTE_KEYS,
|
|
6
|
+
PULSE_TYPES,
|
|
7
|
+
PHASE_VALUES,
|
|
8
|
+
} from '../pulse.constants';
|
|
9
|
+
import type { NavigationRoute } from './navigation.interface';
|
|
10
|
+
import { LOG_TAGS } from './utils';
|
|
11
|
+
|
|
12
|
+
export interface ScreenLoadState {
|
|
13
|
+
navigationSpan: Span | undefined;
|
|
14
|
+
latestRoute: NavigationRoute | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const INITIAL_SCREEN_LOAD_STATE: ScreenLoadState = {
|
|
18
|
+
navigationSpan: undefined,
|
|
19
|
+
latestRoute: undefined,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function createScreenLoadTracker(
|
|
23
|
+
enabled: boolean,
|
|
24
|
+
state: ScreenLoadState,
|
|
25
|
+
getRecentRouteKeys: () => string[],
|
|
26
|
+
pushRecentRouteKey: (key: string) => void,
|
|
27
|
+
onLoadEnd?: (route: NavigationRoute) => void
|
|
28
|
+
) {
|
|
29
|
+
const startNavigationSpan = (): void => {
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
state.navigationSpan = Pulse.startSpan(SPAN_NAMES.NAVIGATED, {
|
|
35
|
+
attributes: {
|
|
36
|
+
[ATTRIBUTE_KEYS.PULSE_TYPE]: PULSE_TYPES.SCREEN_LOAD,
|
|
37
|
+
[ATTRIBUTE_KEYS.PHASE]: PHASE_VALUES.START,
|
|
38
|
+
[ATTRIBUTE_KEYS.PLATFORM]: Platform.OS as 'android' | 'ios',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
console.log(`${LOG_TAGS.SCREEN_LOAD} started`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const endNavigationSpan = (
|
|
45
|
+
currentRoute?: NavigationRoute,
|
|
46
|
+
previousRoute?: NavigationRoute,
|
|
47
|
+
routeHasBeenSeen?: boolean
|
|
48
|
+
): void => {
|
|
49
|
+
if (state.navigationSpan) {
|
|
50
|
+
const route = currentRoute || state.latestRoute;
|
|
51
|
+
|
|
52
|
+
if (route) {
|
|
53
|
+
const hasBeenSeen =
|
|
54
|
+
routeHasBeenSeen !== undefined
|
|
55
|
+
? routeHasBeenSeen
|
|
56
|
+
: getRecentRouteKeys().includes(route.key);
|
|
57
|
+
|
|
58
|
+
state.navigationSpan.setAttributes({
|
|
59
|
+
[ATTRIBUTE_KEYS.SCREEN_NAME]: route.name,
|
|
60
|
+
[ATTRIBUTE_KEYS.LAST_SCREEN_NAME]: previousRoute?.name || undefined,
|
|
61
|
+
[ATTRIBUTE_KEYS.ROUTE_HAS_BEEN_SEEN]: hasBeenSeen,
|
|
62
|
+
[ATTRIBUTE_KEYS.ROUTE_KEY]: route.key,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
state.navigationSpan.end();
|
|
67
|
+
state.navigationSpan = undefined;
|
|
68
|
+
|
|
69
|
+
if (route) {
|
|
70
|
+
console.log(`${LOG_TAGS.SCREEN_LOAD} ${route.name} ended`);
|
|
71
|
+
if (onLoadEnd) {
|
|
72
|
+
onLoadEnd(route);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleStateChange = (currentRoute: NavigationRoute): void => {
|
|
79
|
+
if (!enabled || !state.navigationSpan) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const previousRoute = state.latestRoute;
|
|
84
|
+
|
|
85
|
+
if (previousRoute && previousRoute.key === currentRoute.key) {
|
|
86
|
+
const routeHasBeenSeen = getRecentRouteKeys().includes(currentRoute.key);
|
|
87
|
+
endNavigationSpan(currentRoute, previousRoute, routeHasBeenSeen);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const routeHasBeenSeen = getRecentRouteKeys().includes(currentRoute.key);
|
|
92
|
+
state.latestRoute = currentRoute;
|
|
93
|
+
pushRecentRouteKey(currentRoute.key);
|
|
94
|
+
|
|
95
|
+
endNavigationSpan(currentRoute, previousRoute, routeHasBeenSeen);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
startNavigationSpan,
|
|
100
|
+
endNavigationSpan,
|
|
101
|
+
handleStateChange,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Pulse, type Span } from '../index';
|
|
2
|
+
import { type AppStateStatus, Platform } from 'react-native';
|
|
3
|
+
import { SPAN_NAMES, ATTRIBUTE_KEYS, PULSE_TYPES } from '../pulse.constants';
|
|
4
|
+
import type {
|
|
5
|
+
NavigationRoute,
|
|
6
|
+
NavigationContainer,
|
|
7
|
+
} from './navigation.interface';
|
|
8
|
+
import { LOG_TAGS } from './utils';
|
|
9
|
+
|
|
10
|
+
export interface ScreenSessionState {
|
|
11
|
+
screenSessionSpan: Span | undefined;
|
|
12
|
+
currentScreenKey: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const INITIAL_SCREEN_SESSION_STATE: ScreenSessionState = {
|
|
16
|
+
screenSessionSpan: undefined,
|
|
17
|
+
currentScreenKey: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createScreenSessionTracker(
|
|
21
|
+
enabled: boolean,
|
|
22
|
+
state: ScreenSessionState
|
|
23
|
+
) {
|
|
24
|
+
const startScreenSession = (route: NavigationRoute): void => {
|
|
25
|
+
state.screenSessionSpan = Pulse.startSpan(SPAN_NAMES.SCREEN_SESSION, {
|
|
26
|
+
attributes: {
|
|
27
|
+
[ATTRIBUTE_KEYS.PULSE_TYPE]: PULSE_TYPES.SCREEN_SESSION,
|
|
28
|
+
[ATTRIBUTE_KEYS.SCREEN_NAME]: route.name,
|
|
29
|
+
[ATTRIBUTE_KEYS.ROUTE_KEY]: route.key,
|
|
30
|
+
[ATTRIBUTE_KEYS.PLATFORM]: Platform.OS as 'android' | 'ios',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
state.currentScreenKey = route.key;
|
|
34
|
+
console.log(`${LOG_TAGS.SCREEN_SESSION} ${route.name} started`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const endScreenSession = (routeName?: string): void => {
|
|
38
|
+
if (state.screenSessionSpan) {
|
|
39
|
+
state.screenSessionSpan.end();
|
|
40
|
+
if (routeName) {
|
|
41
|
+
console.log(`${LOG_TAGS.SCREEN_SESSION} ${routeName} ended`);
|
|
42
|
+
}
|
|
43
|
+
state.screenSessionSpan = undefined;
|
|
44
|
+
state.currentScreenKey = undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleAppStateChange = (
|
|
49
|
+
nextAppState: AppStateStatus,
|
|
50
|
+
navigationContainer: NavigationContainer | undefined
|
|
51
|
+
): void => {
|
|
52
|
+
if (!enabled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
|
57
|
+
if (state.screenSessionSpan) {
|
|
58
|
+
const currentRoute = navigationContainer?.getCurrentRoute();
|
|
59
|
+
endScreenSession(currentRoute?.name);
|
|
60
|
+
}
|
|
61
|
+
} else if (nextAppState === 'active') {
|
|
62
|
+
const currentRoute = navigationContainer?.getCurrentRoute();
|
|
63
|
+
if (currentRoute && !state.screenSessionSpan) {
|
|
64
|
+
startScreenSession(currentRoute);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const shouldStartSession = (
|
|
70
|
+
currentRoute: NavigationRoute,
|
|
71
|
+
appState: AppStateStatus
|
|
72
|
+
): boolean => {
|
|
73
|
+
return (
|
|
74
|
+
enabled &&
|
|
75
|
+
appState === 'active' &&
|
|
76
|
+
!state.screenSessionSpan &&
|
|
77
|
+
state.currentScreenKey !== currentRoute.key
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
startScreenSession,
|
|
83
|
+
endScreenSession,
|
|
84
|
+
handleAppStateChange,
|
|
85
|
+
shouldStartSession,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRef, useCallback, useEffect, useMemo, type RefObject } from 'react';
|
|
2
|
+
import type { NavigationIntegrationOptions } from './navigation.interface';
|
|
3
|
+
import type { ReactNavigationIntegration } from './index';
|
|
4
|
+
|
|
5
|
+
export function useNavigationTracking(
|
|
6
|
+
navigationRef: RefObject<any>,
|
|
7
|
+
options?: NavigationIntegrationOptions,
|
|
8
|
+
createIntegration?: (
|
|
9
|
+
options?: NavigationIntegrationOptions
|
|
10
|
+
) => ReactNavigationIntegration
|
|
11
|
+
): () => void {
|
|
12
|
+
const screenSessionTracking = options?.screenSessionTracking ?? true;
|
|
13
|
+
const screenNavigationTracking = options?.screenNavigationTracking ?? true;
|
|
14
|
+
const screenInteractiveTracking = options?.screenInteractiveTracking ?? false;
|
|
15
|
+
|
|
16
|
+
const integration = useMemo(() => {
|
|
17
|
+
if (createIntegration) {
|
|
18
|
+
return createIntegration({
|
|
19
|
+
screenSessionTracking,
|
|
20
|
+
screenNavigationTracking,
|
|
21
|
+
screenInteractiveTracking,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
throw new Error('createIntegration must be provided');
|
|
25
|
+
}, [
|
|
26
|
+
screenSessionTracking,
|
|
27
|
+
screenNavigationTracking,
|
|
28
|
+
screenInteractiveTracking,
|
|
29
|
+
createIntegration,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
return () => {
|
|
36
|
+
if (cleanupRef.current) {
|
|
37
|
+
cleanupRef.current();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const onReady = useCallback(() => {
|
|
43
|
+
if (navigationRef.current && integration) {
|
|
44
|
+
cleanupRef.current =
|
|
45
|
+
integration.registerNavigationContainer(navigationRef);
|
|
46
|
+
}
|
|
47
|
+
}, [navigationRef, integration]);
|
|
48
|
+
|
|
49
|
+
return onReady;
|
|
50
|
+
}
|