@grafana/faro-react-native-tracing 1.3.1 → 2.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/README.md +33 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js +33 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js.map +1 -1
- package/dist/cjs/instrumentation.js +259 -44
- package/dist/cjs/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js +28 -20
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js.map +1 -1
- package/dist/cjs/instrumentations/instrumentationUtils.js +15 -0
- package/dist/cjs/instrumentations/instrumentationUtils.js.map +1 -1
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js +27 -13
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/exporters/faroTraceExporter.utils.js +32 -0
- package/dist/esm/exporters/faroTraceExporter.utils.js.map +1 -1
- package/dist/esm/instrumentation.js +237 -43
- package/dist/esm/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js +27 -21
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js.map +1 -1
- package/dist/esm/instrumentations/instrumentationUtils.js +14 -0
- package/dist/esm/instrumentations/instrumentationUtils.js.map +1 -1
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js +27 -13
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/types/instrumentation.d.ts +12 -4
- package/dist/types/instrumentations/getDefaultOTELInstrumentations.d.ts +2 -0
- package/dist/types/instrumentations/instrumentationUtils.d.ts +2 -0
- package/dist/types/types.d.ts +4 -0
- package/package.json +3 -3
- package/src/exporters/faroTraceExporter.events.test.ts +90 -0
- package/src/exporters/faroTraceExporter.utils.ts +32 -0
- package/src/httpSemantics.test.ts +300 -0
- package/src/instrumentation.test.ts +740 -0
- package/src/instrumentation.ts +299 -67
- package/src/instrumentations/getDefaultOTELInstrumentations.test.ts +28 -1
- package/src/instrumentations/getDefaultOTELInstrumentations.ts +41 -32
- package/src/instrumentations/instrumentationUtils.ts +13 -0
- package/src/processors/httpRequestMonitorSpanProcessor.ts +27 -15
- package/src/types.ts +4 -0
package/src/instrumentation.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { context, propagation, trace } from '@opentelemetry/api';
|
|
2
|
-
import type { Attributes } from '@opentelemetry/api';
|
|
2
|
+
import type { Attributes, Context, ContextManager } from '@opentelemetry/api';
|
|
3
3
|
import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
4
4
|
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
5
|
+
import type { Instrumentation } from '@opentelemetry/instrumentation';
|
|
5
6
|
import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';
|
|
6
7
|
import { BatchSpanProcessor, BasicTracerProvider as ReactNativeTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
7
|
-
import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
8
|
+
import type { BasicTracerProvider, ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
8
9
|
import { StackContextManager } from '@opentelemetry/sdk-trace-web';
|
|
9
10
|
import {
|
|
10
11
|
ATTR_SERVICE_NAME,
|
|
@@ -13,11 +14,14 @@ import {
|
|
|
13
14
|
} from '@opentelemetry/semantic-conventions';
|
|
14
15
|
|
|
15
16
|
import { BaseInstrumentation, getInternalFaroFromGlobalObject, VERSION } from '@grafana/faro-core';
|
|
16
|
-
import type { Transport } from '@grafana/faro-core';
|
|
17
|
+
import type { Faro, OTELApi, Transport } from '@grafana/faro-core';
|
|
17
18
|
|
|
18
19
|
import { FaroTraceExporter } from './exporters/faroTraceExporter';
|
|
19
20
|
import { getReactNativeDevServerIgnoreUrls } from './instrumentations/devServerIgnoreUrls';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getDefaultOTELInstrumentations,
|
|
23
|
+
updateDefaultOTELInstrumentations,
|
|
24
|
+
} from './instrumentations/getDefaultOTELInstrumentations';
|
|
21
25
|
import { FaroMetaAttributesSpanProcessor } from './processors/faroMetaAttributesSpanProcessor';
|
|
22
26
|
import { HttpRequestMonitorSpanProcessor } from './processors/httpRequestMonitorSpanProcessor';
|
|
23
27
|
import {
|
|
@@ -40,6 +44,53 @@ import { getSamplingDecision } from './utils/sampler';
|
|
|
40
44
|
// Import React Native TracerProvider
|
|
41
45
|
// Note: We use the base provider since React Native doesn't have a specific one
|
|
42
46
|
|
|
47
|
+
type FaroWithOtel = Faro & { otel?: unknown };
|
|
48
|
+
const detachedInstrumentations = new WeakSet<Instrumentation>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Keeps a caller-supplied processor reusable across Faro remove/add cycles.
|
|
52
|
+
* Explicit TracingInstrumentation.shutdown() remains responsible for the
|
|
53
|
+
* processor's terminal shutdown.
|
|
54
|
+
*/
|
|
55
|
+
class BorrowedSpanProcessor implements SpanProcessor {
|
|
56
|
+
constructor(private readonly processor: SpanProcessor) {}
|
|
57
|
+
|
|
58
|
+
forceFlush(): Promise<void> {
|
|
59
|
+
return this.processor.forceFlush();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onStart(span: Span, parentContext: Context): void {
|
|
63
|
+
this.processor.onStart(span, parentContext);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onEnding(span: Span): void {
|
|
67
|
+
this.processor.onEnding?.(span);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onEnd(span: ReadableSpan): void {
|
|
71
|
+
this.processor.onEnd(span);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
shutdown(): Promise<void> {
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TracingRegistration {
|
|
80
|
+
contextManager?: ContextManager;
|
|
81
|
+
faro?: FaroWithOtel;
|
|
82
|
+
faroOtel?: OTELApi;
|
|
83
|
+
instrumentations: Instrumentation[];
|
|
84
|
+
ownsContextManager: boolean;
|
|
85
|
+
ownsContextManagerInstance: boolean;
|
|
86
|
+
ownsPropagator: boolean;
|
|
87
|
+
ownsTracerProvider: boolean;
|
|
88
|
+
previousFaroOtel?: unknown;
|
|
89
|
+
provider: BasicTracerProvider;
|
|
90
|
+
shutdownPromise?: Promise<void>;
|
|
91
|
+
unregisterInstrumentations?: VoidFunction;
|
|
92
|
+
}
|
|
93
|
+
|
|
43
94
|
/**
|
|
44
95
|
* TracingInstrumentation for React Native
|
|
45
96
|
*
|
|
@@ -72,13 +123,18 @@ export class TracingInstrumentation extends BaseInstrumentation {
|
|
|
72
123
|
|
|
73
124
|
static SCHEDULED_BATCH_DELAY_MS = 1000;
|
|
74
125
|
|
|
75
|
-
private
|
|
126
|
+
private activeRegistration?: TracingRegistration;
|
|
127
|
+
private defaultInstrumentations?: Instrumentation[];
|
|
128
|
+
private pendingShutdowns = new Set<Promise<void>>();
|
|
129
|
+
private suppliedSpanProcessorShutdown?: Promise<void>;
|
|
76
130
|
|
|
77
131
|
constructor(private options: TracingInstrumentationOptions = {}) {
|
|
78
132
|
super();
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
initialize(): void {
|
|
136
|
+
this.destroy();
|
|
137
|
+
|
|
82
138
|
const options = this.options;
|
|
83
139
|
const attributes: Attributes = {};
|
|
84
140
|
|
|
@@ -151,7 +207,7 @@ export class TracingInstrumentation extends BaseInstrumentation {
|
|
|
151
207
|
const resource = defaultResource().merge(resourceFromAttributes(attributes));
|
|
152
208
|
|
|
153
209
|
// Create tracer provider with span processors
|
|
154
|
-
|
|
210
|
+
const provider = new ReactNativeTracerProvider({
|
|
155
211
|
resource,
|
|
156
212
|
sampler: {
|
|
157
213
|
shouldSample: () => {
|
|
@@ -161,7 +217,7 @@ export class TracingInstrumentation extends BaseInstrumentation {
|
|
|
161
217
|
},
|
|
162
218
|
},
|
|
163
219
|
spanProcessors: [
|
|
164
|
-
options.spanProcessor ??
|
|
220
|
+
(options.spanProcessor && new BorrowedSpanProcessor(options.spanProcessor)) ??
|
|
165
221
|
new HttpRequestMonitorSpanProcessor(
|
|
166
222
|
new FaroMetaAttributesSpanProcessor(
|
|
167
223
|
new BatchSpanProcessor(new FaroTraceExporter({ api: this.api }), {
|
|
@@ -174,62 +230,102 @@ export class TracingInstrumentation extends BaseInstrumentation {
|
|
|
174
230
|
],
|
|
175
231
|
});
|
|
176
232
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
const registration: TracingRegistration = {
|
|
234
|
+
instrumentations: [],
|
|
235
|
+
ownsContextManager: false,
|
|
236
|
+
ownsContextManagerInstance: false,
|
|
237
|
+
ownsPropagator: false,
|
|
238
|
+
ownsTracerProvider: false,
|
|
239
|
+
provider,
|
|
240
|
+
};
|
|
241
|
+
this.activeRegistration = registration;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Register the provider as the global tracer provider
|
|
245
|
+
// This is CRITICAL for the tracer to generate real trace IDs instead of all zeros
|
|
246
|
+
if (!trace.setGlobalTracerProvider(provider)) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Unable to register the Faro OpenTelemetry tracer provider. Remove the existing global tracer provider before initializing Faro tracing.'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
registration.ownsTracerProvider = true;
|
|
252
|
+
|
|
253
|
+
// Register a global ContextManager. Without one, OTel falls back to the NoopContextManager,
|
|
254
|
+
// which always returns ROOT_CONTEXT — so when `@opentelemetry/instrumentation-fetch` does
|
|
255
|
+
// `context.with(setSpan(active(), createdSpan), () => _addHeaders(...))` the span set on the
|
|
256
|
+
// wrapped context is invisible inside `_addHeaders` and `propagation.inject` writes nothing.
|
|
257
|
+
// `StackContextManager` is pure JS (no DOM/Zone deps) and works in React Native.
|
|
258
|
+
const contextManager = (options.contextManager ?? new StackContextManager()).enable();
|
|
259
|
+
registration.contextManager = contextManager;
|
|
260
|
+
registration.ownsContextManagerInstance = options.contextManager == null;
|
|
261
|
+
registration.ownsContextManager = context.setGlobalContextManager(contextManager);
|
|
262
|
+
|
|
263
|
+
// Register the global text-map propagator. Without this, OTel falls back to a
|
|
264
|
+
// NoopTextMapPropagator and `propagation.inject(...)` becomes a no-op, meaning
|
|
265
|
+
// `traceparent` / `tracestate` (and `baggage`) headers are never written on the
|
|
266
|
+
// outbound fetch/XHR — so the backend receives no context and starts a new trace.
|
|
267
|
+
registration.ownsPropagator = propagation.setGlobalPropagator(
|
|
268
|
+
options.propagator ??
|
|
269
|
+
new CompositePropagator({
|
|
270
|
+
propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const {
|
|
275
|
+
enableFetchInstrumentation,
|
|
276
|
+
enableXhrInstrumentation,
|
|
277
|
+
propagateTraceHeaderCorsUrls,
|
|
278
|
+
fetchInstrumentationOptions,
|
|
279
|
+
xhrInstrumentationOptions,
|
|
280
|
+
} = this.options.instrumentationOptions ?? {};
|
|
281
|
+
|
|
282
|
+
// Get ignore URLs from transports to prevent infinite loops
|
|
283
|
+
const ignoreUrls = this.getIgnoreUrls();
|
|
284
|
+
|
|
285
|
+
const defaultInstrumentationOptions = {
|
|
286
|
+
ignoreUrls,
|
|
287
|
+
enableFetchInstrumentation,
|
|
288
|
+
enableXhrInstrumentation,
|
|
289
|
+
propagateTraceHeaderCorsUrls,
|
|
290
|
+
fetchInstrumentationOptions,
|
|
291
|
+
xhrInstrumentationOptions,
|
|
232
292
|
};
|
|
293
|
+
|
|
294
|
+
if (options.instrumentations) {
|
|
295
|
+
registration.instrumentations = options.instrumentations.flat();
|
|
296
|
+
} else if (this.defaultInstrumentations) {
|
|
297
|
+
updateDefaultOTELInstrumentations(this.defaultInstrumentations, defaultInstrumentationOptions);
|
|
298
|
+
registration.instrumentations = this.defaultInstrumentations;
|
|
299
|
+
} else {
|
|
300
|
+
this.defaultInstrumentations = getDefaultOTELInstrumentations(defaultInstrumentationOptions).flat();
|
|
301
|
+
registration.instrumentations = this.defaultInstrumentations;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const instrumentationsNeedingExplicitEnable = registration.instrumentations.filter(
|
|
305
|
+
(instrumentation) =>
|
|
306
|
+
detachedInstrumentations.has(instrumentation) && instrumentation.getConfig().enabled === true
|
|
307
|
+
);
|
|
308
|
+
registration.unregisterInstrumentations = registerInstrumentations({
|
|
309
|
+
instrumentations: registration.instrumentations,
|
|
310
|
+
});
|
|
311
|
+
registration.instrumentations.forEach((instrumentation) => {
|
|
312
|
+
detachedInstrumentations.delete(instrumentation);
|
|
313
|
+
});
|
|
314
|
+
instrumentationsNeedingExplicitEnable.forEach((instrumentation) => instrumentation.enable());
|
|
315
|
+
|
|
316
|
+
// Expose OTEL API on the global Faro instance for manual span creation
|
|
317
|
+
// This allows users to access trace and context APIs via faro.otel
|
|
318
|
+
const globalFaroInstance = getInternalFaroFromGlobalObject() as FaroWithOtel | undefined;
|
|
319
|
+
if (globalFaroInstance) {
|
|
320
|
+
const faroOtel: OTELApi = { trace, context };
|
|
321
|
+
registration.faro = globalFaroInstance;
|
|
322
|
+
registration.faroOtel = faroOtel;
|
|
323
|
+
registration.previousFaroOtel = globalFaroInstance.otel;
|
|
324
|
+
globalFaroInstance.otel = faroOtel;
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
this.destroy();
|
|
328
|
+
throw error;
|
|
233
329
|
}
|
|
234
330
|
}
|
|
235
331
|
|
|
@@ -258,12 +354,148 @@ export class TracingInstrumentation extends BaseInstrumentation {
|
|
|
258
354
|
return [...getReactNativeDevServerIgnoreUrls(), ...transportUrls, ...regexPatterns];
|
|
259
355
|
}
|
|
260
356
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
357
|
+
private detachRegistration(): TracingRegistration | undefined {
|
|
358
|
+
const registration = this.activeRegistration;
|
|
359
|
+
if (!registration) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
this.activeRegistration = undefined;
|
|
363
|
+
registration.instrumentations.forEach((instrumentation) => detachedInstrumentations.add(instrumentation));
|
|
364
|
+
|
|
365
|
+
let unregistered = false;
|
|
366
|
+
if (registration.unregisterInstrumentations) {
|
|
367
|
+
try {
|
|
368
|
+
registration.unregisterInstrumentations();
|
|
369
|
+
unregistered = true;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
this.logCleanupError('unregister OpenTelemetry instrumentations', error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!unregistered) {
|
|
375
|
+
registration.instrumentations.forEach((instrumentation) => {
|
|
376
|
+
this.runCleanup(`disable ${instrumentation.instrumentationName}`, () => instrumentation.disable());
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const { faro, faroOtel } = registration;
|
|
381
|
+
if (faro && faroOtel && faro.otel === faroOtel) {
|
|
382
|
+
this.runCleanup('restore faro.otel', () => {
|
|
383
|
+
if (registration.previousFaroOtel === undefined) {
|
|
384
|
+
delete faro.otel;
|
|
385
|
+
} else {
|
|
386
|
+
faro.otel = registration.previousFaroOtel;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (registration.ownsPropagator) {
|
|
392
|
+
this.runCleanup('clear the OpenTelemetry propagator', () => propagation.disable());
|
|
393
|
+
}
|
|
394
|
+
if (registration.ownsContextManager) {
|
|
395
|
+
this.runCleanup('clear the OpenTelemetry context manager', () => context.disable());
|
|
396
|
+
} else if (registration.ownsContextManagerInstance && registration.contextManager) {
|
|
397
|
+
this.runCleanup('disable the unused OpenTelemetry context manager', () => registration.contextManager?.disable());
|
|
398
|
+
}
|
|
399
|
+
if (registration.ownsTracerProvider) {
|
|
400
|
+
this.runCleanup('clear the OpenTelemetry tracer provider', () => trace.disable());
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return registration;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private runCleanup(action: string, cleanup: VoidFunction): void {
|
|
407
|
+
try {
|
|
408
|
+
cleanup();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
this.logCleanupError(action, error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private logCleanupError(action: string, error: unknown): void {
|
|
415
|
+
try {
|
|
416
|
+
this.logError(`Failed to ${action}:`, error);
|
|
417
|
+
} catch (_loggingError) {
|
|
418
|
+
// Cleanup must not fail because a custom logger threw.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private startProviderShutdown(registration: TracingRegistration): Promise<void> {
|
|
423
|
+
if (!registration.shutdownPromise) {
|
|
424
|
+
const shutdownPromise = Promise.resolve()
|
|
425
|
+
.then(() => registration.provider.forceFlush())
|
|
426
|
+
.catch((error) => {
|
|
427
|
+
this.logCleanupError('flush the OpenTelemetry tracer provider', error);
|
|
428
|
+
})
|
|
429
|
+
.then(() => registration.provider.shutdown());
|
|
430
|
+
registration.shutdownPromise = shutdownPromise;
|
|
431
|
+
this.pendingShutdowns.add(shutdownPromise);
|
|
432
|
+
void shutdownPromise.then(
|
|
433
|
+
() => this.pendingShutdowns.delete(shutdownPromise),
|
|
434
|
+
() => this.pendingShutdowns.delete(shutdownPromise)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return registration.shutdownPromise;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private startSuppliedSpanProcessorShutdown(): Promise<void> | undefined {
|
|
441
|
+
const spanProcessor = this.options.spanProcessor;
|
|
442
|
+
if (!spanProcessor) {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
if (!this.suppliedSpanProcessorShutdown) {
|
|
446
|
+
try {
|
|
447
|
+
this.suppliedSpanProcessorShutdown = spanProcessor.shutdown();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.suppliedSpanProcessorShutdown = Promise.reject(error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return this.suppliedSpanProcessorShutdown;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Detach request instrumentations and wait for the tracer provider to flush. */
|
|
264
456
|
async shutdown(): Promise<void> {
|
|
265
|
-
|
|
266
|
-
|
|
457
|
+
const pendingShutdowns = Array.from(this.pendingShutdowns);
|
|
458
|
+
const registration = this.detachRegistration();
|
|
459
|
+
if (registration) {
|
|
460
|
+
pendingShutdowns.push(this.startProviderShutdown(registration));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let firstError: unknown;
|
|
464
|
+
let shutdownFailed = false;
|
|
465
|
+
for (const shutdownPromise of pendingShutdowns) {
|
|
466
|
+
try {
|
|
467
|
+
await shutdownPromise;
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (!shutdownFailed) {
|
|
470
|
+
firstError = error;
|
|
471
|
+
shutdownFailed = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
267
474
|
}
|
|
475
|
+
const spanProcessorShutdown = this.startSuppliedSpanProcessorShutdown();
|
|
476
|
+
if (spanProcessorShutdown) {
|
|
477
|
+
try {
|
|
478
|
+
await spanProcessorShutdown;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (!shutdownFailed) {
|
|
481
|
+
firstError = error;
|
|
482
|
+
shutdownFailed = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (shutdownFailed) {
|
|
487
|
+
throw firstError;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Detach synchronously when Faro removes this instrumentation. */
|
|
492
|
+
destroy(): void {
|
|
493
|
+
const registration = this.detachRegistration();
|
|
494
|
+
if (!registration) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
void this.startProviderShutdown(registration).then(undefined, (error) => {
|
|
498
|
+
this.logCleanupError('shut down the OpenTelemetry tracer provider', error);
|
|
499
|
+
});
|
|
268
500
|
}
|
|
269
501
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
2
2
|
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
3
3
|
|
|
4
|
-
import { getDefaultOTELInstrumentations } from './getDefaultOTELInstrumentations';
|
|
4
|
+
import { getDefaultOTELInstrumentations, updateDefaultOTELInstrumentations } from './getDefaultOTELInstrumentations';
|
|
5
5
|
|
|
6
6
|
describe('getDefaultOTELInstrumentations', () => {
|
|
7
7
|
it('should return FetchInstrumentation by default', () => {
|
|
@@ -9,6 +9,18 @@ describe('getDefaultOTELInstrumentations', () => {
|
|
|
9
9
|
|
|
10
10
|
expect(instrumentations).toHaveLength(1);
|
|
11
11
|
expect(instrumentations[0]).toBeInstanceOf(FetchInstrumentation);
|
|
12
|
+
expect((instrumentations[0] as FetchInstrumentation).getConfig().semconvStabilityOptIn).toBe('http');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it.each(['', 'http/dup'])('ignores unsupported fetch convention overrides (%s)', (mode) => {
|
|
16
|
+
// JavaScript callers can still pass properties absent from the public type.
|
|
17
|
+
const options = { fetchInstrumentationOptions: { ignoreNetworkEvents: true, semconvStabilityOptIn: mode } };
|
|
18
|
+
const instrumentations = getDefaultOTELInstrumentations(options).flat();
|
|
19
|
+
const fetch = instrumentations[0] as FetchInstrumentation;
|
|
20
|
+
expect(fetch.getConfig().semconvStabilityOptIn).toBe('http');
|
|
21
|
+
|
|
22
|
+
updateDefaultOTELInstrumentations(instrumentations, options);
|
|
23
|
+
expect(fetch.getConfig().semconvStabilityOptIn).toBe('http');
|
|
12
24
|
});
|
|
13
25
|
|
|
14
26
|
it('should allow enabling XMLHttpRequestInstrumentation', () => {
|
|
@@ -49,6 +61,21 @@ describe('getDefaultOTELInstrumentations', () => {
|
|
|
49
61
|
expect((instrumentations[1] as any)._config.ignoreUrls).toEqual(ignoreUrls);
|
|
50
62
|
});
|
|
51
63
|
|
|
64
|
+
it('updates existing Fetch and XMLHttpRequest instrumentation configs', () => {
|
|
65
|
+
const instrumentations = getDefaultOTELInstrumentations({
|
|
66
|
+
enableXhrInstrumentation: true,
|
|
67
|
+
ignoreUrls: [/first-url/],
|
|
68
|
+
}).flat();
|
|
69
|
+
|
|
70
|
+
updateDefaultOTELInstrumentations(instrumentations, {
|
|
71
|
+
enableXhrInstrumentation: true,
|
|
72
|
+
ignoreUrls: [/second-url/],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect((instrumentations[0] as FetchInstrumentation).getConfig().ignoreUrls).toEqual([/second-url/]);
|
|
76
|
+
expect((instrumentations[1] as XMLHttpRequestInstrumentation).getConfig().ignoreUrls).toEqual([/second-url/]);
|
|
77
|
+
});
|
|
78
|
+
|
|
52
79
|
it('should set ignoreNetworkEvents to true by default', () => {
|
|
53
80
|
const instrumentations = getDefaultOTELInstrumentations();
|
|
54
81
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Instrumentation } from '@opentelemetry/instrumentation';
|
|
2
2
|
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
3
3
|
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
4
4
|
|
|
5
|
-
import { faro, type UserActionInternalInterface, UserActionState } from '@grafana/faro-core';
|
|
6
|
-
|
|
7
5
|
import type { DefaultInstrumentationsOptions, InstrumentationOption } from '../types';
|
|
8
6
|
|
|
9
7
|
import {
|
|
8
|
+
captureFetchUserAction,
|
|
10
9
|
fetchCustomAttributeFunctionWithDefaults,
|
|
11
10
|
xhrCustomAttributeFunctionWithDefaults,
|
|
12
11
|
} from './instrumentationUtils';
|
|
@@ -27,31 +26,54 @@ import {
|
|
|
27
26
|
* @returns Array of OTEL instrumentations
|
|
28
27
|
*/
|
|
29
28
|
export function getDefaultOTELInstrumentations(options: DefaultInstrumentationsOptions = {}): InstrumentationOption[] {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
enableXhrInstrumentation = false,
|
|
33
|
-
fetchInstrumentationOptions,
|
|
34
|
-
xhrInstrumentationOptions,
|
|
35
|
-
...sharedOptions
|
|
36
|
-
} = options;
|
|
29
|
+
const { enableFetchInstrumentation, enableXhrInstrumentation, fetchConfig, xhrConfig } =
|
|
30
|
+
resolveDefaultInstrumentationOptions(options);
|
|
37
31
|
|
|
38
32
|
const instrumentations: InstrumentationOption[] = [];
|
|
39
33
|
|
|
40
34
|
if (enableFetchInstrumentation) {
|
|
41
|
-
instrumentations.push(
|
|
42
|
-
new FetchInstrumentation(createFetchInstrumentationOptions(fetchInstrumentationOptions, sharedOptions))
|
|
43
|
-
);
|
|
35
|
+
instrumentations.push(new FetchInstrumentation(fetchConfig));
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
if (enableXhrInstrumentation) {
|
|
47
|
-
instrumentations.push(
|
|
48
|
-
new XMLHttpRequestInstrumentation(createXhrInstrumentationOptions(xhrInstrumentationOptions, sharedOptions))
|
|
49
|
-
);
|
|
39
|
+
instrumentations.push(new XMLHttpRequestInstrumentation(xhrConfig));
|
|
50
40
|
}
|
|
51
41
|
|
|
52
42
|
return instrumentations;
|
|
53
43
|
}
|
|
54
44
|
|
|
45
|
+
export function updateDefaultOTELInstrumentations(
|
|
46
|
+
instrumentations: Instrumentation[],
|
|
47
|
+
options: DefaultInstrumentationsOptions = {}
|
|
48
|
+
): void {
|
|
49
|
+
const { fetchConfig, xhrConfig } = resolveDefaultInstrumentationOptions(options);
|
|
50
|
+
|
|
51
|
+
instrumentations.forEach((instrumentation) => {
|
|
52
|
+
if (instrumentation instanceof FetchInstrumentation) {
|
|
53
|
+
instrumentation.setConfig(fetchConfig);
|
|
54
|
+
} else if (instrumentation instanceof XMLHttpRequestInstrumentation) {
|
|
55
|
+
instrumentation.setConfig(xhrConfig);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveDefaultInstrumentationOptions(options: DefaultInstrumentationsOptions) {
|
|
61
|
+
const {
|
|
62
|
+
enableFetchInstrumentation = true,
|
|
63
|
+
enableXhrInstrumentation = false,
|
|
64
|
+
fetchInstrumentationOptions,
|
|
65
|
+
xhrInstrumentationOptions,
|
|
66
|
+
...sharedOptions
|
|
67
|
+
} = options;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
enableFetchInstrumentation,
|
|
71
|
+
enableXhrInstrumentation,
|
|
72
|
+
fetchConfig: createFetchInstrumentationOptions(fetchInstrumentationOptions, sharedOptions),
|
|
73
|
+
xhrConfig: createXhrInstrumentationOptions(xhrInstrumentationOptions, sharedOptions),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
function createFetchInstrumentationOptions(
|
|
56
78
|
fetchInstrumentationOptions: DefaultInstrumentationsOptions['fetchInstrumentationOptions'],
|
|
57
79
|
sharedOptions: Record<string, unknown>
|
|
@@ -62,26 +84,13 @@ function createFetchInstrumentationOptions(
|
|
|
62
84
|
ignoreNetworkEvents: true,
|
|
63
85
|
// Keep this here to overwrite the defaults above if provided by the users
|
|
64
86
|
...fetchInstrumentationOptions,
|
|
87
|
+
semconvStabilityOptIn: 'http',
|
|
65
88
|
// Always keep this function
|
|
66
89
|
applyCustomAttributesOnSpan: fetchCustomAttributeFunctionWithDefaults(
|
|
67
90
|
fetchInstrumentationOptions?.applyCustomAttributesOnSpan
|
|
68
91
|
),
|
|
69
|
-
//
|
|
70
|
-
requestHook:
|
|
71
|
-
try {
|
|
72
|
-
const currentAction = faro.api.getActiveUserAction();
|
|
73
|
-
if (
|
|
74
|
-
currentAction &&
|
|
75
|
-
(currentAction as unknown as UserActionInternalInterface)?.getState() === UserActionState.Started
|
|
76
|
-
) {
|
|
77
|
-
span.setAttribute('faro.action.user.name', currentAction.name);
|
|
78
|
-
span.setAttribute('faro.action.user.parentId', currentAction.parentId);
|
|
79
|
-
}
|
|
80
|
-
} catch (_error) {
|
|
81
|
-
// Silently fail - don't log to avoid infinite loops
|
|
82
|
-
// The span will just not have user action context
|
|
83
|
-
}
|
|
84
|
-
},
|
|
92
|
+
// Also supports callers using a custom processor without the RN request monitor.
|
|
93
|
+
requestHook: captureFetchUserAction,
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -5,6 +5,19 @@ import type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-
|
|
|
5
5
|
|
|
6
6
|
import { faro, type UserActionInternalInterface, UserActionState } from '@grafana/faro-core';
|
|
7
7
|
|
|
8
|
+
/** Capture request ownership at start, before the monitor can halt the action. */
|
|
9
|
+
export function captureFetchUserAction(span: Span): void {
|
|
10
|
+
try {
|
|
11
|
+
const action = faro.api?.getActiveUserAction?.();
|
|
12
|
+
if (action && (action as UserActionInternalInterface).getState() === UserActionState.Started) {
|
|
13
|
+
span.setAttribute('faro.action.user.name', action.name);
|
|
14
|
+
span.setAttribute('faro.action.user.parentId', action.parentId);
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// Instrumentation must not interrupt the request or log recursively.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
/**
|
|
9
22
|
* FetchError interface matching OpenTelemetry's internal type
|
|
10
23
|
* Note: FetchError is not exported from @opentelemetry/instrumentation-fetch
|