@dreamhorizonorg/pulse-react-native 0.0.1
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/LICENSE +20 -0
- package/PulseReactNativeOtel.podspec +21 -0
- package/README.md +900 -0
- package/android/build.gradle +97 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +108 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseOtelConstants.kt +20 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelLogger.kt +73 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelModule.kt +85 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelPackage.kt +33 -0
- package/android/src/main/java/com/pulsereactnativeotel/PulseReactNativeOtelTracer.kt +127 -0
- package/ios/PulseReactNativeOtel.h +5 -0
- package/ios/PulseReactNativeOtel.mm +100 -0
- package/lib/module/NativePulseReactNativeOtel.js +5 -0
- package/lib/module/NativePulseReactNativeOtel.js.map +1 -0
- package/lib/module/config.js +55 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/errorBoundary.js +62 -0
- package/lib/module/errorBoundary.js.map +1 -0
- package/lib/module/errorHandler.js +112 -0
- package/lib/module/errorHandler.js.map +1 -0
- package/lib/module/events.js +14 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/global.d.js +4 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/globalAttributes.js +13 -0
- package/lib/module/globalAttributes.js.map +1 -0
- package/lib/module/index.js +27 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/initialization.js +18 -0
- package/lib/module/initialization.js.map +1 -0
- package/lib/module/network-interceptor/initialization.js +25 -0
- package/lib/module/network-interceptor/initialization.js.map +1 -0
- package/lib/module/network-interceptor/network.interface.js +2 -0
- package/lib/module/network-interceptor/network.interface.js.map +1 -0
- package/lib/module/network-interceptor/request-tracker-fetch.js +72 -0
- package/lib/module/network-interceptor/request-tracker-fetch.js.map +1 -0
- package/lib/module/network-interceptor/request-tracker-xhr.js +75 -0
- package/lib/module/network-interceptor/request-tracker-xhr.js.map +1 -0
- package/lib/module/network-interceptor/request-tracker.js +25 -0
- package/lib/module/network-interceptor/request-tracker.js.map +1 -0
- package/lib/module/network-interceptor/span-helpers.js +62 -0
- package/lib/module/network-interceptor/span-helpers.js.map +1 -0
- package/lib/module/network-interceptor/url-helper.js +127 -0
- package/lib/module/network-interceptor/url-helper.js.map +1 -0
- package/lib/module/pulse.interface.js +2 -0
- package/lib/module/pulse.interface.js.map +1 -0
- package/lib/module/reactNavigation.js +100 -0
- package/lib/module/reactNavigation.js.map +1 -0
- package/lib/module/trace.js +75 -0
- package/lib/module/trace.js.map +1 -0
- package/lib/module/user.js +23 -0
- package/lib/module/user.js.map +1 -0
- package/lib/module/utility.js +34 -0
- package/lib/module/utility.js.map +1 -0
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts +31 -0
- package/lib/typescript/src/NativePulseReactNativeOtel.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +14 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/errorBoundary.d.ts +26 -0
- package/lib/typescript/src/errorBoundary.d.ts.map +1 -0
- package/lib/typescript/src/errorHandler.d.ts +4 -0
- package/lib/typescript/src/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +3 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/globalAttributes.d.ts +4 -0
- package/lib/typescript/src/globalAttributes.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +30 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/initialization.d.ts +3 -0
- package/lib/typescript/src/initialization.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/initialization.d.ts +3 -0
- package/lib/typescript/src/network-interceptor/initialization.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/network.interface.d.ts +29 -0
- package/lib/typescript/src/network-interceptor/network.interface.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts +7 -0
- package/lib/typescript/src/network-interceptor/request-tracker-fetch.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts +4 -0
- package/lib/typescript/src/network-interceptor/request-tracker-xhr.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/request-tracker.d.ts +9 -0
- package/lib/typescript/src/network-interceptor/request-tracker.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts +7 -0
- package/lib/typescript/src/network-interceptor/span-helpers.d.ts.map +1 -0
- package/lib/typescript/src/network-interceptor/url-helper.d.ts +38 -0
- package/lib/typescript/src/network-interceptor/url-helper.d.ts.map +1 -0
- package/lib/typescript/src/pulse.interface.d.ts +8 -0
- package/lib/typescript/src/pulse.interface.d.ts.map +1 -0
- package/lib/typescript/src/reactNavigation.d.ts +10 -0
- package/lib/typescript/src/reactNavigation.d.ts.map +1 -0
- package/lib/typescript/src/trace.d.ts +19 -0
- package/lib/typescript/src/trace.d.ts.map +1 -0
- package/lib/typescript/src/user.d.ts +5 -0
- package/lib/typescript/src/user.d.ts.map +1 -0
- package/lib/typescript/src/utility.d.ts +8 -0
- package/lib/typescript/src/utility.d.ts.map +1 -0
- package/package.json +165 -0
- package/src/NativePulseReactNativeOtel.ts +57 -0
- package/src/config.ts +75 -0
- package/src/errorBoundary.tsx +92 -0
- package/src/errorHandler.ts +169 -0
- package/src/events.ts +15 -0
- package/src/global.d.ts +9 -0
- package/src/globalAttributes.ts +16 -0
- package/src/index.tsx +35 -0
- package/src/initialization.ts +18 -0
- package/src/network-interceptor/initialization.ts +28 -0
- package/src/network-interceptor/network.interface.ts +36 -0
- package/src/network-interceptor/request-tracker-fetch.ts +96 -0
- package/src/network-interceptor/request-tracker-xhr.ts +108 -0
- package/src/network-interceptor/request-tracker.ts +31 -0
- package/src/network-interceptor/span-helpers.ts +84 -0
- package/src/network-interceptor/url-helper.ts +155 -0
- package/src/pulse.interface.ts +14 -0
- package/src/reactNavigation.tsx +146 -0
- package/src/trace.ts +108 -0
- package/src/user.ts +24 -0
- package/src/utility.ts +36 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import PulseReactNativeOtel from './NativePulseReactNativeOtel';
|
|
2
|
+
import { mergeWithGlobalAttributes } from './globalAttributes';
|
|
3
|
+
import { isSupportedPlatform } from './initialization';
|
|
4
|
+
import { extractErrorDetails } from './utility';
|
|
5
|
+
import type { PulseAttributes } from './pulse.interface';
|
|
6
|
+
|
|
7
|
+
let previousErrorHandler: ((error: Error, isFatal?: boolean) => void) | null =
|
|
8
|
+
null;
|
|
9
|
+
|
|
10
|
+
let isInitialized = false;
|
|
11
|
+
let handlingFatal = false;
|
|
12
|
+
|
|
13
|
+
function reportToOpenTelemetry(
|
|
14
|
+
errorMessage: string,
|
|
15
|
+
stackTrace: string,
|
|
16
|
+
isFatal: boolean,
|
|
17
|
+
errorType: string,
|
|
18
|
+
attributes?: PulseAttributes
|
|
19
|
+
): void {
|
|
20
|
+
const observedTimeMs = Date.now();
|
|
21
|
+
const mergedAttributes = mergeWithGlobalAttributes(attributes || {});
|
|
22
|
+
PulseReactNativeOtel.reportException(
|
|
23
|
+
errorMessage,
|
|
24
|
+
observedTimeMs,
|
|
25
|
+
stackTrace,
|
|
26
|
+
isFatal,
|
|
27
|
+
errorType,
|
|
28
|
+
mergedAttributes
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleGlobalError(error: Error, isFatal?: boolean): void {
|
|
33
|
+
const shouldHandleFatal = !!isFatal;
|
|
34
|
+
if (shouldHandleFatal) {
|
|
35
|
+
if (handlingFatal) {
|
|
36
|
+
PulseReactNativeOtel.trackEvent(
|
|
37
|
+
'Encountered multiple fatal errors. The latest:',
|
|
38
|
+
Date.now(),
|
|
39
|
+
{
|
|
40
|
+
message: error.message,
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
handlingFatal = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { message, stackTrace, errorType } = extractErrorDetails(error);
|
|
49
|
+
reportToOpenTelemetry(message, stackTrace, isFatal || false, errorType);
|
|
50
|
+
console.error('[Pulse RN Crash]', 'Fatal:', isFatal, 'Error:', error);
|
|
51
|
+
if (previousErrorHandler && typeof previousErrorHandler === 'function') {
|
|
52
|
+
try {
|
|
53
|
+
if (isFatal) {
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
previousErrorHandler!(error, isFatal);
|
|
56
|
+
}, 150);
|
|
57
|
+
} else {
|
|
58
|
+
previousErrorHandler(error, isFatal);
|
|
59
|
+
}
|
|
60
|
+
} catch (handlerError) {
|
|
61
|
+
console.error('[Pulse RN] Previous error handler threw:', handlerError);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function reportException(
|
|
67
|
+
error: Error | string,
|
|
68
|
+
isFatal: boolean = false,
|
|
69
|
+
attributes?: PulseAttributes
|
|
70
|
+
): void {
|
|
71
|
+
if (!isSupportedPlatform()) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let errorMessage: string;
|
|
76
|
+
let stackTrace: string;
|
|
77
|
+
let errorType: string;
|
|
78
|
+
|
|
79
|
+
if (typeof error === 'string') {
|
|
80
|
+
errorMessage = error;
|
|
81
|
+
stackTrace = '';
|
|
82
|
+
errorType = '';
|
|
83
|
+
} else {
|
|
84
|
+
const details = extractErrorDetails(error);
|
|
85
|
+
errorMessage = details.message;
|
|
86
|
+
stackTrace = details.stackTrace;
|
|
87
|
+
errorType = details.errorType;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
reportToOpenTelemetry(
|
|
91
|
+
errorMessage,
|
|
92
|
+
stackTrace,
|
|
93
|
+
isFatal,
|
|
94
|
+
errorType,
|
|
95
|
+
attributes
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function initializeErrorHandler(): void {
|
|
100
|
+
if (isInitialized) {
|
|
101
|
+
console.warn('[Pulse RN] Error handler already initialized. Skipping.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
handlingFatal = false;
|
|
105
|
+
|
|
106
|
+
if (!ErrorUtils) {
|
|
107
|
+
console.warn(
|
|
108
|
+
'[Pulse RN] ErrorUtils not available; cannot install global error handler.'
|
|
109
|
+
);
|
|
110
|
+
PulseReactNativeOtel.trackEvent(
|
|
111
|
+
'ErrorUtils not found. Cannot install global error handler.',
|
|
112
|
+
Date.now()
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentHandler = ErrorUtils.getGlobalHandler?.();
|
|
118
|
+
|
|
119
|
+
if (currentHandler) {
|
|
120
|
+
previousErrorHandler = currentHandler;
|
|
121
|
+
console.log(
|
|
122
|
+
'[Pulse RN] Previous error handler detected (likely React Native default) - will be preserved'
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(
|
|
126
|
+
'[Pulse RN] No previous error handler detected (unusual in React Native)'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ErrorUtils.setGlobalHandler(handleGlobalError);
|
|
131
|
+
|
|
132
|
+
isInitialized = true;
|
|
133
|
+
handlingFatal = false;
|
|
134
|
+
|
|
135
|
+
console.log('[Pulse RN] Error handler initialized successfully');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function disableErrorHandler(): void {
|
|
139
|
+
if (!isInitialized) {
|
|
140
|
+
console.warn(
|
|
141
|
+
'[Pulse RN] Error handler not initialized. Nothing to disable.'
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (previousErrorHandler) {
|
|
147
|
+
ErrorUtils.setGlobalHandler(previousErrorHandler);
|
|
148
|
+
console.log(
|
|
149
|
+
'[Pulse RN] Error handler disabled. Previous handler restored.'
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
ErrorUtils.setGlobalHandler(null as any);
|
|
153
|
+
console.log(
|
|
154
|
+
'[Pulse RN] Error handler disabled. Restored to React Native default.'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isInitialized = false;
|
|
159
|
+
previousErrorHandler = null;
|
|
160
|
+
handlingFatal = false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function setupErrorHandler(enableErrorHandler: boolean): void {
|
|
164
|
+
if (enableErrorHandler) {
|
|
165
|
+
initializeErrorHandler();
|
|
166
|
+
} else {
|
|
167
|
+
disableErrorHandler();
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PulseReactNativeOtel from './NativePulseReactNativeOtel';
|
|
2
|
+
import { mergeWithGlobalAttributes } from './globalAttributes';
|
|
3
|
+
import { isSupportedPlatform } from './initialization';
|
|
4
|
+
import type { PulseAttributes } from './pulse.interface';
|
|
5
|
+
|
|
6
|
+
export function trackEvent(event: string, attributes?: PulseAttributes): void {
|
|
7
|
+
if (!isSupportedPlatform()) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const observedTimeMs = Date.now();
|
|
12
|
+
const mergedAttributes = mergeWithGlobalAttributes(attributes || {});
|
|
13
|
+
|
|
14
|
+
PulseReactNativeOtel.trackEvent(event, observedTimeMs, mergedAttributes);
|
|
15
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PulseAttributes, PulseAttributeValue } from './pulse.interface';
|
|
2
|
+
|
|
3
|
+
const globalAttributes: PulseAttributes = {};
|
|
4
|
+
|
|
5
|
+
export function setGlobalAttribute(
|
|
6
|
+
key: string,
|
|
7
|
+
value: PulseAttributeValue
|
|
8
|
+
): void {
|
|
9
|
+
globalAttributes[key] = value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mergeWithGlobalAttributes<T extends PulseAttributes>(
|
|
13
|
+
attributes?: T
|
|
14
|
+
): T & PulseAttributes {
|
|
15
|
+
return { ...globalAttributes, ...attributes } as T & PulseAttributes;
|
|
16
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { startSpan, trackSpan } from './trace';
|
|
2
|
+
import { reportException } from './errorHandler';
|
|
3
|
+
import { trackEvent } from './events';
|
|
4
|
+
import { start, createNavigationIntegrationWithConfig } from './config';
|
|
5
|
+
import { isInitialized } from './initialization';
|
|
6
|
+
import { setGlobalAttribute } from './globalAttributes';
|
|
7
|
+
import { setUserId, setUserProperty, setUserProperties } from './user';
|
|
8
|
+
import { ErrorBoundary, withErrorBoundary } from './errorBoundary';
|
|
9
|
+
|
|
10
|
+
export type { Span } from './trace';
|
|
11
|
+
export type { PulseConfig, PulseStartOptions } from './config';
|
|
12
|
+
export type { PulseAttributes, PulseAttributeValue } from './pulse.interface';
|
|
13
|
+
export type {
|
|
14
|
+
ReactNavigationIntegration,
|
|
15
|
+
NavigationRoute,
|
|
16
|
+
} from './reactNavigation';
|
|
17
|
+
|
|
18
|
+
export type { ErrorBoundaryProps, FallbackRender } from './errorBoundary';
|
|
19
|
+
|
|
20
|
+
export { SpanStatusCode } from './trace';
|
|
21
|
+
export const Pulse = {
|
|
22
|
+
start,
|
|
23
|
+
isInitialized,
|
|
24
|
+
createNavigationIntegration: createNavigationIntegrationWithConfig,
|
|
25
|
+
trackEvent,
|
|
26
|
+
reportException,
|
|
27
|
+
trackSpan,
|
|
28
|
+
startSpan,
|
|
29
|
+
setGlobalAttribute,
|
|
30
|
+
setUserId,
|
|
31
|
+
setUserProperty,
|
|
32
|
+
setUserProperties,
|
|
33
|
+
ErrorBoundary,
|
|
34
|
+
withErrorBoundary,
|
|
35
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import PulseReactNativeOtel from './NativePulseReactNativeOtel';
|
|
3
|
+
|
|
4
|
+
let cachedInitStatus: boolean = false;
|
|
5
|
+
|
|
6
|
+
export function isInitialized(): boolean {
|
|
7
|
+
if (!isSupportedPlatform()) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (!cachedInitStatus) {
|
|
11
|
+
cachedInitStatus = PulseReactNativeOtel.isInitialized();
|
|
12
|
+
}
|
|
13
|
+
return cachedInitStatus;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isSupportedPlatform(): boolean {
|
|
17
|
+
return Platform.OS === 'android';
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import createXmlHttpRequestTracker from './request-tracker-xhr';
|
|
2
|
+
|
|
3
|
+
let isInitialized = false;
|
|
4
|
+
|
|
5
|
+
export function initializeNetworkInterceptor(): void {
|
|
6
|
+
if (isInitialized) {
|
|
7
|
+
console.warn('[Pulse] Network interceptor already initialized');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
console.log('[Pulse] 🔄 Starting network interceptor initialization...');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// In react-native, we are intercepting XMLHttpRequest only, since axios and fetch both use it internally.
|
|
15
|
+
// See: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Network/fetch.js
|
|
16
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
17
|
+
createXmlHttpRequestTracker(XMLHttpRequest);
|
|
18
|
+
} else {
|
|
19
|
+
console.warn('[Pulse] XMLHttpRequest is not available');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isInitialized = true;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('[Pulse] Failed to initialize network interceptor:', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const isNetworkInterceptorInitialized = (): boolean => isInitialized;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface RequestStartContext {
|
|
2
|
+
url: string;
|
|
3
|
+
method: string;
|
|
4
|
+
type: 'fetch' | 'xmlhttprequest';
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RequestEndContextSuccess {
|
|
9
|
+
status: number;
|
|
10
|
+
state: 'success';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RequestEndContextError {
|
|
14
|
+
state: 'error';
|
|
15
|
+
status?: number;
|
|
16
|
+
error?: Error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RequestEndContext =
|
|
20
|
+
| RequestEndContextSuccess
|
|
21
|
+
| RequestEndContextError;
|
|
22
|
+
|
|
23
|
+
export type RequestStartCallback = (
|
|
24
|
+
context: RequestStartContext
|
|
25
|
+
) => { onRequestEnd?: RequestEndCallback } | undefined;
|
|
26
|
+
|
|
27
|
+
export type RequestEndCallback = (context: RequestEndContext) => void;
|
|
28
|
+
|
|
29
|
+
export interface NetworkRequestInfo {
|
|
30
|
+
url: string;
|
|
31
|
+
method: string;
|
|
32
|
+
type: 'fetch' | 'xmlhttprequest';
|
|
33
|
+
status?: number;
|
|
34
|
+
state: 'success' | 'error';
|
|
35
|
+
error?: Error;
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RequestStartContext,
|
|
3
|
+
RequestEndContext,
|
|
4
|
+
} from './network.interface';
|
|
5
|
+
import { RequestTracker } from './request-tracker';
|
|
6
|
+
import { getAbsoluteUrl } from '../utility';
|
|
7
|
+
import { createNetworkSpan, completeNetworkSpan } from './span-helpers';
|
|
8
|
+
|
|
9
|
+
interface GlobalWithFetch {
|
|
10
|
+
fetch: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createStartContext(
|
|
14
|
+
input: unknown,
|
|
15
|
+
init?: unknown
|
|
16
|
+
): RequestStartContext {
|
|
17
|
+
const inputIsRequest = isRequest(input);
|
|
18
|
+
const url = inputIsRequest ? input.url : String(input);
|
|
19
|
+
const method =
|
|
20
|
+
(!!init && (init as RequestInit).method) ||
|
|
21
|
+
(inputIsRequest && input.method) ||
|
|
22
|
+
'GET';
|
|
23
|
+
return { url: getAbsoluteUrl(url), method, type: 'fetch' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRequest(input: unknown): input is Request {
|
|
27
|
+
return !!input && typeof input === 'object' && !(input instanceof URL);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let isFetchIntercepted = false;
|
|
31
|
+
let originalFetch: typeof fetch | null = null;
|
|
32
|
+
|
|
33
|
+
function createFetchRequestTracker(global: GlobalWithFetch): RequestTracker {
|
|
34
|
+
if (isFetchIntercepted) {
|
|
35
|
+
console.warn('[Pulse] Fetch already intercepted');
|
|
36
|
+
return new RequestTracker();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (originalFetch && global.fetch !== originalFetch) {
|
|
40
|
+
console.warn('[Pulse] Fetch already wrapped by another interceptor');
|
|
41
|
+
isFetchIntercepted = true;
|
|
42
|
+
return new RequestTracker();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ((global.fetch as any)?._pulseIntercepted) {
|
|
46
|
+
console.warn('[Pulse] Fetch already wrapped by Pulse');
|
|
47
|
+
isFetchIntercepted = true;
|
|
48
|
+
return new RequestTracker();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const requestTracker = new RequestTracker();
|
|
52
|
+
originalFetch = global.fetch;
|
|
53
|
+
isFetchIntercepted = true;
|
|
54
|
+
|
|
55
|
+
const fetchWrapper = function fetch(input: unknown, init?: unknown) {
|
|
56
|
+
const startContext = createStartContext(input, init);
|
|
57
|
+
|
|
58
|
+
const span = createNetworkSpan(startContext, 'fetch');
|
|
59
|
+
const { onRequestEnd } = requestTracker.start(startContext);
|
|
60
|
+
|
|
61
|
+
const fetchToCall = originalFetch!;
|
|
62
|
+
return fetchToCall
|
|
63
|
+
.call(global, input as RequestInfo, init as RequestInit)
|
|
64
|
+
.then((response) => {
|
|
65
|
+
// Determine if response is successful based on status code
|
|
66
|
+
// 2xx and 3xx are successful, 4xx and 5xx are errors
|
|
67
|
+
const isSuccess = response.status >= 200 && response.status < 400;
|
|
68
|
+
|
|
69
|
+
const endContext: RequestEndContext = {
|
|
70
|
+
status: response.status,
|
|
71
|
+
state: isSuccess ? 'success' : 'error',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
completeNetworkSpan(span, startContext, endContext, !isSuccess);
|
|
75
|
+
onRequestEnd(endContext);
|
|
76
|
+
return response;
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
const endContext: RequestEndContext = {
|
|
80
|
+
error,
|
|
81
|
+
state: 'error',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
completeNetworkSpan(span, startContext, endContext, true);
|
|
85
|
+
onRequestEnd(endContext);
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
(fetchWrapper as any)._pulseIntercepted = true;
|
|
91
|
+
global.fetch = fetchWrapper;
|
|
92
|
+
|
|
93
|
+
return requestTracker;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default createFetchRequestTracker;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RequestStartContext,
|
|
3
|
+
RequestEndContext,
|
|
4
|
+
} from './network.interface';
|
|
5
|
+
import { RequestTracker } from './request-tracker';
|
|
6
|
+
import { getAbsoluteUrl } from '../utility';
|
|
7
|
+
import type { Span } from '../index';
|
|
8
|
+
import { createNetworkSpan, completeNetworkSpan } from './span-helpers';
|
|
9
|
+
|
|
10
|
+
interface RequestData {
|
|
11
|
+
method: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ReadyStateChangeHandler = (this: XMLHttpRequest, ev: Event) => any;
|
|
16
|
+
|
|
17
|
+
let isXHRIntercepted = false;
|
|
18
|
+
|
|
19
|
+
function createXmlHttpRequestTracker(
|
|
20
|
+
xhr: typeof XMLHttpRequest
|
|
21
|
+
): RequestTracker {
|
|
22
|
+
if (isXHRIntercepted) {
|
|
23
|
+
console.warn('[Pulse] XMLHttpRequest already intercepted');
|
|
24
|
+
return new RequestTracker();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const requestTracker = new RequestTracker();
|
|
28
|
+
const trackedRequests = new WeakMap<XMLHttpRequest, RequestData>();
|
|
29
|
+
const trackedSpans = new WeakMap<XMLHttpRequest, Span>();
|
|
30
|
+
const requestHandlers = new WeakMap<
|
|
31
|
+
XMLHttpRequest,
|
|
32
|
+
ReadyStateChangeHandler
|
|
33
|
+
>();
|
|
34
|
+
|
|
35
|
+
const originalOpen = xhr.prototype.open;
|
|
36
|
+
xhr.prototype.open = function open(
|
|
37
|
+
method: string,
|
|
38
|
+
url: string | URL,
|
|
39
|
+
...rest: any[]
|
|
40
|
+
): void {
|
|
41
|
+
trackedRequests.set(this, {
|
|
42
|
+
method,
|
|
43
|
+
url: getAbsoluteUrl(String(url)),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// @ts-expect-error rest
|
|
47
|
+
originalOpen.call(this, method, url, ...rest);
|
|
48
|
+
};
|
|
49
|
+
isXHRIntercepted = true;
|
|
50
|
+
|
|
51
|
+
const originalSend = xhr.prototype.send;
|
|
52
|
+
xhr.prototype.send = function send(
|
|
53
|
+
body?: Document | XMLHttpRequestBodyInit | null
|
|
54
|
+
) {
|
|
55
|
+
const requestData = trackedRequests.get(this);
|
|
56
|
+
if (requestData) {
|
|
57
|
+
const existingHandler = requestHandlers.get(this);
|
|
58
|
+
if (existingHandler)
|
|
59
|
+
this.removeEventListener('readystatechange', existingHandler);
|
|
60
|
+
|
|
61
|
+
const startContext: RequestStartContext = {
|
|
62
|
+
type: 'xmlhttprequest',
|
|
63
|
+
method: requestData.method,
|
|
64
|
+
url: requestData.url,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const span = createNetworkSpan(startContext, 'xmlhttprequest');
|
|
68
|
+
trackedSpans.set(this, span);
|
|
69
|
+
const { onRequestEnd } = requestTracker.start(startContext);
|
|
70
|
+
|
|
71
|
+
const onReadyStateChange: ReadyStateChangeHandler = () => {
|
|
72
|
+
if (this.readyState === xhr.DONE && onRequestEnd) {
|
|
73
|
+
const activeSpan = trackedSpans.get(this);
|
|
74
|
+
|
|
75
|
+
// Determine request outcome based on status code
|
|
76
|
+
let endContext: RequestEndContext;
|
|
77
|
+
|
|
78
|
+
if (this.status <= 0 || this.status >= 400) {
|
|
79
|
+
endContext = { state: 'error', status: this.status };
|
|
80
|
+
} else {
|
|
81
|
+
endContext = { state: 'success', status: this.status };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (activeSpan) {
|
|
85
|
+
completeNetworkSpan(
|
|
86
|
+
activeSpan,
|
|
87
|
+
startContext,
|
|
88
|
+
endContext,
|
|
89
|
+
endContext.state === 'error'
|
|
90
|
+
);
|
|
91
|
+
trackedSpans.delete(this);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onRequestEnd(endContext);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.addEventListener('readystatechange', onReadyStateChange);
|
|
99
|
+
requestHandlers.set(this, onReadyStateChange);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
originalSend.call(this, body);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return requestTracker;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default createXmlHttpRequestTracker;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RequestStartContext,
|
|
3
|
+
RequestEndContext,
|
|
4
|
+
RequestStartCallback,
|
|
5
|
+
} from './network.interface';
|
|
6
|
+
|
|
7
|
+
export class RequestTracker {
|
|
8
|
+
private callbacks: RequestStartCallback[] = [];
|
|
9
|
+
|
|
10
|
+
onStart(startCallback: RequestStartCallback) {
|
|
11
|
+
this.callbacks.push(startCallback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
start(context: RequestStartContext) {
|
|
15
|
+
const results: Array<ReturnType<RequestStartCallback>> = [];
|
|
16
|
+
for (const startCallback of this.callbacks) {
|
|
17
|
+
const result = startCallback(context);
|
|
18
|
+
if (result) results.push(result);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
onRequestEnd: (endContext: RequestEndContext) => {
|
|
23
|
+
for (const result of results) {
|
|
24
|
+
if (result && result.onRequestEnd) {
|
|
25
|
+
result.onRequestEnd(endContext);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import type {
|
|
3
|
+
RequestStartContext,
|
|
4
|
+
RequestEndContext,
|
|
5
|
+
} from './network.interface';
|
|
6
|
+
import type { Span } from '../index';
|
|
7
|
+
import { Pulse, SpanStatusCode } from '../index';
|
|
8
|
+
import type { PulseAttributes } from '../pulse.interface';
|
|
9
|
+
import { extractHttpAttributes } from './url-helper';
|
|
10
|
+
|
|
11
|
+
export function setNetworkSpanAttributes(
|
|
12
|
+
span: Span,
|
|
13
|
+
startContext: RequestStartContext,
|
|
14
|
+
endContext: RequestEndContext
|
|
15
|
+
): PulseAttributes {
|
|
16
|
+
const method = startContext.method.toUpperCase();
|
|
17
|
+
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',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// We had implemented our own URL parsing helper to avoid errors on RN < 0.80. Since this is not supported by React Native.
|
|
26
|
+
// Check here: https://github.com/facebook/react-native/blob/v0.79.0/packages/react-native/Libraries/Blob/URL.js
|
|
27
|
+
const urlAttributes = extractHttpAttributes(startContext.url);
|
|
28
|
+
attributes = { ...attributes, ...urlAttributes };
|
|
29
|
+
|
|
30
|
+
if (endContext.status) {
|
|
31
|
+
attributes['http.status_code'] = endContext.status;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (endContext.state === 'error' && endContext.error) {
|
|
35
|
+
attributes.error = true;
|
|
36
|
+
attributes['error.message'] =
|
|
37
|
+
endContext.error.message || String(endContext.error);
|
|
38
|
+
if (endContext.error.stack) {
|
|
39
|
+
attributes['error.stack'] = endContext.error.stack;
|
|
40
|
+
}
|
|
41
|
+
span.recordException(endContext.error, attributes);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
span.setAttributes(attributes);
|
|
45
|
+
return attributes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createNetworkSpan(
|
|
49
|
+
startContext: RequestStartContext,
|
|
50
|
+
interceptorType: 'fetch' | 'xmlhttprequest'
|
|
51
|
+
): Span {
|
|
52
|
+
const method = startContext.method.toUpperCase();
|
|
53
|
+
const spanName = `HTTP ${method}`;
|
|
54
|
+
|
|
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
|
+
});
|
|
63
|
+
|
|
64
|
+
return span;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function completeNetworkSpan(
|
|
68
|
+
span: Span,
|
|
69
|
+
startContext: RequestStartContext,
|
|
70
|
+
endContext: RequestEndContext,
|
|
71
|
+
isError: boolean
|
|
72
|
+
): void {
|
|
73
|
+
try {
|
|
74
|
+
const attributes = setNetworkSpanAttributes(span, startContext, endContext);
|
|
75
|
+
console.log('[Pulse] Network span completed', {
|
|
76
|
+
spanId: span.spanId,
|
|
77
|
+
spanAttributes: attributes,
|
|
78
|
+
});
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error('[Pulse] Failed to set span attributes:', e);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
span.end(isError ? SpanStatusCode.ERROR : SpanStatusCode.UNSET);
|
|
84
|
+
}
|