@dreamhorizonorg/pulse-react-native 0.0.1 → 0.0.3

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