@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,59 @@
1
+ import { useRef, useCallback, useEffect, useMemo, type RefObject } from 'react';
2
+ import {
3
+ DEFAULT_NAVIGATION_OPTIONS,
4
+ type NavigationIntegrationOptions,
5
+ } from './navigation.interface';
6
+ import type { ReactNavigationIntegration } from './index';
7
+
8
+ export function useNavigationTracking(
9
+ navigationRef: RefObject<any>,
10
+ options?: NavigationIntegrationOptions,
11
+ createIntegration?: (
12
+ options?: NavigationIntegrationOptions
13
+ ) => ReactNavigationIntegration
14
+ ): () => void {
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;
24
+
25
+ const integration = useMemo(() => {
26
+ if (createIntegration) {
27
+ return createIntegration({
28
+ screenSessionTracking,
29
+ screenNavigationTracking,
30
+ screenInteractiveTracking,
31
+ });
32
+ }
33
+ throw new Error('createIntegration must be provided');
34
+ }, [
35
+ screenSessionTracking,
36
+ screenNavigationTracking,
37
+ screenInteractiveTracking,
38
+ createIntegration,
39
+ ]);
40
+
41
+ const cleanupRef = useRef<(() => void) | null>(null);
42
+
43
+ useEffect(() => {
44
+ return () => {
45
+ if (cleanupRef.current) {
46
+ cleanupRef.current();
47
+ }
48
+ };
49
+ }, []);
50
+
51
+ const onReady = useCallback(() => {
52
+ if (navigationRef.current && integration) {
53
+ cleanupRef.current =
54
+ integration.registerNavigationContainer(navigationRef);
55
+ }
56
+ }, [navigationRef, integration]);
57
+
58
+ return onReady;
59
+ }
@@ -0,0 +1,19 @@
1
+ const NAVIGATION_HISTORY_MAX_SIZE = 200;
2
+
3
+ export const LOG_TAGS = {
4
+ NAVIGATION: '[Pulse Navigation]',
5
+ SCREEN_LOAD: '[Pulse Screen Load]',
6
+ SCREEN_SESSION: '[Pulse Screen Session]',
7
+ SCREEN_INTERACTIVE: '[Pulse Screen Interactive]',
8
+ } as const;
9
+
10
+ export function pushRecentRouteKey(
11
+ recentRouteKeys: string[],
12
+ key: string
13
+ ): string[] {
14
+ const updated = [...recentRouteKeys, key];
15
+ if (updated.length > NAVIGATION_HISTORY_MAX_SIZE) {
16
+ return updated.slice(updated.length - NAVIGATION_HISTORY_MAX_SIZE);
17
+ }
18
+ return updated;
19
+ }
@@ -0,0 +1,110 @@
1
+ import type { PulseAttributes } from '../pulse.interface';
2
+ import { parseUrl } from './url-helper';
3
+ import { ATTRIBUTE_KEYS } from '../pulse.constants';
4
+
5
+ export interface GraphQLOperationData {
6
+ operationName?: string;
7
+ operationType?: string;
8
+ }
9
+
10
+ export function isGraphQLRequest(url: string): boolean {
11
+ if (!url || typeof url !== 'string') {
12
+ return false;
13
+ }
14
+ return url.toLowerCase().includes('graphql');
15
+ }
16
+
17
+ function parseGraphQLQuery(query: string): GraphQLOperationData {
18
+ if (!query || typeof query !== 'string') {
19
+ return {};
20
+ }
21
+
22
+ // Named query: "query MyQuery { ... }" or "mutation CreateUser { ... }"
23
+ const namedQueryRe =
24
+ /^(?:\s*)(query|mutation|subscription)(?:\s+)(\w+)(?:\s*)[{(]/;
25
+ const namedMatch = query.match(namedQueryRe);
26
+ if (namedMatch && namedMatch.length >= 3) {
27
+ return {
28
+ operationType: namedMatch[1],
29
+ operationName: namedMatch[2],
30
+ };
31
+ }
32
+
33
+ // Unnamed query: "query { ... }" or "mutation { ... }"
34
+ const unnamedQueryRe = /^(?:\s*)(query|mutation|subscription)(?:\s*)[{(]/;
35
+ const unnamedMatch = query.match(unnamedQueryRe);
36
+ if (unnamedMatch && unnamedMatch.length >= 2) {
37
+ return {
38
+ operationType: unnamedMatch[1],
39
+ operationName: undefined,
40
+ };
41
+ }
42
+
43
+ return {};
44
+ }
45
+
46
+ function extractFromBody(body: string): GraphQLOperationData {
47
+ try {
48
+ const payload = JSON.parse(body);
49
+ let data: GraphQLOperationData = {
50
+ operationName: payload?.operationName,
51
+ operationType: payload?.operation,
52
+ };
53
+
54
+ // Fallback: If operationName/operation not in payload, parse from query string
55
+ if ((!data.operationName || !data.operationType) && payload?.query) {
56
+ const parsedQuery = parseGraphQLQuery(payload.query);
57
+ data = {
58
+ operationName: data.operationName || parsedQuery.operationName,
59
+ operationType: data.operationType || parsedQuery.operationType,
60
+ };
61
+ }
62
+
63
+ return data;
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+
69
+ function extractFromQueryParams(url: string): GraphQLOperationData {
70
+ try {
71
+ const parsedUrl = parseUrl(url);
72
+ if (!parsedUrl) {
73
+ return {};
74
+ }
75
+ return {
76
+ operationName: parsedUrl.searchParams.get('operationName') || undefined,
77
+ operationType: parsedUrl.searchParams.get('operation') || undefined,
78
+ };
79
+ } catch {
80
+ return {};
81
+ }
82
+ }
83
+
84
+ export function updateAttributesWithGraphQLData(
85
+ url: string,
86
+ body?: Document | XMLHttpRequestBodyInit | null
87
+ ): PulseAttributes {
88
+ if (!isGraphQLRequest(url)) {
89
+ return {};
90
+ }
91
+
92
+ let data: GraphQLOperationData = {};
93
+
94
+ if (body && typeof body === 'string') {
95
+ data = extractFromBody(body);
96
+ }
97
+ if (!data.operationName && !data.operationType) {
98
+ data = extractFromQueryParams(url);
99
+ }
100
+
101
+ const attributes: PulseAttributes = {};
102
+ if (data.operationName) {
103
+ attributes[ATTRIBUTE_KEYS.GRAPHQL_OPERATION_NAME] = data.operationName;
104
+ }
105
+ if (data.operationType) {
106
+ attributes[ATTRIBUTE_KEYS.GRAPHQL_OPERATION_TYPE] = data.operationType;
107
+ }
108
+
109
+ return attributes;
110
+ }
@@ -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,13 +82,33 @@ 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
- const span = createNetworkSpan(startContext, 'xmlhttprequest');
109
+ this.setRequestHeader('X-Pulse-RN-Tracked', 'true');
110
+
111
+ const span = createNetworkSpan(startContext, 'xmlhttprequest', body);
68
112
  trackedSpans.set(this, span);
69
113
  const { onRequestEnd } = requestTracker.start(startContext);
70
114
 
@@ -72,13 +116,56 @@ function createXmlHttpRequestTracker(
72
116
  if (this.readyState === xhr.DONE && onRequestEnd) {
73
117
  const activeSpan = trackedSpans.get(this);
74
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
+
75
149
  // Determine request outcome based on status code
76
150
  let endContext: RequestEndContext;
77
151
 
152
+ const responseHeaders =
153
+ Object.keys(capturedResponseHeaders).length > 0
154
+ ? capturedResponseHeaders
155
+ : undefined;
156
+
78
157
  if (this.status <= 0 || this.status >= 400) {
79
- endContext = { state: 'error', status: this.status };
158
+ endContext = {
159
+ state: 'error',
160
+ status: this.status,
161
+ responseHeaders,
162
+ };
80
163
  } else {
81
- endContext = { state: 'success', status: this.status };
164
+ endContext = {
165
+ state: 'success',
166
+ status: this.status,
167
+ responseHeaders,
168
+ };
82
169
  }
83
170
 
84
171
  if (activeSpan) {
@@ -91,6 +178,9 @@ function createXmlHttpRequestTracker(
91
178
  trackedSpans.delete(this);
92
179
  }
93
180
 
181
+ // Clean up
182
+ requestHeadersMap.delete(this);
183
+
94
184
  onRequestEnd(endContext);
95
185
  }
96
186
  };
@@ -7,6 +7,9 @@ import type { Span } from '../index';
7
7
  import { Pulse, SpanStatusCode } from '../index';
8
8
  import type { PulseAttributes } from '../pulse.interface';
9
9
  import { extractHttpAttributes } from './url-helper';
10
+ import { updateAttributesWithGraphQLData } from './graphql-helper';
11
+ import { ATTRIBUTE_KEYS, PULSE_TYPES } from '../pulse.constants';
12
+ import { normalizeHeaderName } from './header-helper';
10
13
 
11
14
  export function setNetworkSpanAttributes(
12
15
  span: Span,
@@ -15,51 +18,77 @@ export function setNetworkSpanAttributes(
15
18
  ): PulseAttributes {
16
19
  const method = startContext.method.toUpperCase();
17
20
  let attributes: PulseAttributes = {
18
- 'http.method': method,
19
- 'http.url': startContext.url,
20
- 'pulse.type': `network.${endContext.status ?? 0}`,
21
- 'http.request.type': startContext.type,
22
- 'platform': Platform.OS as 'android' | 'ios' | 'web',
21
+ [ATTRIBUTE_KEYS.HTTP_METHOD]: method,
22
+ [ATTRIBUTE_KEYS.HTTP_URL]: startContext.url,
23
+ [ATTRIBUTE_KEYS.PULSE_TYPE]: `${PULSE_TYPES.NETWORK}.${endContext.status ?? 0}`,
24
+ [ATTRIBUTE_KEYS.HTTP_REQUEST_TYPE]: startContext.type,
25
+ [ATTRIBUTE_KEYS.PLATFORM]: Platform.OS,
23
26
  };
24
27
 
25
- // We had implemented our own URL parsing helper to avoid errors on RN < 0.80. Since this is not supported by React Native.
28
+ // We had implemented our own URL parsing helper to avoid errors on RN < 0.80. Since this is not supported by React Native.
26
29
  // Check here: https://github.com/facebook/react-native/blob/v0.79.0/packages/react-native/Libraries/Blob/URL.js
27
30
  const urlAttributes = extractHttpAttributes(startContext.url);
28
31
  attributes = { ...attributes, ...urlAttributes };
29
32
 
30
33
  if (endContext.status) {
31
- attributes['http.status_code'] = endContext.status;
34
+ attributes[ATTRIBUTE_KEYS.HTTP_STATUS_CODE] = endContext.status;
32
35
  }
33
36
 
34
37
  if (endContext.state === 'error' && endContext.error) {
35
38
  attributes.error = true;
36
- attributes['error.message'] =
39
+ attributes[ATTRIBUTE_KEYS.ERROR_MESSAGE] =
37
40
  endContext.error.message || String(endContext.error);
38
41
  if (endContext.error.stack) {
39
- attributes['error.stack'] = endContext.error.stack;
42
+ attributes[ATTRIBUTE_KEYS.ERROR_STACK] = endContext.error.stack;
40
43
  }
41
44
  span.recordException(endContext.error, attributes);
42
45
  }
43
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
+
44
67
  span.setAttributes(attributes);
45
68
  return attributes;
46
69
  }
47
70
 
48
71
  export function createNetworkSpan(
49
72
  startContext: RequestStartContext,
50
- interceptorType: 'fetch' | 'xmlhttprequest'
73
+ interceptorType: 'fetch' | 'xmlhttprequest',
74
+ body?: Document | XMLHttpRequestBodyInit | null
51
75
  ): Span {
52
76
  const method = startContext.method.toUpperCase();
53
77
  const spanName = `HTTP ${method}`;
54
78
 
55
- const span = Pulse.startSpan(spanName, {
56
- attributes: {
57
- 'http.method': method,
58
- 'http.url': startContext.url,
59
- 'pulse.type': 'network',
60
- 'http.request.type': interceptorType,
61
- },
62
- });
79
+ let baseAttributes: PulseAttributes = {
80
+ [ATTRIBUTE_KEYS.HTTP_METHOD]: method,
81
+ [ATTRIBUTE_KEYS.HTTP_URL]: startContext.url,
82
+ [ATTRIBUTE_KEYS.HTTP_REQUEST_TYPE]: interceptorType,
83
+ };
84
+
85
+ const graphqlAttributes = updateAttributesWithGraphQLData(
86
+ startContext.url,
87
+ body
88
+ );
89
+ const attributes = { ...baseAttributes, ...graphqlAttributes };
90
+
91
+ const span = Pulse.startSpan(spanName, { attributes });
63
92
 
64
93
  return span;
65
94
  }
@@ -8,6 +8,67 @@
8
8
  * Based on: https://github.com/facebook/react-native/blob/v0.80.0/packages/react-native/Libraries/Blob/URL.js
9
9
  */
10
10
 
11
+ export class SearchParams {
12
+ private params: Map<string, string> = new Map();
13
+
14
+ constructor(search: string) {
15
+ if (!search || typeof search !== 'string') {
16
+ return;
17
+ }
18
+
19
+ // Remove leading '?' if present
20
+ const queryString = search.startsWith('?') ? search.slice(1) : search;
21
+
22
+ if (!queryString) {
23
+ return;
24
+ }
25
+
26
+ try {
27
+ // Split by '&' to get individual parameters
28
+ const pairs = queryString.split('&');
29
+
30
+ for (const pair of pairs) {
31
+ if (!pair) continue;
32
+
33
+ // Split by '=' to get key and value
34
+ const equalIndex = pair.indexOf('=');
35
+ if (equalIndex === -1) {
36
+ // Parameter without value
37
+ const key = decodeURIComponent(pair);
38
+ if (key) {
39
+ this.params.set(key, '');
40
+ }
41
+ } else {
42
+ const key = decodeURIComponent(pair.substring(0, equalIndex));
43
+ const value = decodeURIComponent(pair.substring(equalIndex + 1));
44
+ if (key) {
45
+ this.params.set(key, value);
46
+ }
47
+ }
48
+ }
49
+ } catch (e) {
50
+ // If decoding fails, params remain empty
51
+ console.warn('[Pulse] Query parameter parsing failed:', e);
52
+ }
53
+ }
54
+
55
+ get(name: string): string | null {
56
+ return this.params.has(name) ? this.params.get(name)! : null;
57
+ }
58
+
59
+ has(name: string): boolean {
60
+ return this.params.has(name);
61
+ }
62
+
63
+ keys(): string[] {
64
+ return Array.from(this.params.keys());
65
+ }
66
+
67
+ values(): string[] {
68
+ return Array.from(this.params.values());
69
+ }
70
+ }
71
+
11
72
  export interface ParsedUrl {
12
73
  protocol: string;
13
74
  hostname: string;
@@ -17,6 +78,7 @@ export interface ParsedUrl {
17
78
  search: string;
18
79
  hash: string;
19
80
  href: string;
81
+ searchParams: SearchParams;
20
82
  }
21
83
 
22
84
  /**
@@ -31,7 +93,7 @@ function safeMatch(
31
93
  try {
32
94
  const match = text.match(regex);
33
95
  return match && match[groupIndex] ? match[groupIndex] : '';
34
- } catch (e) {
96
+ } catch {
35
97
  return '';
36
98
  }
37
99
  }
@@ -81,6 +143,9 @@ export function parseUrl(url: string): ParsedUrl | null {
81
143
  const hashContent = safeMatch(url, /#([^/]*)/);
82
144
  const hash = hashContent ? `#${hashContent}` : '';
83
145
 
146
+ // Create SearchParams instance for query parameters
147
+ const searchParams = new SearchParams(search);
148
+
84
149
  return {
85
150
  protocol: protocolWithColon,
86
151
  hostname: hostname,
@@ -90,6 +155,7 @@ export function parseUrl(url: string): ParsedUrl | null {
90
155
  search: search,
91
156
  hash: hash,
92
157
  href: url,
158
+ searchParams: searchParams,
93
159
  };
94
160
  } catch (e) {
95
161
  // Any unexpected error during parsing - return null
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Constants for Pulse React Native OpenTelemetry integration
3
+ */
4
+
5
+ export enum SPAN_NAMES {
6
+ SCREEN_SESSION = 'ScreenSession',
7
+ NAVIGATED = 'Navigated',
8
+ SCREEN_INTERACTIVE = 'ScreenInteractive',
9
+ }
10
+
11
+ export enum ATTRIBUTE_KEYS {
12
+ PULSE_TYPE = 'pulse.type',
13
+ SCREEN_NAME = 'screen.name',
14
+ ROUTE_KEY = 'routeKey',
15
+ LAST_SCREEN_NAME = 'last.screen.name',
16
+ ROUTE_HAS_BEEN_SEEN = 'routeHasBeenSeen',
17
+ PLATFORM = 'platform',
18
+ GRAPHQL_OPERATION_NAME = 'graphql.operation.name',
19
+ GRAPHQL_OPERATION_TYPE = 'graphql.operation.type',
20
+ HTTP_METHOD = 'http.method',
21
+ HTTP_URL = 'http.url',
22
+ HTTP_STATUS_CODE = 'http.status_code',
23
+ HTTP_REQUEST_TYPE = 'http.request.type',
24
+ HTTP_REQUEST_HEADER = 'http.request.header',
25
+ HTTP_RESPONSE_HEADER = 'http.response.header',
26
+ ERROR_MESSAGE = 'error.message',
27
+ ERROR_STACK = 'error.stack',
28
+ }
29
+
30
+ export enum PULSE_TYPES {
31
+ SCREEN_SESSION = 'screen_session',
32
+ SCREEN_LOAD = 'screen_load',
33
+ SCREEN_INTERACTIVE = 'screen_interactive',
34
+ NETWORK = 'network',
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;
@@ -11,4 +11,9 @@ export type PulseAttributeValue =
11
11
  | number[]
12
12
  | boolean[];
13
13
 
14
- export type PulseAttributes = Record<string, PulseAttributeValue | undefined>;
14
+ export type PulseAttributes = Record<
15
+ string,
16
+ PulseAttributeValue | undefined | null
17
+ >;
18
+
19
+ export type PulseFeatureConfig = Record<string, boolean> | null | undefined;