@dreamhorizonorg/pulse-react-native 0.0.3 → 0.0.4

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 (68) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +4 -0
  3. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +5 -0
  4. package/ios/PulseReactNativeOtel.mm +124 -63
  5. package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
  6. package/lib/module/config.js +46 -4
  7. package/lib/module/config.js.map +1 -1
  8. package/lib/module/errorBoundary.js +4 -1
  9. package/lib/module/errorBoundary.js.map +1 -1
  10. package/lib/module/errorHandler.js +5 -1
  11. package/lib/module/errorHandler.js.map +1 -1
  12. package/lib/module/events.js +2 -2
  13. package/lib/module/events.js.map +1 -1
  14. package/lib/module/index.js +2 -1
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/navigation/index.js +11 -0
  17. package/lib/module/navigation/index.js.map +1 -1
  18. package/lib/module/navigation/screen-interactive.js +4 -0
  19. package/lib/module/navigation/screen-interactive.js.map +1 -1
  20. package/lib/module/network-interceptor/initialization.js +13 -1
  21. package/lib/module/network-interceptor/initialization.js.map +1 -1
  22. package/lib/module/network-interceptor/request-tracker-xhr.js +15 -2
  23. package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
  24. package/lib/module/trace.js +16 -10
  25. package/lib/module/trace.js.map +1 -1
  26. package/lib/module/user.js +4 -3
  27. package/lib/module/user.js.map +1 -1
  28. package/lib/typescript/plugin/src/types.d.ts +6 -0
  29. package/lib/typescript/plugin/src/types.d.ts.map +1 -1
  30. package/lib/typescript/plugin/src/utils.d.ts +2 -0
  31. package/lib/typescript/plugin/src/utils.d.ts.map +1 -1
  32. package/lib/typescript/plugin/src/withAndroidPulse.d.ts.map +1 -1
  33. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +2 -0
  34. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
  35. package/lib/typescript/src/config.d.ts +5 -1
  36. package/lib/typescript/src/config.d.ts.map +1 -1
  37. package/lib/typescript/src/errorBoundary.d.ts.map +1 -1
  38. package/lib/typescript/src/errorHandler.d.ts +1 -0
  39. package/lib/typescript/src/errorHandler.d.ts.map +1 -1
  40. package/lib/typescript/src/events.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +2 -1
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/navigation/index.d.ts +1 -0
  44. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  45. package/lib/typescript/src/navigation/screen-interactive.d.ts.map +1 -1
  46. package/lib/typescript/src/network-interceptor/initialization.d.ts +1 -0
  47. package/lib/typescript/src/network-interceptor/initialization.d.ts.map +1 -1
  48. package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts +5 -1
  49. package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -1
  50. package/lib/typescript/src/trace.d.ts.map +1 -1
  51. package/lib/typescript/src/user.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/plugin/build/types.d.ts +6 -0
  54. package/plugin/build/utils.d.ts +2 -0
  55. package/plugin/build/utils.js +6 -2
  56. package/plugin/build/withAndroidPulse.js +3 -1
  57. package/src/NativePulseReactNativeOtel.ts +3 -0
  58. package/src/config.ts +53 -3
  59. package/src/errorBoundary.tsx +4 -1
  60. package/src/errorHandler.ts +6 -1
  61. package/src/events.ts +6 -2
  62. package/src/index.tsx +2 -1
  63. package/src/navigation/index.ts +13 -0
  64. package/src/navigation/screen-interactive.ts +4 -0
  65. package/src/network-interceptor/initialization.ts +14 -1
  66. package/src/network-interceptor/request-tracker-xhr.ts +16 -3
  67. package/src/trace.ts +17 -10
  68. package/src/user.ts +4 -3
package/src/config.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { setupErrorHandler } from './errorHandler';
1
+ import { setupErrorHandler, uninstallErrorHandler } from './errorHandler';
2
2
  import { isSupportedPlatform } from './initialization';
3
3
  import {
4
4
  createReactNavigationIntegration,
5
+ uninstallNavigationIntegration,
5
6
  type ReactNavigationIntegration,
6
7
  type NavigationIntegrationOptions,
7
8
  } from './navigation';
8
- import { initializeNetworkInterceptor } from './network-interceptor/initialization';
9
+ import {
10
+ initializeNetworkInterceptor,
11
+ uninstallNetworkInterceptor,
12
+ } from './network-interceptor/initialization';
9
13
  import PulseReactNativeOtel from './NativePulseReactNativeOtel';
10
14
  import type { PulseFeatureConfig } from './pulse.interface';
11
15
  import { PULSE_FEATURE_NAMES } from './pulse.constants';
@@ -34,12 +38,27 @@ const defaultConfig: Required<PulseConfig> = {
34
38
 
35
39
  let currentConfig: PulseConfig = { ...defaultConfig };
36
40
 
41
+ /** After shutdown, start() and initialize are no-ops; re-initialization is not supported. */
42
+ let isShutdown = false;
43
+
44
+ /** True only after start() has been called at least once. Integrations (e.g. navigation) are no-ops until then. */
45
+ let isStarted = false;
46
+
37
47
  // Cache for features from remote SDK config
38
48
  let cachedFeatures: PulseFeatureConfig;
39
49
 
50
+ export function getIsShutdown(): boolean {
51
+ return isShutdown;
52
+ }
53
+
54
+ /** True only after start() has been called at least once. Public APIs (trackEvent, reportException, startSpan, etc.) no-op until then. */
55
+ export function getIsStarted(): boolean {
56
+ return isStarted;
57
+ }
58
+
40
59
  /**
41
60
  * Gets all features from the remote SDK config.
42
- * @returns Record of feature names to their enabled status, or null if config not available
61
+ * @returns Record of feature names to their enabled status, or null if config not available or start() not called
43
62
  */
44
63
  export function getFeaturesFromRemoteConfig(): PulseFeatureConfig {
45
64
  if (cachedFeatures !== undefined) {
@@ -93,7 +112,14 @@ function resolveNavigationState(
93
112
 
94
113
  export function start(options?: PulseConfig): void {
95
114
  if (!isSupportedPlatform()) return;
115
+ if (isShutdown) {
116
+ console.log(
117
+ '[Pulse] SDK has been shut down. Pulse.start() is a no-op; re-initialization is not supported.'
118
+ );
119
+ return;
120
+ }
96
121
 
122
+ isStarted = true;
97
123
  const features = getFeaturesFromRemoteConfig();
98
124
  const config: PulseConfig = {
99
125
  autoDetectExceptions: resolveFeatureState(
@@ -119,6 +145,18 @@ export function start(options?: PulseConfig): void {
119
145
  configure(config);
120
146
  }
121
147
 
148
+ export function shutdown(): void {
149
+ if (isShutdown) {
150
+ console.warn('[Pulse] SDK already shut down.');
151
+ return;
152
+ }
153
+ uninstallErrorHandler();
154
+ uninstallNetworkInterceptor();
155
+ uninstallNavigationIntegration();
156
+ PulseReactNativeOtel.shutdown();
157
+ isShutdown = true;
158
+ }
159
+
122
160
  export function createNavigationIntegrationWithConfig(
123
161
  options?: NavigationIntegrationOptions
124
162
  ): ReactNavigationIntegration {
@@ -128,6 +166,18 @@ export function createNavigationIntegrationWithConfig(
128
166
  markContentReady: () => {},
129
167
  };
130
168
  }
169
+ if (!isStarted) {
170
+ return {
171
+ registerNavigationContainer: (_: unknown) => () => {},
172
+ markContentReady: () => {},
173
+ };
174
+ }
175
+ if (isShutdown) {
176
+ return {
177
+ registerNavigationContainer: (_: unknown) => () => {},
178
+ markContentReady: () => {},
179
+ };
180
+ }
131
181
  if (!currentConfig.autoDetectNavigation) {
132
182
  console.warn(
133
183
  '[Pulse Navigation] auto-detection disabled via Pulse.start; createNavigationIntegration() returning no-op.'
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { Pulse } from './index';
3
+ import { getIsShutdown } from './config';
3
4
 
4
5
  export const UNKNOWN_COMPONENT = 'unknown';
5
6
  const COMPONENT_STACK_UNAVAILABLE = '<component stack unavailable>';
@@ -46,7 +47,9 @@ export class ErrorBoundary extends React.Component<
46
47
 
47
48
  const errorToReport =
48
49
  error instanceof Error ? error : new Error(String(error));
49
- Pulse.reportException(errorToReport, !handled);
50
+ if (!getIsShutdown()) {
51
+ Pulse.reportException(errorToReport, !handled);
52
+ }
50
53
 
51
54
  if (onError) {
52
55
  onError(error, componentStack);
@@ -1,4 +1,5 @@
1
1
  import PulseReactNativeOtel from './NativePulseReactNativeOtel';
2
+ import { getIsShutdown, getIsStarted } from './config';
2
3
  import { mergeWithGlobalAttributes } from './globalAttributes';
3
4
  import { isSupportedPlatform } from './initialization';
4
5
  import { extractErrorDetails } from './utility';
@@ -68,7 +69,7 @@ export function reportException(
68
69
  isFatal: boolean = false,
69
70
  attributes?: PulseAttributes
70
71
  ): void {
71
- if (!isSupportedPlatform()) {
72
+ if (!isSupportedPlatform() || !getIsStarted() || getIsShutdown()) {
72
73
  return;
73
74
  }
74
75
 
@@ -160,6 +161,10 @@ function disableErrorHandler(): void {
160
161
  handlingFatal = false;
161
162
  }
162
163
 
164
+ export function uninstallErrorHandler(): void {
165
+ disableErrorHandler();
166
+ }
167
+
163
168
  export function setupErrorHandler(enableErrorHandler: boolean): void {
164
169
  if (enableErrorHandler) {
165
170
  initializeErrorHandler();
package/src/events.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import PulseReactNativeOtel from './NativePulseReactNativeOtel';
2
- import { getFeaturesFromRemoteConfig } from './config';
2
+ import {
3
+ getFeaturesFromRemoteConfig,
4
+ getIsShutdown,
5
+ getIsStarted,
6
+ } from './config';
3
7
  import { mergeWithGlobalAttributes } from './globalAttributes';
4
8
  import { isSupportedPlatform } from './initialization';
5
9
  import type { PulseAttributes } from './pulse.interface';
6
10
 
7
11
  export function trackEvent(event: string, attributes?: PulseAttributes): void {
8
- if (!isSupportedPlatform()) {
12
+ if (!isSupportedPlatform() || !getIsStarted() || getIsShutdown()) {
9
13
  return;
10
14
  }
11
15
  const features = getFeaturesFromRemoteConfig();
package/src/index.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { startSpan, trackSpan } from './trace';
2
2
  import { reportException } from './errorHandler';
3
3
  import { trackEvent } from './events';
4
- import { start } from './config';
4
+ import { start, shutdown } from './config';
5
5
  import { isInitialized } from './initialization';
6
6
  import { setGlobalAttribute } from './globalAttributes';
7
7
  import { setUserId, setUserProperty, setUserProperties } from './user';
@@ -22,6 +22,7 @@ export type { ErrorBoundaryProps, FallbackRender } from './errorBoundary';
22
22
  export { SpanStatusCode } from './trace';
23
23
  export const Pulse = {
24
24
  start,
25
+ shutdown,
25
26
  isInitialized,
26
27
  useNavigationTracking,
27
28
  markContentReady,
@@ -37,6 +37,15 @@ import {
37
37
  export type { NavigationRoute, NavigationIntegrationOptions };
38
38
  export { DEFAULT_NAVIGATION_OPTIONS } from './navigation.interface';
39
39
 
40
+ let currentNavigationUnregister: (() => void) | null = null;
41
+
42
+ export function uninstallNavigationIntegration(): void {
43
+ if (currentNavigationUnregister) {
44
+ currentNavigationUnregister();
45
+ currentNavigationUnregister = null;
46
+ }
47
+ }
48
+
40
49
  export interface ReactNavigationIntegration {
41
50
  registerNavigationContainer: (
42
51
  maybeNavigationContainer: unknown
@@ -270,6 +279,9 @@ export function createReactNavigationIntegration(
270
279
  }
271
280
  navigationContainer = undefined;
272
281
  isInitialized = false;
282
+ if (currentNavigationUnregister === unmountCleanup) {
283
+ currentNavigationUnregister = null;
284
+ }
273
285
 
274
286
  clearGlobalMarkContentReady(
275
287
  updatedInteractiveTracker.markContentReady
@@ -304,6 +316,7 @@ export function createReactNavigationIntegration(
304
316
  );
305
317
  isInitialized = true;
306
318
 
319
+ currentNavigationUnregister = unmountCleanup;
307
320
  return unmountCleanup;
308
321
  } catch (error) {
309
322
  console.error(
@@ -1,4 +1,5 @@
1
1
  import { Pulse, type Span } from '../index';
2
+ import { getIsStarted } from '../config';
2
3
  import { Platform } from 'react-native';
3
4
  import { SPAN_NAMES, ATTRIBUTE_KEYS, PULSE_TYPES } from '../pulse.constants';
4
5
  import { discardSpan } from '../trace';
@@ -131,6 +132,9 @@ export function createScreenInteractiveTracker(
131
132
  }
132
133
 
133
134
  export function markContentReady(): void {
135
+ if (!getIsStarted()) {
136
+ return;
137
+ }
134
138
  if (globalMarkContentReady) {
135
139
  globalMarkContentReady();
136
140
  } else {
@@ -4,6 +4,7 @@ import type { NetworkHeaderConfig } from '../config';
4
4
  export { normalizeHeaderName, shouldCaptureHeader } from './header-helper';
5
5
 
6
6
  let isInitialized = false;
7
+ let uninstallXmlHttpRequestTracker: (() => void) | null = null;
7
8
  let headerConfig: NetworkHeaderConfig = {
8
9
  requestHeaders: [],
9
10
  responseHeaders: [],
@@ -35,7 +36,8 @@ export function initializeNetworkInterceptor(
35
36
  // In react-native, we are intercepting XMLHttpRequest only, since axios and fetch both use it internally.
36
37
  // See: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Network/fetch.js
37
38
  if (typeof XMLHttpRequest !== 'undefined') {
38
- createXmlHttpRequestTracker(XMLHttpRequest);
39
+ const result = createXmlHttpRequestTracker(XMLHttpRequest);
40
+ uninstallXmlHttpRequestTracker = result.uninstall;
39
41
  } else {
40
42
  console.warn('[Pulse] XMLHttpRequest is not available');
41
43
  }
@@ -47,3 +49,14 @@ export function initializeNetworkInterceptor(
47
49
  }
48
50
 
49
51
  export const isNetworkInterceptorInitialized = (): boolean => isInitialized;
52
+
53
+ export function uninstallNetworkInterceptor(): void {
54
+ if (!isInitialized) {
55
+ return;
56
+ }
57
+ if (uninstallXmlHttpRequestTracker) {
58
+ uninstallXmlHttpRequestTracker();
59
+ uninstallXmlHttpRequestTracker = null;
60
+ }
61
+ isInitialized = false;
62
+ }
@@ -18,12 +18,17 @@ type ReadyStateChangeHandler = (this: XMLHttpRequest, ev: Event) => any;
18
18
 
19
19
  let isXHRIntercepted = false;
20
20
 
21
+ export interface XmlHttpRequestTrackerResult {
22
+ requestTracker: RequestTracker;
23
+ uninstall: () => void;
24
+ }
25
+
21
26
  function createXmlHttpRequestTracker(
22
27
  xhr: typeof XMLHttpRequest
23
- ): RequestTracker {
28
+ ): XmlHttpRequestTrackerResult {
24
29
  if (isXHRIntercepted) {
25
30
  console.warn('[Pulse] XMLHttpRequest already intercepted');
26
- return new RequestTracker();
31
+ return { requestTracker: new RequestTracker(), uninstall: () => {} };
27
32
  }
28
33
 
29
34
  const requestTracker = new RequestTracker();
@@ -192,7 +197,15 @@ function createXmlHttpRequestTracker(
192
197
  originalSend.call(this, body);
193
198
  };
194
199
 
195
- return requestTracker;
200
+ const uninstall = (): void => {
201
+ if (!isXHRIntercepted) return;
202
+ xhr.prototype.open = originalOpen;
203
+ xhr.prototype.setRequestHeader = originalSetRequestHeader;
204
+ xhr.prototype.send = originalSend;
205
+ isXHRIntercepted = false;
206
+ };
207
+
208
+ return { requestTracker, uninstall };
196
209
  }
197
210
 
198
211
  export default createXmlHttpRequestTracker;
package/src/trace.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import PulseReactNativeOtel from './NativePulseReactNativeOtel';
2
+ import { getIsShutdown, getIsStarted } from './config';
2
3
  import { isSupportedPlatform } from './initialization';
3
4
  import { mergeWithGlobalAttributes } from './globalAttributes';
4
5
  import { extractErrorDetails } from './utility';
5
6
  import type { PulseAttributes } from './pulse.interface';
6
7
 
8
+ const noopSpan: Span = {
9
+ end: (_statusCode?: SpanStatusCode) => {},
10
+ addEvent: (_eventName: string, _eventAttributes?: PulseAttributes) => {},
11
+ setAttributes: (_attributes?: PulseAttributes) => {},
12
+ recordException: (_error: Error, _attributes?: PulseAttributes) => {},
13
+ spanId: undefined,
14
+ };
15
+
7
16
  /**
8
17
  * Options for starting a span.
9
18
  * @param attributes - Attributes to set on the span.
@@ -30,14 +39,8 @@ export type Span = {
30
39
  };
31
40
 
32
41
  export function startSpan(name: string, options?: SpanOptions): Span {
33
- if (!isSupportedPlatform()) {
34
- return {
35
- end: (_statusCode?: SpanStatusCode) => {},
36
- addEvent: (_eventName: string, _eventAttributes?: PulseAttributes) => {},
37
- setAttributes: (_attributes?: PulseAttributes) => {},
38
- recordException: (_error: Error, _attributes?: PulseAttributes) => {},
39
- spanId: undefined,
40
- };
42
+ if (!isSupportedPlatform() || !getIsStarted() || getIsShutdown()) {
43
+ return noopSpan;
41
44
  }
42
45
 
43
46
  const mergedAttributes = mergeWithGlobalAttributes(options?.attributes || {});
@@ -69,7 +72,7 @@ export function trackSpan<T>(
69
72
  options: SpanOptions,
70
73
  fn: () => T | Promise<T>
71
74
  ): T | Promise<T> {
72
- if (!isSupportedPlatform()) {
75
+ if (!isSupportedPlatform() || !getIsStarted() || getIsShutdown()) {
73
76
  return fn();
74
77
  }
75
78
 
@@ -94,11 +97,12 @@ export function trackSpan<T>(
94
97
  }
95
98
 
96
99
  function endSpan(spanId: string, statusCode?: SpanStatusCode): void {
100
+ if (getIsShutdown()) return;
97
101
  PulseReactNativeOtel.endSpan(spanId, statusCode);
98
102
  }
99
103
 
100
104
  export function discardSpan(spanId: string): void {
101
- if (!isSupportedPlatform()) {
105
+ if (!isSupportedPlatform() || !getIsStarted() || getIsShutdown()) {
102
106
  return;
103
107
  }
104
108
  PulseReactNativeOtel.discardSpan(spanId);
@@ -109,14 +113,17 @@ function addSpanEvent(
109
113
  name: string,
110
114
  attributes?: PulseAttributes
111
115
  ): void {
116
+ if (getIsShutdown()) return;
112
117
  PulseReactNativeOtel.addSpanEvent(spanId, name, attributes || undefined);
113
118
  }
114
119
 
115
120
  function setSpanAttributes(spanId: string, attributes?: PulseAttributes): void {
121
+ if (getIsShutdown()) return;
116
122
  PulseReactNativeOtel.setSpanAttributes(spanId, attributes || undefined);
117
123
  }
118
124
 
119
125
  function recordSpanException(error: Error, attributes?: PulseAttributes): void {
126
+ if (getIsShutdown()) return;
120
127
  const { message, stackTrace, errorType } = extractErrorDetails(error);
121
128
  const observedTimeMs = Date.now();
122
129
  const mergedAttributes = mergeWithGlobalAttributes(attributes || {});
package/src/user.ts CHANGED
@@ -1,23 +1,24 @@
1
1
  import PulseReactNativeOtel from './NativePulseReactNativeOtel';
2
+ import { getIsShutdown } from './config';
2
3
  import { isSupportedPlatform } from './initialization';
3
4
  import type { PulseAttributes } from './pulse.interface';
4
5
 
5
6
  export function setUserId(id: string | null): void {
6
- if (!isSupportedPlatform()) {
7
+ if (!isSupportedPlatform() || getIsShutdown()) {
7
8
  return;
8
9
  }
9
10
  PulseReactNativeOtel.setUserId(id);
10
11
  }
11
12
 
12
13
  export function setUserProperty(name: string, value: string | null): void {
13
- if (!isSupportedPlatform()) {
14
+ if (!isSupportedPlatform() || getIsShutdown()) {
14
15
  return;
15
16
  }
16
17
  PulseReactNativeOtel.setUserProperty(name, value);
17
18
  }
18
19
 
19
20
  export function setUserProperties(properties: PulseAttributes): void {
20
- if (!isSupportedPlatform()) {
21
+ if (!isSupportedPlatform() || getIsShutdown()) {
21
22
  return;
22
23
  }
23
24
  PulseReactNativeOtel.setUserProperties(properties);