@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.
- package/PulseReactNativeOtel.podspec +1 -1
- package/README.md +34 -879
- package/android/build.gradle +10 -15
- package/android/proguard-rules.pro +3 -99
- package/android/src/main/java/com/pulsereactnativeotel/Pulse.kt +89 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseOtelConstants.kt +1 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelLogger.kt +3 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +69 -3
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelPackage.kt +1 -1
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelTracer.kt +24 -8
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesLogRecordProcessor.kt +21 -0
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenAttributesSpanProcessor.kt +30 -0
- package/android/src/main/java/com/pulsereactnativeotel/ReactNativeScreenNameTracker.kt +17 -0
- package/app.plugin.js +1 -0
- package/ios/PulseReactNativeOtel.mm +7 -1
- package/lib/module/NativePulseReactNativeOtel.js.map +1 -1
- package/lib/module/config.js +57 -19
- package/lib/module/config.js.map +1 -1
- package/lib/module/errorBoundary.js.map +1 -1
- package/lib/module/events.js +6 -0
- package/lib/module/events.js.map +1 -1
- package/lib/module/index.js +4 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigation/index.js +179 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/navigation.interface.js +8 -0
- package/lib/module/navigation/navigation.interface.js.map +1 -0
- package/lib/module/navigation/screen-interactive.js +101 -0
- package/lib/module/navigation/screen-interactive.js.map +1 -0
- package/lib/module/navigation/screen-load.js +67 -0
- package/lib/module/navigation/screen-load.js.map +1 -0
- package/lib/module/navigation/screen-session.js +60 -0
- package/lib/module/navigation/screen-session.js.map +1 -0
- package/lib/module/navigation/useNavigationTracking.js +34 -0
- package/lib/module/navigation/useNavigationTracking.js.map +1 -0
- package/lib/module/navigation/utils.js +17 -0
- package/lib/module/navigation/utils.js.map +1 -0
- package/lib/module/network-interceptor/graphql-helper.js +92 -0
- package/lib/module/network-interceptor/graphql-helper.js.map +1 -0
- 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 +61 -4
- package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -1
- package/lib/module/network-interceptor/span-helpers.js +36 -16
- package/lib/module/network-interceptor/span-helpers.js.map +1 -1
- package/lib/module/network-interceptor/url-helper.js +58 -2
- package/lib/module/network-interceptor/url-helper.js.map +1 -1
- package/lib/module/pulse.constants.js +47 -0
- package/lib/module/pulse.constants.js.map +1 -0
- package/lib/module/trace.js +17 -2
- package/lib/module/trace.js.map +1 -1
- package/lib/typescript/plugin/src/index.d.ts +5 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/types.d.ts +27 -0
- package/lib/typescript/plugin/src/types.d.ts.map +1 -0
- package/lib/typescript/plugin/src/utils.d.ts +10 -0
- package/lib/typescript/plugin/src/utils.d.ts.map +1 -0
- package/lib/typescript/plugin/src/withAndroidPulse.d.ts +4 -0
- package/lib/typescript/plugin/src/withAndroidPulse.d.ts.map +1 -0
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +15 -2
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -1
- package/lib/typescript/src/config.d.ts +14 -8
- package/lib/typescript/src/config.d.ts.map +1 -1
- package/lib/typescript/src/errorBoundary.d.ts.map +1 -1
- package/lib/typescript/src/events.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/index.d.ts +13 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/navigation.interface.d.ts +18 -0
- package/lib/typescript/src/navigation/navigation.interface.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-interactive.d.ts +16 -0
- package/lib/typescript/src/navigation/screen-interactive.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-load.d.ts +13 -0
- package/lib/typescript/src/navigation/screen-load.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen-session.d.ts +15 -0
- package/lib/typescript/src/navigation/screen-session.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts +5 -0
- package/lib/typescript/src/navigation/useNavigationTracking.d.ts.map +1 -0
- package/lib/typescript/src/navigation/utils.d.ts +8 -0
- package/lib/typescript/src/navigation/utils.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/graphql-helper.d.ts +8 -0
- package/lib/typescript/src/network-interceptor/graphql-helper.d.ts.map +1 -0
- 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 +1 -1
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -1
- package/lib/typescript/src/network-interceptor/url-helper.d.ts +9 -0
- package/lib/typescript/src/network-interceptor/url-helper.d.ts.map +1 -1
- package/lib/typescript/src/pulse.constants.d.ts +43 -0
- package/lib/typescript/src/pulse.constants.d.ts.map +1 -0
- package/lib/typescript/src/pulse.interface.d.ts +2 -1
- package/lib/typescript/src/pulse.interface.d.ts.map +1 -1
- package/lib/typescript/src/trace.d.ts +7 -0
- package/lib/typescript/src/trace.d.ts.map +1 -1
- package/package.json +29 -9
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/index.js +10 -0
- package/plugin/build/types.d.ts +26 -0
- package/plugin/build/types.js +2 -0
- package/plugin/build/utils.d.ts +9 -0
- package/plugin/build/utils.js +102 -0
- package/plugin/build/withAndroidPulse.d.ts +3 -0
- package/plugin/build/withAndroidPulse.js +53 -0
- package/scripts/pulse-cli.js +82 -0
- package/scripts/uploadService.js +122 -0
- package/scripts/utils.js +125 -0
- package/src/NativePulseReactNativeOtel.ts +18 -2
- package/src/config.ts +94 -23
- package/src/errorBoundary.tsx +11 -5
- package/src/events.ts +7 -0
- package/src/global.d.ts +0 -1
- package/src/index.tsx +7 -4
- package/src/navigation/index.ts +335 -0
- package/src/navigation/navigation.interface.ts +26 -0
- package/src/navigation/screen-interactive.ts +149 -0
- package/src/navigation/screen-load.ts +97 -0
- package/src/navigation/screen-session.ts +87 -0
- package/src/navigation/useNavigationTracking.ts +59 -0
- package/src/navigation/utils.ts +19 -0
- package/src/network-interceptor/graphql-helper.ts +110 -0
- 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 +93 -3
- package/src/network-interceptor/span-helpers.ts +47 -18
- package/src/network-interceptor/url-helper.ts +67 -1
- package/src/pulse.constants.ts +52 -0
- package/src/pulse.interface.ts +6 -1
- package/src/trace.ts +25 -2
- package/LICENSE +0 -20
- package/lib/module/network-interceptor/request-tracker-fetch.js +0 -72
- package/lib/module/network-interceptor/request-tracker-fetch.js.map +0 -1
- package/lib/module/reactNavigation.js +0 -100
- package/lib/module/reactNavigation.js.map +0 -1
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts +0 -7
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts.map +0 -1
- package/lib/typescript/src/reactNavigation.d.ts +0 -10
- package/lib/typescript/src/reactNavigation.d.ts.map +0 -1
- package/src/network-interceptor/request-tracker-fetch.ts +0 -96
- 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
|
|
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
|
-
|
|
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 = {
|
|
158
|
+
endContext = {
|
|
159
|
+
state: 'error',
|
|
160
|
+
status: this.status,
|
|
161
|
+
responseHeaders,
|
|
162
|
+
};
|
|
80
163
|
} else {
|
|
81
|
-
endContext = {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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[
|
|
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[
|
|
39
|
+
attributes[ATTRIBUTE_KEYS.ERROR_MESSAGE] =
|
|
37
40
|
endContext.error.message || String(endContext.error);
|
|
38
41
|
if (endContext.error.stack) {
|
|
39
|
-
attributes[
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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;
|
package/src/pulse.interface.ts
CHANGED
|
@@ -11,4 +11,9 @@ export type PulseAttributeValue =
|
|
|
11
11
|
| number[]
|
|
12
12
|
| boolean[];
|
|
13
13
|
|
|
14
|
-
export type PulseAttributes = Record<
|
|
14
|
+
export type PulseAttributes = Record<
|
|
15
|
+
string,
|
|
16
|
+
PulseAttributeValue | undefined | null
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export type PulseFeatureConfig = Record<string, boolean> | null | undefined;
|