@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.
Files changed (135) hide show
  1. package/PulseReactNativeOtel.podspec +1 -1
  2. package/README.md +34 -879
  3. package/android/build.gradle +10 -15
  4. package/android/proguard-rules.pro +3 -99
  5. package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +87 -0
  6. package/android/src/main/java/com/pulsereactnativeotel/PulseOtelConstants.kt +1 -1
  7. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelLogger.kt +3 -1
  8. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +53 -3
  9. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelPackage.kt +1 -1
  10. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelTracer.kt +24 -8
  11. package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesLogRecordProcessor.kt +21 -0
  12. package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesSpanProcessor.kt +30 -0
  13. package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenNameTracker.kt +17 -0
  14. package/app.plugin.js +1 -0
  15. package/ios/PulseReactNativeOtel.mm +7 -1
  16. package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
  17. package/lib/module/config.js +29 -9
  18. package/lib/module/config.js.map +1 -1
  19. package/lib/module/errorBoundary.js.map +1 -1
  20. package/lib/module/events.js +6 -0
  21. package/lib/module/events.js.map +1 -1
  22. package/lib/module/index.js +4 -2
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/navigation/index.js +172 -0
  25. package/lib/module/navigation/index.js.map +1 -0
  26. package/lib/module/navigation/navigation.interface.js +2 -0
  27. package/lib/module/navigation/navigation.interface.js.map +1 -0
  28. package/lib/module/navigation/screen-interactive.js +101 -0
  29. package/lib/module/navigation/screen-interactive.js.map +1 -0
  30. package/lib/module/navigation/screen-load.js +68 -0
  31. package/lib/module/navigation/screen-load.js.map +1 -0
  32. package/lib/module/navigation/screen-session.js +60 -0
  33. package/lib/module/navigation/screen-session.js.map +1 -0
  34. package/lib/module/navigation/useNavigationTracking.js +33 -0
  35. package/lib/module/navigation/useNavigationTracking.js.map +1 -0
  36. package/lib/module/navigation/utils.js +17 -0
  37. package/lib/module/navigation/utils.js.map +1 -0
  38. package/lib/module/network-interceptor/graphql-helper.js +92 -0
  39. package/lib/module/network-interceptor/graphql-helper.js.map +1 -0
  40. package/lib/module/network-interceptor/request-tracker-xhr.js +2 -1
  41. package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
  42. package/lib/module/network-interceptor/span-helpers.js +24 -16
  43. package/lib/module/network-interceptor/span-helpers.js.map +1 -1
  44. package/lib/module/network-interceptor/url-helper.js +58 -2
  45. package/lib/module/network-interceptor/url-helper.js.map +1 -1
  46. package/lib/module/pulse.constants.js +42 -0
  47. package/lib/module/pulse.constants.js.map +1 -0
  48. package/lib/module/trace.js +17 -2
  49. package/lib/module/trace.js.map +1 -1
  50. package/lib/typescript/plugin/src/index.d.ts +5 -0
  51. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  52. package/lib/typescript/plugin/src/types.d.ts +27 -0
  53. package/lib/typescript/plugin/src/types.d.ts.map +1 -0
  54. package/lib/typescript/plugin/src/utils.d.ts +10 -0
  55. package/lib/typescript/plugin/src/utils.d.ts.map +1 -0
  56. package/lib/typescript/plugin/src/withAndroidPulse.d.ts +4 -0
  57. package/lib/typescript/plugin/src/withAndroidPulse.d.ts.map +1 -0
  58. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +8 -2
  59. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
  60. package/lib/typescript/src/config.d.ts +8 -2
  61. package/lib/typescript/src/config.d.ts.map +1 -1
  62. package/lib/typescript/src/errorBoundary.d.ts.map +1 -1
  63. package/lib/typescript/src/events.d.ts.map +1 -1
  64. package/lib/typescript/src/index.d.ts +5 -3
  65. package/lib/typescript/src/index.d.ts.map +1 -1
  66. package/lib/typescript/src/navigation/index.d.ts +12 -0
  67. package/lib/typescript/src/navigation/index.d.ts.map +1 -0
  68. package/lib/typescript/src/navigation/navigation.interface.d.ts +17 -0
  69. package/lib/typescript/src/navigation/navigation.interface.d.ts.map +1 -0
  70. package/lib/typescript/src/navigation/screen-interactive.d.ts +16 -0
  71. package/lib/typescript/src/navigation/screen-interactive.d.ts.map +1 -0
  72. package/lib/typescript/src/navigation/screen-load.d.ts +13 -0
  73. package/lib/typescript/src/navigation/screen-load.d.ts.map +1 -0
  74. package/lib/typescript/src/navigation/screen-session.d.ts +15 -0
  75. package/lib/typescript/src/navigation/screen-session.d.ts.map +1 -0
  76. package/lib/typescript/src/navigation/useNavigationTracking.d.ts +5 -0
  77. package/lib/typescript/src/navigation/useNavigationTracking.d.ts.map +1 -0
  78. package/lib/typescript/src/navigation/utils.d.ts +8 -0
  79. package/lib/typescript/src/navigation/utils.d.ts.map +1 -0
  80. package/lib/typescript/src/network-interceptor/graphql-helper.d.ts +8 -0
  81. package/lib/typescript/src/network-interceptor/graphql-helper.d.ts.map +1 -0
  82. package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -1
  83. package/lib/typescript/src/network-interceptor/span-helpers.d.ts +1 -1
  84. package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -1
  85. package/lib/typescript/src/network-interceptor/url-helper.d.ts +9 -0
  86. package/lib/typescript/src/network-interceptor/url-helper.d.ts.map +1 -1
  87. package/lib/typescript/src/pulse.constants.d.ts +35 -0
  88. package/lib/typescript/src/pulse.constants.d.ts.map +1 -0
  89. package/lib/typescript/src/pulse.interface.d.ts +2 -1
  90. package/lib/typescript/src/pulse.interface.d.ts.map +1 -1
  91. package/lib/typescript/src/trace.d.ts +7 -0
  92. package/lib/typescript/src/trace.d.ts.map +1 -1
  93. package/package.json +29 -9
  94. package/plugin/build/index.d.ts +4 -0
  95. package/plugin/build/index.js +10 -0
  96. package/plugin/build/types.d.ts +26 -0
  97. package/plugin/build/types.js +2 -0
  98. package/plugin/build/utils.d.ts +9 -0
  99. package/plugin/build/utils.js +102 -0
  100. package/plugin/build/withAndroidPulse.d.ts +3 -0
  101. package/plugin/build/withAndroidPulse.js +53 -0
  102. package/scripts/pulse-cli.js +82 -0
  103. package/scripts/uploadService.js +122 -0
  104. package/scripts/utils.js +125 -0
  105. package/src/NativePulseReactNativeOtel.ts +11 -2
  106. package/src/config.ts +37 -8
  107. package/src/errorBoundary.tsx +11 -5
  108. package/src/events.ts +7 -0
  109. package/src/global.d.ts +0 -1
  110. package/src/index.tsx +6 -3
  111. package/src/navigation/index.ts +306 -0
  112. package/src/navigation/navigation.interface.ts +19 -0
  113. package/src/navigation/screen-interactive.ts +149 -0
  114. package/src/navigation/screen-load.ts +103 -0
  115. package/src/navigation/screen-session.ts +87 -0
  116. package/src/navigation/useNavigationTracking.ts +50 -0
  117. package/src/navigation/utils.ts +19 -0
  118. package/src/network-interceptor/graphql-helper.ts +110 -0
  119. package/src/network-interceptor/request-tracker-xhr.ts +3 -1
  120. package/src/network-interceptor/span-helpers.ts +27 -18
  121. package/src/network-interceptor/url-helper.ts +67 -1
  122. package/src/pulse.constants.ts +38 -0
  123. package/src/pulse.interface.ts +6 -1
  124. package/src/trace.ts +25 -2
  125. package/LICENSE +0 -20
  126. package/lib/module/network-interceptor/request-tracker-fetch.js +0 -72
  127. package/lib/module/network-interceptor/request-tracker-fetch.js.map +0 -1
  128. package/lib/module/reactNavigation.js +0 -100
  129. package/lib/module/reactNavigation.js.map +0 -1
  130. package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts +0 -7
  131. package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts.map +0 -1
  132. package/lib/typescript/src/reactNavigation.d.ts +0 -10
  133. package/lib/typescript/src/reactNavigation.d.ts.map +0 -1
  134. package/src/network-interceptor/request-tracker-fetch.ts +0 -96
  135. 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
+ }