@dreamhorizonorg/pulse-react-native 0.0.2 → 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 (64) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +2 -0
  3. package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +40 -24
  4. package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
  5. package/lib/module/config.js +36 -18
  6. package/lib/module/config.js.map +1 -1
  7. package/lib/module/navigation/index.js +10 -3
  8. package/lib/module/navigation/index.js.map +1 -1
  9. package/lib/module/navigation/navigation.interface.js +6 -0
  10. package/lib/module/navigation/navigation.interface.js.map +1 -1
  11. package/lib/module/navigation/screen-load.js +1 -2
  12. package/lib/module/navigation/screen-load.js.map +1 -1
  13. package/lib/module/navigation/useNavigationTracking.js +4 -3
  14. package/lib/module/navigation/useNavigationTracking.js.map +1 -1
  15. package/lib/module/network-interceptor/header-helper.js +24 -0
  16. package/lib/module/network-interceptor/header-helper.js.map +1 -0
  17. package/lib/module/network-interceptor/initialization.js +18 -1
  18. package/lib/module/network-interceptor/initialization.js.map +1 -1
  19. package/lib/module/network-interceptor/request-tracker-xhr.js +59 -3
  20. package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
  21. package/lib/module/network-interceptor/span-helpers.js +15 -3
  22. package/lib/module/network-interceptor/span-helpers.js.map +1 -1
  23. package/lib/module/pulse.constants.js +11 -6
  24. package/lib/module/pulse.constants.js.map +1 -1
  25. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +8 -1
  26. package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
  27. package/lib/typescript/src/config.d.ts +7 -7
  28. package/lib/typescript/src/config.d.ts.map +1 -1
  29. package/lib/typescript/src/index.d.ts +1 -1
  30. package/lib/typescript/src/index.d.ts.map +1 -1
  31. package/lib/typescript/src/navigation/index.d.ts +1 -0
  32. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  33. package/lib/typescript/src/navigation/navigation.interface.d.ts +1 -0
  34. package/lib/typescript/src/navigation/navigation.interface.d.ts.map +1 -1
  35. package/lib/typescript/src/navigation/screen-load.d.ts.map +1 -1
  36. package/lib/typescript/src/navigation/useNavigationTracking.d.ts +1 -1
  37. package/lib/typescript/src/navigation/useNavigationTracking.d.ts.map +1 -1
  38. package/lib/typescript/src/network-interceptor/header-helper.d.ts +15 -0
  39. package/lib/typescript/src/network-interceptor/header-helper.d.ts.map +1 -0
  40. package/lib/typescript/src/network-interceptor/initialization.d.ts +4 -1
  41. package/lib/typescript/src/network-interceptor/initialization.d.ts.map +1 -1
  42. package/lib/typescript/src/network-interceptor/network.interface.d.ts +3 -0
  43. package/lib/typescript/src/network-interceptor/network.interface.d.ts.map +1 -1
  44. package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -1
  45. package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -1
  46. package/lib/typescript/src/pulse.constants.d.ts +13 -5
  47. package/lib/typescript/src/pulse.constants.d.ts.map +1 -1
  48. package/lib/typescript/src/pulse.interface.d.ts +1 -1
  49. package/lib/typescript/src/pulse.interface.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/NativePulseReactNativeOtel.ts +8 -1
  52. package/src/config.ts +68 -26
  53. package/src/index.tsx +1 -1
  54. package/src/navigation/index.ts +36 -7
  55. package/src/navigation/navigation.interface.ts +7 -0
  56. package/src/navigation/screen-load.ts +1 -7
  57. package/src/navigation/useNavigationTracking.ts +13 -4
  58. package/src/network-interceptor/header-helper.ts +26 -0
  59. package/src/network-interceptor/initialization.ts +22 -1
  60. package/src/network-interceptor/network.interface.ts +3 -0
  61. package/src/network-interceptor/request-tracker-xhr.ts +90 -2
  62. package/src/network-interceptor/span-helpers.ts +23 -3
  63. package/src/pulse.constants.ts +19 -5
  64. package/src/pulse.interface.ts +1 -1
@@ -1,5 +1,8 @@
1
1
  import { useRef, useCallback, useEffect, useMemo, type RefObject } from 'react';
2
- import type { NavigationIntegrationOptions } from './navigation.interface';
2
+ import {
3
+ DEFAULT_NAVIGATION_OPTIONS,
4
+ type NavigationIntegrationOptions,
5
+ } from './navigation.interface';
3
6
  import type { ReactNavigationIntegration } from './index';
4
7
 
5
8
  export function useNavigationTracking(
@@ -9,9 +12,15 @@ export function useNavigationTracking(
9
12
  options?: NavigationIntegrationOptions
10
13
  ) => ReactNavigationIntegration
11
14
  ): () => void {
12
- const screenSessionTracking = options?.screenSessionTracking ?? true;
13
- const screenNavigationTracking = options?.screenNavigationTracking ?? true;
14
- const screenInteractiveTracking = options?.screenInteractiveTracking ?? false;
15
+ const screenSessionTracking =
16
+ options?.screenSessionTracking ??
17
+ DEFAULT_NAVIGATION_OPTIONS.screenSessionTracking;
18
+ const screenNavigationTracking =
19
+ options?.screenNavigationTracking ??
20
+ DEFAULT_NAVIGATION_OPTIONS.screenNavigationTracking;
21
+ const screenInteractiveTracking =
22
+ options?.screenInteractiveTracking ??
23
+ DEFAULT_NAVIGATION_OPTIONS.screenInteractiveTracking;
15
24
 
16
25
  const integration = useMemo(() => {
17
26
  if (createIntegration) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Normalizes header name according to OpenTelemetry HTTP semantic conventions:
3
+ * - Lowercase
4
+ * - Dashes replaced by underscores
5
+ * - Reference: https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/
6
+ * @example
7
+ * normalizeHeaderName('Content-Type') => 'content_type'
8
+ * normalizeHeaderName('X-Request-ID') => 'x_request_id'
9
+ */
10
+ export function normalizeHeaderName(headerName: string): string {
11
+ return headerName.toLowerCase().replace(/-/g, '_');
12
+ }
13
+
14
+ /**
15
+ * Checks if a header should be captured based on configuration.
16
+ */
17
+ export function shouldCaptureHeader(
18
+ headerName: string,
19
+ headerList: string[]
20
+ ): boolean {
21
+ if (headerList.length === 0) return false;
22
+ // Case-insensitive comparison
23
+ return headerList.some(
24
+ (configHeader) => configHeader.toLowerCase() === headerName.toLowerCase()
25
+ );
26
+ }
@@ -1,13 +1,34 @@
1
1
  import createXmlHttpRequestTracker from './request-tracker-xhr';
2
+ import type { NetworkHeaderConfig } from '../config';
3
+ // Re-export header utilities for convenience (they're in a separate file to avoid dependency issues)
4
+ export { normalizeHeaderName, shouldCaptureHeader } from './header-helper';
2
5
 
3
6
  let isInitialized = false;
7
+ let headerConfig: NetworkHeaderConfig = {
8
+ requestHeaders: [],
9
+ responseHeaders: [],
10
+ };
4
11
 
5
- export function initializeNetworkInterceptor(): void {
12
+ export function getHeaderConfig(): NetworkHeaderConfig {
13
+ return headerConfig;
14
+ }
15
+
16
+ export function initializeNetworkInterceptor(
17
+ config?: NetworkHeaderConfig
18
+ ): void {
6
19
  if (isInitialized) {
7
20
  console.warn('[Pulse] Network interceptor already initialized');
8
21
  return;
9
22
  }
10
23
 
24
+ // Store header configuration
25
+ if (config) {
26
+ headerConfig = {
27
+ requestHeaders: config.requestHeaders ?? [],
28
+ responseHeaders: config.responseHeaders ?? [],
29
+ };
30
+ }
31
+
11
32
  console.log('[Pulse] 🔄 Starting network interceptor initialization...');
12
33
 
13
34
  try {
@@ -3,17 +3,20 @@ export interface RequestStartContext {
3
3
  method: string;
4
4
  type: 'fetch' | 'xmlhttprequest';
5
5
  baseUrl?: string;
6
+ requestHeaders?: Record<string, string>;
6
7
  }
7
8
 
8
9
  export interface RequestEndContextSuccess {
9
10
  status: number;
10
11
  state: 'success';
12
+ responseHeaders?: Record<string, string>;
11
13
  }
12
14
 
13
15
  export interface RequestEndContextError {
14
16
  state: 'error';
15
17
  status?: number;
16
18
  error?: Error;
19
+ responseHeaders?: Record<string, string>;
17
20
  }
18
21
 
19
22
  export type RequestEndContext =
@@ -6,6 +6,8 @@ import { RequestTracker } from './request-tracker';
6
6
  import { getAbsoluteUrl } from '../utility';
7
7
  import type { Span } from '../index';
8
8
  import { createNetworkSpan, completeNetworkSpan } from './span-helpers';
9
+ import { getHeaderConfig } from './initialization';
10
+ import { shouldCaptureHeader } from './header-helper';
9
11
 
10
12
  interface RequestData {
11
13
  method: string;
@@ -48,6 +50,28 @@ function createXmlHttpRequestTracker(
48
50
  };
49
51
  isXHRIntercepted = true;
50
52
 
53
+ // Store request headers before send
54
+ const requestHeadersMap = new WeakMap<
55
+ XMLHttpRequest,
56
+ Record<string, string>
57
+ >();
58
+ const originalSetRequestHeader = xhr.prototype.setRequestHeader;
59
+ xhr.prototype.setRequestHeader = function setRequestHeader(
60
+ name: string,
61
+ value: string
62
+ ): void {
63
+ const headerConfig = getHeaderConfig();
64
+ const requestHeadersList = headerConfig.requestHeaders ?? [];
65
+ if (
66
+ requestHeadersList.length > 0 &&
67
+ shouldCaptureHeader(name, requestHeadersList)
68
+ ) {
69
+ const existing = requestHeadersMap.get(this) || {};
70
+ requestHeadersMap.set(this, { ...existing, [name]: value });
71
+ }
72
+ originalSetRequestHeader.call(this, name, value);
73
+ };
74
+
51
75
  const originalSend = xhr.prototype.send;
52
76
  xhr.prototype.send = function send(
53
77
  body?: Document | XMLHttpRequestBodyInit | null
@@ -58,10 +82,28 @@ function createXmlHttpRequestTracker(
58
82
  if (existingHandler)
59
83
  this.removeEventListener('readystatechange', existingHandler);
60
84
 
85
+ // Capture request headers
86
+ const headerConfig = getHeaderConfig();
87
+ const capturedRequestHeaders = requestHeadersMap.get(this);
88
+ const requestHeadersList = headerConfig.requestHeaders ?? [];
89
+ const filteredRequestHeaders: Record<string, string> | undefined =
90
+ capturedRequestHeaders && requestHeadersList.length > 0
91
+ ? Object.fromEntries(
92
+ Object.entries(capturedRequestHeaders).filter(([name]) =>
93
+ shouldCaptureHeader(name, requestHeadersList)
94
+ )
95
+ )
96
+ : undefined;
97
+
61
98
  const startContext: RequestStartContext = {
62
99
  type: 'xmlhttprequest',
63
100
  method: requestData.method,
64
101
  url: requestData.url,
102
+ requestHeaders:
103
+ filteredRequestHeaders &&
104
+ Object.keys(filteredRequestHeaders).length > 0
105
+ ? filteredRequestHeaders
106
+ : undefined,
65
107
  };
66
108
 
67
109
  this.setRequestHeader('X-Pulse-RN-Tracked', 'true');
@@ -74,13 +116,56 @@ function createXmlHttpRequestTracker(
74
116
  if (this.readyState === xhr.DONE && onRequestEnd) {
75
117
  const activeSpan = trackedSpans.get(this);
76
118
 
119
+ // Capture response headers
120
+ const responseHeaderConfig = getHeaderConfig();
121
+ const capturedResponseHeaders: Record<string, string> = {};
122
+ const responseHeadersList =
123
+ responseHeaderConfig.responseHeaders ?? [];
124
+ if (responseHeadersList.length > 0) {
125
+ try {
126
+ const allHeaders = this.getAllResponseHeaders();
127
+ if (allHeaders) {
128
+ const headerLines = allHeaders.trim().split(/[\r\n]+/);
129
+ for (const line of headerLines) {
130
+ const parts = line.split(': ');
131
+ if (parts.length === 2) {
132
+ const [name, value] = parts;
133
+ if (
134
+ name &&
135
+ value &&
136
+ shouldCaptureHeader(name, responseHeadersList)
137
+ ) {
138
+ capturedResponseHeaders[name] = value;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ } catch (e) {
144
+ // Headers may not be available in some cases (CORS, etc.)
145
+ console.debug('[Pulse] Could not read response headers:', e);
146
+ }
147
+ }
148
+
77
149
  // Determine request outcome based on status code
78
150
  let endContext: RequestEndContext;
79
151
 
152
+ const responseHeaders =
153
+ Object.keys(capturedResponseHeaders).length > 0
154
+ ? capturedResponseHeaders
155
+ : undefined;
156
+
80
157
  if (this.status <= 0 || this.status >= 400) {
81
- endContext = { state: 'error', status: this.status };
158
+ endContext = {
159
+ state: 'error',
160
+ status: this.status,
161
+ responseHeaders,
162
+ };
82
163
  } else {
83
- endContext = { state: 'success', status: this.status };
164
+ endContext = {
165
+ state: 'success',
166
+ status: this.status,
167
+ responseHeaders,
168
+ };
84
169
  }
85
170
 
86
171
  if (activeSpan) {
@@ -93,6 +178,9 @@ function createXmlHttpRequestTracker(
93
178
  trackedSpans.delete(this);
94
179
  }
95
180
 
181
+ // Clean up
182
+ requestHeadersMap.delete(this);
183
+
96
184
  onRequestEnd(endContext);
97
185
  }
98
186
  };
@@ -8,7 +8,8 @@ import { Pulse, SpanStatusCode } from '../index';
8
8
  import type { PulseAttributes } from '../pulse.interface';
9
9
  import { extractHttpAttributes } from './url-helper';
10
10
  import { updateAttributesWithGraphQLData } from './graphql-helper';
11
- import { ATTRIBUTE_KEYS, PHASE_VALUES } from '../pulse.constants';
11
+ import { ATTRIBUTE_KEYS, PULSE_TYPES } from '../pulse.constants';
12
+ import { normalizeHeaderName } from './header-helper';
12
13
 
13
14
  export function setNetworkSpanAttributes(
14
15
  span: Span,
@@ -19,7 +20,7 @@ export function setNetworkSpanAttributes(
19
20
  let attributes: PulseAttributes = {
20
21
  [ATTRIBUTE_KEYS.HTTP_METHOD]: method,
21
22
  [ATTRIBUTE_KEYS.HTTP_URL]: startContext.url,
22
- [ATTRIBUTE_KEYS.PULSE_TYPE]: `network.${endContext.status ?? 0}`,
23
+ [ATTRIBUTE_KEYS.PULSE_TYPE]: `${PULSE_TYPES.NETWORK}.${endContext.status ?? 0}`,
23
24
  [ATTRIBUTE_KEYS.HTTP_REQUEST_TYPE]: startContext.type,
24
25
  [ATTRIBUTE_KEYS.PLATFORM]: Platform.OS,
25
26
  };
@@ -43,6 +44,26 @@ export function setNetworkSpanAttributes(
43
44
  span.recordException(endContext.error, attributes);
44
45
  }
45
46
 
47
+ if (startContext.requestHeaders) {
48
+ for (const [headerName, headerValue] of Object.entries(
49
+ startContext.requestHeaders
50
+ )) {
51
+ const normalizedName = normalizeHeaderName(headerName);
52
+ attributes[`${ATTRIBUTE_KEYS.HTTP_REQUEST_HEADER}.${normalizedName}`] =
53
+ headerValue;
54
+ }
55
+ }
56
+
57
+ if (endContext.responseHeaders) {
58
+ for (const [headerName, headerValue] of Object.entries(
59
+ endContext.responseHeaders
60
+ )) {
61
+ const normalizedName = normalizeHeaderName(headerName);
62
+ attributes[`${ATTRIBUTE_KEYS.HTTP_RESPONSE_HEADER}.${normalizedName}`] =
63
+ headerValue;
64
+ }
65
+ }
66
+
46
67
  span.setAttributes(attributes);
47
68
  return attributes;
48
69
  }
@@ -58,7 +79,6 @@ export function createNetworkSpan(
58
79
  let baseAttributes: PulseAttributes = {
59
80
  [ATTRIBUTE_KEYS.HTTP_METHOD]: method,
60
81
  [ATTRIBUTE_KEYS.HTTP_URL]: startContext.url,
61
- [ATTRIBUTE_KEYS.PULSE_TYPE]: PHASE_VALUES.NETWORK,
62
82
  [ATTRIBUTE_KEYS.HTTP_REQUEST_TYPE]: interceptorType,
63
83
  };
64
84
 
@@ -12,7 +12,6 @@ export enum ATTRIBUTE_KEYS {
12
12
  PULSE_TYPE = 'pulse.type',
13
13
  SCREEN_NAME = 'screen.name',
14
14
  ROUTE_KEY = 'routeKey',
15
- PHASE = 'phase',
16
15
  LAST_SCREEN_NAME = 'last.screen.name',
17
16
  ROUTE_HAS_BEEN_SEEN = 'routeHasBeenSeen',
18
17
  PLATFORM = 'platform',
@@ -22,6 +21,8 @@ export enum ATTRIBUTE_KEYS {
22
21
  HTTP_URL = 'http.url',
23
22
  HTTP_STATUS_CODE = 'http.status_code',
24
23
  HTTP_REQUEST_TYPE = 'http.request.type',
24
+ HTTP_REQUEST_HEADER = 'http.request.header',
25
+ HTTP_RESPONSE_HEADER = 'http.response.header',
25
26
  ERROR_MESSAGE = 'error.message',
26
27
  ERROR_STACK = 'error.stack',
27
28
  }
@@ -30,9 +31,22 @@ export enum PULSE_TYPES {
30
31
  SCREEN_SESSION = 'screen_session',
31
32
  SCREEN_LOAD = 'screen_load',
32
33
  SCREEN_INTERACTIVE = 'screen_interactive',
33
- }
34
-
35
- export enum PHASE_VALUES {
36
- START = 'start',
37
34
  NETWORK = 'network',
38
35
  }
36
+
37
+ export const PULSE_FEATURE_NAMES = {
38
+ RN_SCREEN_LOAD: 'rn_screen_load',
39
+ SCREEN_SESSION: 'screen_session',
40
+ RN_SCREEN_INTERACTIVE: 'rn_screen_interactive',
41
+ NETWORK_INSTRUMENTATION: 'network_instrumentation',
42
+ CUSTOM_EVENTS: 'custom_events',
43
+ JS_CRASH: 'js_crash',
44
+ } as const;
45
+
46
+ export type PulseFeatureName =
47
+ (typeof PULSE_FEATURE_NAMES)[keyof typeof PULSE_FEATURE_NAMES];
48
+
49
+ export type NavigationFeatureName =
50
+ | typeof PULSE_FEATURE_NAMES.SCREEN_SESSION
51
+ | typeof PULSE_FEATURE_NAMES.RN_SCREEN_LOAD
52
+ | typeof PULSE_FEATURE_NAMES.RN_SCREEN_INTERACTIVE;
@@ -16,4 +16,4 @@ export type PulseAttributes = Record<
16
16
  PulseAttributeValue | undefined | null
17
17
  >;
18
18
 
19
- export type PulseFeatureConfig = Record<string, boolean> | null;
19
+ export type PulseFeatureConfig = Record<string, boolean> | null | undefined;