@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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +2 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +40 -24
- package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
- package/lib/module/config.js +36 -18
- package/lib/module/config.js.map +1 -1
- package/lib/module/navigation/index.js +10 -3
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/navigation.interface.js +6 -0
- package/lib/module/navigation/navigation.interface.js.map +1 -1
- package/lib/module/navigation/screen-load.js +1 -2
- package/lib/module/navigation/screen-load.js.map +1 -1
- package/lib/module/navigation/useNavigationTracking.js +4 -3
- package/lib/module/navigation/useNavigationTracking.js.map +1 -1
- package/lib/module/network-interceptor/header-helper.js +24 -0
- package/lib/module/network-interceptor/header-helper.js.map +1 -0
- package/lib/module/network-interceptor/initialization.js +18 -1
- package/lib/module/network-interceptor/initialization.js.map +1 -1
- package/lib/module/network-interceptor/request-tracker-xhr.js +59 -3
- package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
- package/lib/module/network-interceptor/span-helpers.js +15 -3
- package/lib/module/network-interceptor/span-helpers.js.map +1 -1
- package/lib/module/pulse.constants.js +11 -6
- package/lib/module/pulse.constants.js.map +1 -1
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +8 -1
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
- package/lib/typescript/src/config.d.ts +7 -7
- package/lib/typescript/src/config.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/index.d.ts +1 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/navigation.interface.d.ts +1 -0
- package/lib/typescript/src/navigation/navigation.interface.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen-load.d.ts.map +1 -1
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts +1 -1
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/header-helper.d.ts +15 -0
- package/lib/typescript/src/network-interceptor/header-helper.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/initialization.d.ts +4 -1
- package/lib/typescript/src/network-interceptor/initialization.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/network.interface.d.ts +3 -0
- package/lib/typescript/src/network-interceptor/network.interface.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -1
- package/lib/typescript/src/pulse.constants.d.ts +13 -5
- package/lib/typescript/src/pulse.constants.d.ts.map +1 -1
- package/lib/typescript/src/pulse.interface.d.ts +1 -1
- package/lib/typescript/src/pulse.interface.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativePulseReactNativeOtel.ts +8 -1
- package/src/config.ts +68 -26
- package/src/index.tsx +1 -1
- package/src/navigation/index.ts +36 -7
- package/src/navigation/navigation.interface.ts +7 -0
- package/src/navigation/screen-load.ts +1 -7
- package/src/navigation/useNavigationTracking.ts +13 -4
- package/src/network-interceptor/header-helper.ts +26 -0
- package/src/network-interceptor/initialization.ts +22 -1
- package/src/network-interceptor/network.interface.ts +3 -0
- package/src/network-interceptor/request-tracker-xhr.ts +90 -2
- package/src/network-interceptor/span-helpers.ts +23 -3
- package/src/pulse.constants.ts +19 -5
- package/src/pulse.interface.ts +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useRef, useCallback, useEffect, useMemo, type RefObject } from 'react';
|
|
2
|
-
import
|
|
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 =
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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 = {
|
|
158
|
+
endContext = {
|
|
159
|
+
state: 'error',
|
|
160
|
+
status: this.status,
|
|
161
|
+
responseHeaders,
|
|
162
|
+
};
|
|
82
163
|
} else {
|
|
83
|
-
endContext = {
|
|
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,
|
|
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]:
|
|
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
|
|
package/src/pulse.constants.ts
CHANGED
|
@@ -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;
|
package/src/pulse.interface.ts
CHANGED