@grafana/faro-react-native-tracing 1.0.0-alpha.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 +201 -0
- package/README.md +833 -0
- package/dist/cjs/exporters/faroTraceExporter.js +101 -0
- package/dist/cjs/exporters/faroTraceExporter.js.map +1 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js +99 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js.map +1 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/instrumentation.js +251 -0
- package/dist/cjs/instrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js +77 -0
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js.map +1 -0
- package/dist/cjs/instrumentations/instrumentationUtils.js +89 -0
- package/dist/cjs/instrumentations/instrumentationUtils.js.map +1 -0
- package/dist/cjs/processors/faroMetaAttributesSpanProcessor.js +59 -0
- package/dist/cjs/processors/faroMetaAttributesSpanProcessor.js.map +1 -0
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js +98 -0
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js.map +1 -0
- package/dist/cjs/semconv.js +29 -0
- package/dist/cjs/semconv.js.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils/sampler.js +21 -0
- package/dist/cjs/utils/sampler.js.map +1 -0
- package/dist/esm/exporters/faroTraceExporter.js +65 -0
- package/dist/esm/exporters/faroTraceExporter.js.map +1 -0
- package/dist/esm/exporters/faroTraceExporter.utils.js +88 -0
- package/dist/esm/exporters/faroTraceExporter.utils.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/instrumentation.js +183 -0
- package/dist/esm/instrumentation.js.map +1 -0
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js +62 -0
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js.map +1 -0
- package/dist/esm/instrumentations/instrumentationUtils.js +83 -0
- package/dist/esm/instrumentations/instrumentationUtils.js.map +1 -0
- package/dist/esm/processors/faroMetaAttributesSpanProcessor.js +54 -0
- package/dist/esm/processors/faroMetaAttributesSpanProcessor.js.map +1 -0
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js +93 -0
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js.map +1 -0
- package/dist/esm/semconv.js +26 -0
- package/dist/esm/semconv.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils/sampler.js +17 -0
- package/dist/esm/utils/sampler.js.map +1 -0
- package/dist/types/exporters/faroTraceExporter.d.ts +21 -0
- package/dist/types/exporters/faroTraceExporter.utils.d.ts +17 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/instrumentation.d.ts +46 -0
- package/dist/types/instrumentations/getDefaultOTELInstrumentations.d.ts +17 -0
- package/dist/types/instrumentations/instrumentationUtils.d.ts +26 -0
- package/dist/types/processors/faroMetaAttributesSpanProcessor.d.ts +22 -0
- package/dist/types/processors/httpRequestMonitorSpanProcessor.d.ts +18 -0
- package/dist/types/semconv.d.ts +19 -0
- package/dist/types/types.d.ts +31 -0
- package/dist/types/utils/sampler.d.ts +12 -0
- package/package.json +77 -0
- package/src/exporters/faroTraceExporter.test.ts +110 -0
- package/src/exporters/faroTraceExporter.ts +64 -0
- package/src/exporters/faroTraceExporter.utils.ts +105 -0
- package/src/index.ts +16 -0
- package/src/instrumentation.ts +241 -0
- package/src/instrumentations/getDefaultOTELInstrumentations.test.ts +70 -0
- package/src/instrumentations/getDefaultOTELInstrumentations.ts +83 -0
- package/src/instrumentations/instrumentationUtils.test.ts +107 -0
- package/src/instrumentations/instrumentationUtils.ts +106 -0
- package/src/processors/faroMetaAttributesSpanProcessor.test.ts +127 -0
- package/src/processors/faroMetaAttributesSpanProcessor.ts +71 -0
- package/src/processors/httpRequestMonitorSpanProcessor.ts +106 -0
- package/src/semconv.ts +31 -0
- package/src/types.ts +39 -0
- package/src/utils/sampler.test.ts +51 -0
- package/src/utils/sampler.ts +19 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { context, trace } from '@opentelemetry/api';
|
|
2
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
3
|
+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
4
|
+
import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';
|
|
5
|
+
import { BatchSpanProcessor, BasicTracerProvider as ReactNativeTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
6
|
+
import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
import {
|
|
8
|
+
ATTR_SERVICE_NAME,
|
|
9
|
+
ATTR_SERVICE_VERSION,
|
|
10
|
+
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
|
|
11
|
+
} from '@opentelemetry/semantic-conventions';
|
|
12
|
+
|
|
13
|
+
import { BaseInstrumentation, getInternalFaroFromGlobalObject, VERSION } from '@grafana/faro-core';
|
|
14
|
+
import type { Transport } from '@grafana/faro-core';
|
|
15
|
+
|
|
16
|
+
import { FaroTraceExporter } from './exporters/faroTraceExporter';
|
|
17
|
+
import { getDefaultOTELInstrumentations } from './instrumentations/getDefaultOTELInstrumentations';
|
|
18
|
+
import { FaroMetaAttributesSpanProcessor } from './processors/faroMetaAttributesSpanProcessor';
|
|
19
|
+
import { HttpRequestMonitorSpanProcessor } from './processors/httpRequestMonitorSpanProcessor';
|
|
20
|
+
import {
|
|
21
|
+
ATTR_APP_VERSION,
|
|
22
|
+
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
|
|
23
|
+
ATTR_DEVICE_BRAND,
|
|
24
|
+
ATTR_DEVICE_LOCALE,
|
|
25
|
+
ATTR_DEVICE_MODEL,
|
|
26
|
+
ATTR_DEVICE_OS_VERSION,
|
|
27
|
+
ATTR_DEVICE_PLATFORM,
|
|
28
|
+
ATTR_PROCESS_RUNTIME_NAME,
|
|
29
|
+
ATTR_PROCESS_RUNTIME_VERSION,
|
|
30
|
+
ATTR_SERVICE_NAMESPACE,
|
|
31
|
+
ATTR_TELEMETRY_DISTRO_NAME,
|
|
32
|
+
ATTR_TELEMETRY_DISTRO_VERSION,
|
|
33
|
+
} from './semconv';
|
|
34
|
+
import type { TracingInstrumentationOptions } from './types';
|
|
35
|
+
import { getSamplingDecision } from './utils/sampler';
|
|
36
|
+
|
|
37
|
+
// Import React Native TracerProvider
|
|
38
|
+
// Note: We use the base provider since React Native doesn't have a specific one
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* TracingInstrumentation for React Native
|
|
42
|
+
*
|
|
43
|
+
* Enables distributed tracing with OpenTelemetry for React Native applications.
|
|
44
|
+
*
|
|
45
|
+
* IMPORTANT: Infinite loop prevention
|
|
46
|
+
* - Uses internalLogger for debugging instead of console
|
|
47
|
+
* - Collector URLs are added to ignoreUrls in HTTP instrumentation
|
|
48
|
+
* - BatchSpanProcessor delays span export to avoid blocking
|
|
49
|
+
* - No console logging during trace export
|
|
50
|
+
*
|
|
51
|
+
* Example usage:
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { initializeFaro } from '@grafana/faro-react-native';
|
|
54
|
+
* import { TracingInstrumentation } from '@grafana/faro-react-native-tracing';
|
|
55
|
+
*
|
|
56
|
+
* initializeFaro({
|
|
57
|
+
* // ... other config
|
|
58
|
+
* instrumentations: [
|
|
59
|
+
* new TracingInstrumentation({
|
|
60
|
+
* propagateTraceHeaderCorsUrls: [/https:\\/\\/my-api\\.com/],
|
|
61
|
+
* }),
|
|
62
|
+
* ],
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class TracingInstrumentation extends BaseInstrumentation {
|
|
67
|
+
name = '@grafana/faro-react-native-tracing';
|
|
68
|
+
version = VERSION;
|
|
69
|
+
|
|
70
|
+
static SCHEDULED_BATCH_DELAY_MS = 1000;
|
|
71
|
+
|
|
72
|
+
private provider?: BasicTracerProvider;
|
|
73
|
+
|
|
74
|
+
constructor(private options: TracingInstrumentationOptions = {}) {
|
|
75
|
+
super();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
initialize(): void {
|
|
79
|
+
const options = this.options;
|
|
80
|
+
const attributes: Attributes = {};
|
|
81
|
+
|
|
82
|
+
// App attributes
|
|
83
|
+
if (this.config.app.name) {
|
|
84
|
+
attributes[ATTR_SERVICE_NAME] = this.config.app.name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this.config.app.namespace) {
|
|
88
|
+
attributes[ATTR_SERVICE_NAMESPACE] = this.config.app.namespace;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.config.app.version) {
|
|
92
|
+
attributes[ATTR_SERVICE_VERSION] = this.config.app.version;
|
|
93
|
+
attributes[ATTR_APP_VERSION] = this.config.app.version;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.config.app.environment) {
|
|
97
|
+
attributes[ATTR_DEPLOYMENT_ENVIRONMENT_NAME] = this.config.app.environment;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated will be removed in the future and has been replaced by ATTR_DEPLOYMENT_ENVIRONMENT_NAME (deployment.environment.name)
|
|
101
|
+
* We need to keep this for compatibility with some internal services for now.
|
|
102
|
+
*/
|
|
103
|
+
attributes[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT] = this.config.app.environment;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Device/Platform attributes from React Native
|
|
107
|
+
// Note: metas.value contains all meta providers, we need to check if device meta exists
|
|
108
|
+
const allMetas = this.metas.value as Record<string, unknown>;
|
|
109
|
+
const deviceMeta = allMetas['device'] as
|
|
110
|
+
| {
|
|
111
|
+
model?: string;
|
|
112
|
+
brand?: string;
|
|
113
|
+
osName?: string;
|
|
114
|
+
osVersion?: string;
|
|
115
|
+
locale?: string;
|
|
116
|
+
}
|
|
117
|
+
| undefined;
|
|
118
|
+
|
|
119
|
+
if (deviceMeta?.model) {
|
|
120
|
+
attributes[ATTR_DEVICE_MODEL] = deviceMeta.model;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (deviceMeta?.brand) {
|
|
124
|
+
attributes[ATTR_DEVICE_BRAND] = deviceMeta.brand;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (deviceMeta?.osName) {
|
|
128
|
+
attributes[ATTR_DEVICE_PLATFORM] = deviceMeta.osName;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (deviceMeta?.osVersion) {
|
|
132
|
+
attributes[ATTR_DEVICE_OS_VERSION] = deviceMeta.osVersion;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (deviceMeta?.locale) {
|
|
136
|
+
attributes[ATTR_DEVICE_LOCALE] = deviceMeta.locale;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
attributes[ATTR_PROCESS_RUNTIME_NAME] = 'react-native';
|
|
140
|
+
attributes[ATTR_PROCESS_RUNTIME_VERSION] = deviceMeta?.osVersion ?? 'unknown';
|
|
141
|
+
|
|
142
|
+
attributes[ATTR_TELEMETRY_DISTRO_NAME] = 'faro-react-native-sdk';
|
|
143
|
+
attributes[ATTR_TELEMETRY_DISTRO_VERSION] = VERSION;
|
|
144
|
+
|
|
145
|
+
// Merge with user-provided attributes
|
|
146
|
+
Object.assign(attributes, options.resourceAttributes);
|
|
147
|
+
|
|
148
|
+
const resource = defaultResource().merge(resourceFromAttributes(attributes));
|
|
149
|
+
|
|
150
|
+
// Create tracer provider with span processors
|
|
151
|
+
this.provider = new ReactNativeTracerProvider({
|
|
152
|
+
resource,
|
|
153
|
+
sampler: {
|
|
154
|
+
shouldSample: () => {
|
|
155
|
+
return {
|
|
156
|
+
decision: getSamplingDecision(this.api.getSession()),
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
spanProcessors: [
|
|
161
|
+
options.spanProcessor ??
|
|
162
|
+
new HttpRequestMonitorSpanProcessor(
|
|
163
|
+
new FaroMetaAttributesSpanProcessor(
|
|
164
|
+
new BatchSpanProcessor(new FaroTraceExporter({ api: this.api }), {
|
|
165
|
+
scheduledDelayMillis: TracingInstrumentation.SCHEDULED_BATCH_DELAY_MS,
|
|
166
|
+
maxExportBatchSize: 30,
|
|
167
|
+
}),
|
|
168
|
+
this.metas
|
|
169
|
+
)
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Register the provider as the global tracer provider
|
|
175
|
+
// This is CRITICAL for the tracer to generate real trace IDs instead of all zeros
|
|
176
|
+
trace.setGlobalTracerProvider(this.provider);
|
|
177
|
+
|
|
178
|
+
const { propagateTraceHeaderCorsUrls, fetchInstrumentationOptions, xhrInstrumentationOptions } =
|
|
179
|
+
this.options.instrumentationOptions ?? {};
|
|
180
|
+
|
|
181
|
+
// Get ignore URLs from transports to prevent infinite loops
|
|
182
|
+
const ignoreUrls = this.getIgnoreUrls();
|
|
183
|
+
|
|
184
|
+
// Register instrumentations
|
|
185
|
+
registerInstrumentations({
|
|
186
|
+
instrumentations:
|
|
187
|
+
options.instrumentations ??
|
|
188
|
+
getDefaultOTELInstrumentations({
|
|
189
|
+
ignoreUrls,
|
|
190
|
+
propagateTraceHeaderCorsUrls,
|
|
191
|
+
fetchInstrumentationOptions,
|
|
192
|
+
xhrInstrumentationOptions,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Expose OTEL API on the global Faro instance for manual span creation
|
|
197
|
+
// This allows users to access trace and context APIs via faro.otel
|
|
198
|
+
const globalFaroInstance = getInternalFaroFromGlobalObject();
|
|
199
|
+
if (globalFaroInstance) {
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Extending Faro instance with OTEL API
|
|
201
|
+
(globalFaroInstance as any).otel = {
|
|
202
|
+
trace,
|
|
203
|
+
context,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get ignore URLs from all transports to avoid tracing collector requests
|
|
210
|
+
* CRITICAL: This prevents infinite loops where trace exports trigger more traces
|
|
211
|
+
*/
|
|
212
|
+
private getIgnoreUrls(): Array<string | RegExp> {
|
|
213
|
+
// Get URLs from transports' getIgnoreUrls() method
|
|
214
|
+
const transportUrls = this.transports.transports.flatMap((transport: Transport) => {
|
|
215
|
+
return transport.getIgnoreUrls();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Create regex patterns that match both with and without trailing slashes
|
|
219
|
+
// This is critical because fetch() might add trailing slashes
|
|
220
|
+
const regexPatterns = transportUrls.map((url) => {
|
|
221
|
+
if (typeof url === 'string') {
|
|
222
|
+
// Escape special regex characters and make trailing slash optional
|
|
223
|
+
const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
224
|
+
return new RegExp(`^${escapedUrl}/?$`);
|
|
225
|
+
}
|
|
226
|
+
return url;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Return both original URLs and regex patterns for maximum coverage
|
|
230
|
+
return [...transportUrls, ...regexPatterns];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Shutdown the tracer provider
|
|
235
|
+
*/
|
|
236
|
+
async shutdown(): Promise<void> {
|
|
237
|
+
if (this.provider) {
|
|
238
|
+
await this.provider.shutdown();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
2
|
+
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
3
|
+
|
|
4
|
+
import { getDefaultOTELInstrumentations } from './getDefaultOTELInstrumentations';
|
|
5
|
+
|
|
6
|
+
describe('getDefaultOTELInstrumentations', () => {
|
|
7
|
+
it('should return an array with FetchInstrumentation and XMLHttpRequestInstrumentation', () => {
|
|
8
|
+
const instrumentations = getDefaultOTELInstrumentations();
|
|
9
|
+
|
|
10
|
+
expect(instrumentations).toHaveLength(2);
|
|
11
|
+
expect(instrumentations[0]).toBeInstanceOf(FetchInstrumentation);
|
|
12
|
+
expect(instrumentations[1]).toBeInstanceOf(XMLHttpRequestInstrumentation);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should configure ignoreUrls', () => {
|
|
16
|
+
const ignoreUrls = [/test-url/];
|
|
17
|
+
const instrumentations = getDefaultOTELInstrumentations({ ignoreUrls });
|
|
18
|
+
|
|
19
|
+
expect(instrumentations).toHaveLength(2);
|
|
20
|
+
// FetchInstrumentation should have ignoreUrls configured
|
|
21
|
+
expect((instrumentations[0] as any)._config.ignoreUrls).toEqual(ignoreUrls);
|
|
22
|
+
// XMLHttpRequestInstrumentation should have ignoreUrls configured
|
|
23
|
+
expect((instrumentations[1] as any)._config.ignoreUrls).toEqual(ignoreUrls);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should set ignoreNetworkEvents to true by default', () => {
|
|
27
|
+
const instrumentations = getDefaultOTELInstrumentations();
|
|
28
|
+
|
|
29
|
+
expect((instrumentations[0] as any)._config.ignoreNetworkEvents).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should allow overriding ignoreNetworkEvents', () => {
|
|
33
|
+
const instrumentations = getDefaultOTELInstrumentations({
|
|
34
|
+
fetchInstrumentationOptions: {
|
|
35
|
+
ignoreNetworkEvents: false,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect((instrumentations[0] as any)._config.ignoreNetworkEvents).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should configure propagateTraceHeaderCorsUrls', () => {
|
|
43
|
+
const propagateUrls = [/api\.example\.com/];
|
|
44
|
+
const instrumentations = getDefaultOTELInstrumentations({
|
|
45
|
+
propagateTraceHeaderCorsUrls: propagateUrls,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect((instrumentations[0] as any)._config.propagateTraceHeaderCorsUrls).toEqual(propagateUrls);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should apply custom attribute function', () => {
|
|
52
|
+
const customFn = jest.fn();
|
|
53
|
+
const instrumentations = getDefaultOTELInstrumentations({
|
|
54
|
+
fetchInstrumentationOptions: {
|
|
55
|
+
applyCustomAttributesOnSpan: customFn,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(instrumentations).toHaveLength(2);
|
|
60
|
+
// Custom function should be wrapped by our defaults
|
|
61
|
+
expect((instrumentations[0] as any)._config.applyCustomAttributesOnSpan).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should have a requestHook defined', () => {
|
|
65
|
+
const instrumentations = getDefaultOTELInstrumentations();
|
|
66
|
+
|
|
67
|
+
expect((instrumentations[0] as any)._config.requestHook).toBeDefined();
|
|
68
|
+
expect(typeof (instrumentations[0] as any)._config.requestHook).toBe('function');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api';
|
|
2
|
+
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
3
|
+
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
4
|
+
|
|
5
|
+
import { faro, type UserActionInternalInterface, UserActionState } from '@grafana/faro-core';
|
|
6
|
+
|
|
7
|
+
import type { DefaultInstrumentationsOptions, InstrumentationOption } from '../types';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
fetchCustomAttributeFunctionWithDefaults,
|
|
11
|
+
xhrCustomAttributeFunctionWithDefaults,
|
|
12
|
+
} from './instrumentationUtils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get default OTEL instrumentations for React Native
|
|
16
|
+
*
|
|
17
|
+
* This function creates the default OpenTelemetry instrumentations for React Native:
|
|
18
|
+
* - FetchInstrumentation: Traces fetch() API calls
|
|
19
|
+
* - XMLHttpRequestInstrumentation: Traces XMLHttpRequest and axios (which uses XHR)
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT: Infinite loop prevention
|
|
22
|
+
* - ignoreUrls is used to exclude Faro collector URLs
|
|
23
|
+
* - ignoreNetworkEvents is true to avoid duplicate events
|
|
24
|
+
* - No console logging during instrumentation
|
|
25
|
+
*
|
|
26
|
+
* @param options - Configuration options
|
|
27
|
+
* @returns Array of OTEL instrumentations
|
|
28
|
+
*/
|
|
29
|
+
export function getDefaultOTELInstrumentations(options: DefaultInstrumentationsOptions = {}): InstrumentationOption[] {
|
|
30
|
+
const { fetchInstrumentationOptions, xhrInstrumentationOptions, ...sharedOptions } = options;
|
|
31
|
+
|
|
32
|
+
const fetchOpts = createFetchInstrumentationOptions(fetchInstrumentationOptions, sharedOptions);
|
|
33
|
+
const xhrOpts = createXhrInstrumentationOptions(xhrInstrumentationOptions, sharedOptions);
|
|
34
|
+
|
|
35
|
+
return [new FetchInstrumentation(fetchOpts), new XMLHttpRequestInstrumentation(xhrOpts)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createFetchInstrumentationOptions(
|
|
39
|
+
fetchInstrumentationOptions: DefaultInstrumentationsOptions['fetchInstrumentationOptions'],
|
|
40
|
+
sharedOptions: Record<string, unknown>
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
...sharedOptions,
|
|
44
|
+
// Ignore network performance events to avoid duplicates
|
|
45
|
+
ignoreNetworkEvents: true,
|
|
46
|
+
// Keep this here to overwrite the defaults above if provided by the users
|
|
47
|
+
...fetchInstrumentationOptions,
|
|
48
|
+
// Always keep this function
|
|
49
|
+
applyCustomAttributesOnSpan: fetchCustomAttributeFunctionWithDefaults(
|
|
50
|
+
fetchInstrumentationOptions?.applyCustomAttributesOnSpan
|
|
51
|
+
),
|
|
52
|
+
// Request hook to add user action context
|
|
53
|
+
requestHook: (span: Span, _: Request | RequestInit) => {
|
|
54
|
+
try {
|
|
55
|
+
const currentAction = faro.api.getActiveUserAction();
|
|
56
|
+
if (
|
|
57
|
+
currentAction &&
|
|
58
|
+
(currentAction as unknown as UserActionInternalInterface)?.getState() === UserActionState.Started
|
|
59
|
+
) {
|
|
60
|
+
span.setAttribute('faro.action.user.name', currentAction.name);
|
|
61
|
+
span.setAttribute('faro.action.user.parentId', currentAction.parentId);
|
|
62
|
+
}
|
|
63
|
+
} catch (_error) {
|
|
64
|
+
// Silently fail - don't log to avoid infinite loops
|
|
65
|
+
// The span will just not have user action context
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createXhrInstrumentationOptions(
|
|
72
|
+
xhrInstrumentationOptions: DefaultInstrumentationsOptions['xhrInstrumentationOptions'],
|
|
73
|
+
sharedOptions: Record<string, unknown>
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
...sharedOptions,
|
|
77
|
+
ignoreNetworkEvents: true,
|
|
78
|
+
...xhrInstrumentationOptions,
|
|
79
|
+
applyCustomAttributesOnSpan: xhrCustomAttributeFunctionWithDefaults(
|
|
80
|
+
xhrInstrumentationOptions?.applyCustomAttributesOnSpan
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
|
|
4
|
+
import { fetchCustomAttributeFunctionWithDefaults, setSpanStatusOnFetchError } from './instrumentationUtils';
|
|
5
|
+
|
|
6
|
+
describe('instrumentationUtils', () => {
|
|
7
|
+
let provider: BasicTracerProvider;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
provider = new BasicTracerProvider();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('setSpanStatusOnFetchError', () => {
|
|
14
|
+
it('should set span status to ERROR with error message', () => {
|
|
15
|
+
const tracer = provider.getTracer('test');
|
|
16
|
+
const span = tracer.startSpan('test-span');
|
|
17
|
+
const setStatusSpy = jest.spyOn(span, 'setStatus');
|
|
18
|
+
const error = new Error('Test error');
|
|
19
|
+
|
|
20
|
+
setSpanStatusOnFetchError(span, error);
|
|
21
|
+
|
|
22
|
+
expect(setStatusSpy).toHaveBeenCalledWith({
|
|
23
|
+
code: SpanStatusCode.ERROR,
|
|
24
|
+
message: 'Test error',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should set span status to ERROR with string message', () => {
|
|
29
|
+
const tracer = provider.getTracer('test');
|
|
30
|
+
const span = tracer.startSpan('test-span');
|
|
31
|
+
const setStatusSpy = jest.spyOn(span, 'setStatus');
|
|
32
|
+
|
|
33
|
+
setSpanStatusOnFetchError(span, 'String error message');
|
|
34
|
+
|
|
35
|
+
expect(setStatusSpy).toHaveBeenCalledWith({
|
|
36
|
+
code: SpanStatusCode.ERROR,
|
|
37
|
+
message: 'String error message',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('fetchCustomAttributeFunctionWithDefaults', () => {
|
|
43
|
+
it('should call user function if provided', () => {
|
|
44
|
+
const userFunction = jest.fn();
|
|
45
|
+
const fn = fetchCustomAttributeFunctionWithDefaults(userFunction);
|
|
46
|
+
|
|
47
|
+
const tracer = provider.getTracer('test');
|
|
48
|
+
const span = tracer.startSpan('test-span');
|
|
49
|
+
const request = new Request('https://example.com');
|
|
50
|
+
const response = new Response();
|
|
51
|
+
|
|
52
|
+
fn(span, request, response);
|
|
53
|
+
|
|
54
|
+
expect(userFunction).toHaveBeenCalledWith(span, request, response);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should set error status when result is an Error', () => {
|
|
58
|
+
const fn = fetchCustomAttributeFunctionWithDefaults();
|
|
59
|
+
|
|
60
|
+
const tracer = provider.getTracer('test');
|
|
61
|
+
const span = tracer.startSpan('test-span');
|
|
62
|
+
const setStatusSpy = jest.spyOn(span, 'setStatus');
|
|
63
|
+
const request = new Request('https://example.com');
|
|
64
|
+
const error = new Error('Fetch failed');
|
|
65
|
+
|
|
66
|
+
fn(span, request, error as any);
|
|
67
|
+
|
|
68
|
+
expect(setStatusSpy).toHaveBeenCalledWith({
|
|
69
|
+
code: SpanStatusCode.ERROR,
|
|
70
|
+
message: 'Fetch failed',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should not set error status for successful Response', () => {
|
|
75
|
+
const fn = fetchCustomAttributeFunctionWithDefaults();
|
|
76
|
+
|
|
77
|
+
const tracer = provider.getTracer('test');
|
|
78
|
+
const span = tracer.startSpan('test-span');
|
|
79
|
+
const setStatusSpy = jest.spyOn(span, 'setStatus');
|
|
80
|
+
const request = new Request('https://example.com');
|
|
81
|
+
const response = new Response();
|
|
82
|
+
|
|
83
|
+
fn(span, request, response);
|
|
84
|
+
|
|
85
|
+
expect(setStatusSpy).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should work with both user function and default error handling', () => {
|
|
89
|
+
const userFunction = jest.fn();
|
|
90
|
+
const fn = fetchCustomAttributeFunctionWithDefaults(userFunction);
|
|
91
|
+
|
|
92
|
+
const tracer = provider.getTracer('test');
|
|
93
|
+
const span = tracer.startSpan('test-span');
|
|
94
|
+
const setStatusSpy = jest.spyOn(span, 'setStatus');
|
|
95
|
+
const request = new Request('https://example.com');
|
|
96
|
+
const error = new Error('Combined test error');
|
|
97
|
+
|
|
98
|
+
fn(span, request, error as any);
|
|
99
|
+
|
|
100
|
+
expect(userFunction).toHaveBeenCalledWith(span, request, error);
|
|
101
|
+
expect(setStatusSpy).toHaveBeenCalledWith({
|
|
102
|
+
code: SpanStatusCode.ERROR,
|
|
103
|
+
message: 'Combined test error',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import type { Span } from '@opentelemetry/api';
|
|
3
|
+
import type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';
|
|
4
|
+
import type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';
|
|
5
|
+
|
|
6
|
+
import { faro, type UserActionInternalInterface, UserActionState } from '@grafana/faro-core';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FetchError interface matching OpenTelemetry's internal type
|
|
10
|
+
* Note: FetchError is not exported from @opentelemetry/instrumentation-fetch
|
|
11
|
+
*/
|
|
12
|
+
interface FetchError {
|
|
13
|
+
status?: number;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set span status to ERROR when fetch fails
|
|
19
|
+
*
|
|
20
|
+
* This ensures that failed HTTP requests are marked as errors in traces.
|
|
21
|
+
*/
|
|
22
|
+
export function setSpanStatusOnFetchError(span: Span, error: Error | string): void {
|
|
23
|
+
const message = typeof error === 'string' ? error : error.message;
|
|
24
|
+
span.setStatus({
|
|
25
|
+
code: SpanStatusCode.ERROR,
|
|
26
|
+
message,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to check if result is a FetchError
|
|
32
|
+
*/
|
|
33
|
+
function isFetchError(result: Response | FetchError): result is FetchError {
|
|
34
|
+
return 'message' in result && typeof (result as FetchError).message === 'string';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom attribute function for fetch instrumentation with defaults
|
|
39
|
+
*
|
|
40
|
+
* Combines user-provided custom attributes with default handling.
|
|
41
|
+
*
|
|
42
|
+
* @param userFunction - Optional user-provided custom attribute function
|
|
43
|
+
* @returns Combined custom attribute function
|
|
44
|
+
*/
|
|
45
|
+
export function fetchCustomAttributeFunctionWithDefaults(
|
|
46
|
+
userFunction?: FetchCustomAttributeFunction
|
|
47
|
+
): FetchCustomAttributeFunction {
|
|
48
|
+
const fn: FetchCustomAttributeFunction = (
|
|
49
|
+
span: Span,
|
|
50
|
+
request: Request | RequestInit,
|
|
51
|
+
result: Response | FetchError
|
|
52
|
+
) => {
|
|
53
|
+
// Call user function first if provided
|
|
54
|
+
if (userFunction) {
|
|
55
|
+
userFunction(span, request, result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add default error handling
|
|
59
|
+
if (isFetchError(result)) {
|
|
60
|
+
setSpanStatusOnFetchError(span, result.message);
|
|
61
|
+
} else if (result instanceof Response && result.status >= 400 && result.status < 600) {
|
|
62
|
+
setSpanStatusOnFetchError(span, `HTTP ${result.status}: ${result.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return fn;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set span status to ERROR for XHR failures (status 0 or 4xx/5xx).
|
|
70
|
+
*/
|
|
71
|
+
export function setSpanStatusOnXMLHttpRequestError(span: Span, xhr: XMLHttpRequest): void {
|
|
72
|
+
const status = xhr.status;
|
|
73
|
+
if (status == null) return;
|
|
74
|
+
if (status === 0 || (status >= 400 && status < 600)) {
|
|
75
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add user action context to span when active (for HTTP Errors column in user action table).
|
|
81
|
+
*/
|
|
82
|
+
function addUserActionContextToSpan(span: Span): void {
|
|
83
|
+
try {
|
|
84
|
+
const currentAction = faro.api?.getActiveUserAction?.();
|
|
85
|
+
const state = (currentAction as unknown as UserActionInternalInterface)?.getState?.();
|
|
86
|
+
if (currentAction && (state === UserActionState.Started || state === UserActionState.Halted)) {
|
|
87
|
+
span.setAttribute('faro.action.user.name', currentAction.name);
|
|
88
|
+
span.setAttribute('faro.action.user.parentId', currentAction.parentId);
|
|
89
|
+
}
|
|
90
|
+
} catch (_) {
|
|
91
|
+
// Silently fail - don't log to avoid instrumentation loops
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Custom attribute function for XHR instrumentation with defaults.
|
|
97
|
+
*/
|
|
98
|
+
export function xhrCustomAttributeFunctionWithDefaults(
|
|
99
|
+
userFunction?: XHRCustomAttributeFunction
|
|
100
|
+
): XHRCustomAttributeFunction {
|
|
101
|
+
return (span: Span, xhr: XMLHttpRequest) => {
|
|
102
|
+
setSpanStatusOnXMLHttpRequestError(span, xhr);
|
|
103
|
+
addUserActionContextToSpan(span);
|
|
104
|
+
userFunction?.(span, xhr);
|
|
105
|
+
};
|
|
106
|
+
}
|