Skip to content

Commit 3dcdba6

Browse files
feat(browser otlp exporter): add fetch transport for fetch-only environments (#5807)
Co-authored-by: Marc Pichler <[email protected]>
1 parent 95b40aa commit 3dcdba6

File tree

17 files changed

+518
-72
lines changed

17 files changed

+518
-72
lines changed

experimental/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1616

1717
### :rocket: Features
1818

19+
* feat(otlp-exporter-base): Add fetch transport for fetch-only environments like service workers. [#5807](https://github.com/open-telemetry/opentelemetry-js/pull/5807)
20+
* when using headers, the Browser exporter now prefers `fetch` over `XMLHttpRequest` if present. Sending via `XMLHttpRequest` will be removed in a future release.
1921
* feat(opentelemetry-configuration): creation of basic ConfigProvider [#5809](https://github.com/open-telemetry/opentelemetry-js/pull/5809) @maryliag
2022
* feat(opentelemetry-configuration): creation of basic FileConfigProvider [#5863](https://github.com/open-telemetry/opentelemetry-js/pull/5863) @maryliag
2123
* feat(sdk-node): Add support for multiple metric readers via the new `metricReaders` option in NodeSDK configuration. Users can now register multiple metric readers (e.g., Console, Prometheus) directly through the NodeSDK constructor. The old `metricReader` (singular) option is now deprecated and will show a warning if used, but remains supported for backward compatibility. Comprehensive tests and documentation have been added. [#5760](https://github.com/open-telemetry/opentelemetry-js/issues/5760)
@@ -37,6 +39,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
3739
### :bug: Bug Fixes
3840

3941
* fix(otlp-exporter-base): prioritize `esnext` export condition as it is more specific [#5458](https://github.com/open-telemetry/opentelemetry-js/pull/5458)
42+
* fix(otlp-exporter-base): consider relative urls as valid in browser environments [#5807](https://github.com/open-telemetry/opentelemetry-js/pull/5807)
4043
* fix(instrumentation-fetch): Use ESM version of semconv instead of CJS. Users expecting mixed ESM and CJS modules will now only get ESM modules. [#5878](https://github.com/open-telemetry/opentelemetry-js/pull/5878) @overbalance
4144

4245
### :books: Documentation

experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,22 @@ describe('OTLPLogExporter', function () {
6464
(window.navigator as any).sendBeacon = false;
6565
});
6666

67-
it('should successfully send data using XMLHttpRequest', async function () {
67+
it('should successfully send data using fetch', async function () {
6868
// arrange
69-
const server = sinon.fakeServer.create();
69+
const stubFetch = sinon.stub(window, 'fetch');
7070
const loggerProvider = new LoggerProvider({
7171
processors: [new SimpleLogRecordProcessor(new OTLPLogExporter())],
7272
});
7373

7474
// act
7575
loggerProvider.getLogger('test-logger').emit({ body: 'test-body' });
76-
queueMicrotask(() => {
77-
// simulate success response
78-
server.requests[0].respond(200, {}, '');
79-
});
8076
await loggerProvider.shutdown();
8177

8278
// assert
83-
const request = server.requests[0];
84-
const body = request.requestBody as unknown as Uint8Array;
79+
const request = new Request(...stubFetch.args[0]);
80+
const body = await request.text();
8581
assert.doesNotThrow(
86-
() => JSON.parse(new TextDecoder().decode(body)),
82+
() => JSON.parse(body),
8783
'expected requestBody to be in JSON format, but parsing failed'
8884
);
8985
});

experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,22 @@ describe('OTLPLogExporter', function () {
6464
(window.navigator as any).sendBeacon = false;
6565
});
6666

67-
it('should successfully send data using XMLHttpRequest', async function () {
67+
it('should successfully send data using fetch', async function () {
6868
// arrange
69-
const server = sinon.fakeServer.create();
69+
const stubFetch = sinon.stub(window, 'fetch');
7070
const loggerProvider = new LoggerProvider({
7171
processors: [new SimpleLogRecordProcessor(new OTLPLogExporter())],
7272
});
7373

7474
// act
7575
loggerProvider.getLogger('test-logger').emit({ body: 'test-body' });
76-
queueMicrotask(() => {
77-
// simulate success response
78-
server.requests[0].respond(200, {}, '');
79-
});
8076
await loggerProvider.shutdown();
8177

8278
// assert
83-
const request = server.requests[0];
84-
const body = request.requestBody as unknown as Uint8Array;
79+
const request = new Request(...stubFetch.args[0]);
80+
const body = await request.text();
8581
assert.throws(
86-
() => JSON.parse(new TextDecoder().decode(body)),
82+
() => JSON.parse(body),
8783
'expected requestBody to be in protobuf format, but parsing as JSON succeeded'
8884
);
8985
});

experimental/packages/exporter-trace-otlp-http/test/browser/OTLPTraceExporter.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,22 @@ describe('OTLPTraceExporter', () => {
6565
(window.navigator as any).sendBeacon = false;
6666
});
6767

68-
it('should successfully send data using XMLHttpRequest', async function () {
68+
it('should successfully send data using fetch', async function () {
6969
// arrange
70-
const server = sinon.fakeServer.create();
70+
const stubFetch = sinon.stub(window, 'fetch');
7171
const tracerProvider = new BasicTracerProvider({
7272
spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())],
7373
});
7474

7575
// act
7676
tracerProvider.getTracer('test-tracer').startSpan('test-span').end();
77-
queueMicrotask(() => {
78-
// simulate success response
79-
server.requests[0].respond(200, {}, '');
80-
});
8177
await tracerProvider.shutdown();
8278

8379
// assert
84-
const request = server.requests[0];
85-
const body = request.requestBody as unknown as Uint8Array;
80+
const request = new Request(...stubFetch.args[0]);
81+
const body = await request.text();
8682
assert.doesNotThrow(
87-
() => JSON.parse(new TextDecoder().decode(body)),
83+
() => JSON.parse(body),
8884
'expected requestBody to be in JSON format, but parsing failed'
8985
);
9086
});

experimental/packages/exporter-trace-otlp-proto/test/browser/OTLPTraceExporter.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,23 @@ describe('OTLPTraceExporter', () => {
6565
(window.navigator as any).sendBeacon = false;
6666
});
6767

68-
it('should successfully send data using XMLHttpRequest', async function () {
68+
it('should successfully send data using fetch', async function () {
6969
// arrange
70-
const server = sinon.fakeServer.create();
70+
const stubFetch = sinon.stub(window, 'fetch');
7171
const tracerProvider = new BasicTracerProvider({
7272
spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())],
7373
});
7474

7575
// act
7676
tracerProvider.getTracer('test-tracer').startSpan('test-span').end();
77-
queueMicrotask(() => {
78-
// simulate success response
79-
server.requests[0].respond(200, {}, '');
80-
});
8177
await tracerProvider.shutdown();
8278

8379
// assert
84-
const request = server.requests[0];
85-
const body = request.requestBody as unknown as Uint8Array;
80+
const request = new Request(...stubFetch.args[0]);
81+
const body = await request.text();
8682
assert.throws(
87-
() => JSON.parse(new TextDecoder().decode(body)),
88-
'expected requestBody to be in protobuf format, but parsing as JSON succeeded'
83+
() => JSON.parse(body),
84+
'expected request body to be in protobuf format, but parsing as JSON succeeded'
8985
);
9086
});
9187
});

experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/OTLPMetricExporter.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,9 @@ describe('OTLPMetricExporter', function () {
7676
(window.navigator as any).sendBeacon = false;
7777
});
7878

79-
it('should successfully send data using XMLHttpRequest', async function () {
79+
it('should successfully send data using fetch', async function () {
8080
// arrange
81-
const server = sinon.fakeServer.create();
82-
server.respondWith('OK');
83-
server.respondImmediately = true;
84-
server.autoRespond = true;
81+
const stubFetch = sinon.stub(window, 'fetch');
8582
const meterProvider = new MeterProvider({
8683
readers: [
8784
new PeriodicExportingMetricReader({
@@ -95,15 +92,14 @@ describe('OTLPMetricExporter', function () {
9592
.getMeter('test-meter')
9693
.createCounter('test-counter')
9794
.add(1);
98-
9995
await meterProvider.shutdown();
10096

10197
// assert
102-
const request = server.requests[0];
103-
const body = request.requestBody as unknown as Uint8Array;
98+
const request = new Request(...stubFetch.args[0]);
99+
const body = await request.text();
104100
assert.doesNotThrow(
105-
() => JSON.parse(new TextDecoder().decode(body)),
106-
'expected requestBody to be in JSON format, but parsing failed'
101+
() => JSON.parse(body),
102+
'expected request body to be in JSON format, but parsing failed'
107103
);
108104
});
109105
});

experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/browser/OTLPMetricExporter.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,9 @@ describe('OTLPTraceExporter', () => {
7272
(window.navigator as any).sendBeacon = false;
7373
});
7474

75-
it('should successfully send data using XMLHttpRequest', async function () {
75+
it('should successfully send data using fetch', async function () {
7676
// arrange
77-
const server = sinon.fakeServer.create();
78-
server.respondWith('OK');
79-
server.respondImmediately = true;
80-
server.autoRespond = true;
77+
const stubFetch = sinon.stub(window, 'fetch');
8178
const meterProvider = new MeterProvider({
8279
readers: [
8380
new PeriodicExportingMetricReader({
@@ -95,10 +92,10 @@ describe('OTLPTraceExporter', () => {
9592
await meterProvider.shutdown();
9693

9794
// assert
98-
const request = server.requests[0];
99-
const body = request.requestBody as unknown as Uint8Array;
95+
const request = new Request(...stubFetch.args[0]);
96+
const body = await request.text();
10097
assert.throws(
101-
() => JSON.parse(new TextDecoder().decode(body)),
98+
() => JSON.parse(body),
10299
'expected requestBody to be in protobuf format, but parsing as JSON succeeded'
103100
);
104101
});

experimental/packages/otlp-exporter-base/src/configuration/create-legacy-browser-delegate.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import { ISerializer } from '@opentelemetry/otlp-transformer';
1717
import {
18+
createOtlpFetchExportDelegate,
1819
createOtlpSendBeaconExportDelegate,
1920
createOtlpXhrExportDelegate,
2021
} from '../otlp-browser-http-export-delegate';
@@ -35,17 +36,25 @@ export function createLegacyOtlpBrowserExportDelegate<Internal, Response>(
3536
signalResourcePath: string,
3637
requiredHeaders: Record<string, string>
3738
): IOtlpExportDelegate<Internal> {
38-
const useXhr = !!config.headers || typeof navigator.sendBeacon !== 'function';
39+
const createOtlpExportDelegate = inferExportDelegateToUse(config.headers);
3940

4041
const options = convertLegacyBrowserHttpOptions(
4142
config,
4243
signalResourcePath,
4344
requiredHeaders
4445
);
4546

46-
if (useXhr) {
47-
return createOtlpXhrExportDelegate(options, serializer);
47+
return createOtlpExportDelegate(options, serializer);
48+
}
49+
50+
export function inferExportDelegateToUse(
51+
configHeaders: OTLPExporterConfigBase['headers']
52+
) {
53+
if (!configHeaders && typeof navigator.sendBeacon === 'function') {
54+
return createOtlpSendBeaconExportDelegate;
55+
} else if (typeof globalThis.fetch !== 'undefined') {
56+
return createOtlpFetchExportDelegate;
4857
} else {
49-
return createOtlpSendBeaconExportDelegate(options, serializer);
58+
return createOtlpXhrExportDelegate;
5059
}
5160
}

experimental/packages/otlp-exporter-base/src/configuration/otlp-http-configuration.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ function validateUserProvidedUrl(url: string | undefined): string | undefined {
7676
return undefined;
7777
}
7878
try {
79-
new URL(url);
80-
return url;
79+
// NOTE: In non-browser environments, `globalThis.location` will be `undefined`.
80+
const base = globalThis.location?.href;
81+
return new URL(url, base).href;
8182
} catch {
8283
throw new Error(
8384
`Configuration: Could not parse user-provided export URL: '${url}'`

experimental/packages/otlp-exporter-base/src/otlp-browser-http-export-delegate.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { createRetryingTransport } from './retrying-transport';
2020
import { createXhrTransport } from './transport/xhr-transport';
2121
import { createSendBeaconTransport } from './transport/send-beacon-transport';
2222
import { createOtlpNetworkExportDelegate } from './otlp-network-export-delegate';
23+
import { createFetchTransport } from './transport/fetch-transport';
2324

25+
/**
26+
* @deprecated use {@link createOtlpFetchExportDelegate}
27+
*/
2428
export function createOtlpXhrExportDelegate<Internal, Response>(
2529
options: OtlpHttpConfiguration,
2630
serializer: ISerializer<Internal, Response>
@@ -34,6 +38,19 @@ export function createOtlpXhrExportDelegate<Internal, Response>(
3438
);
3539
}
3640

41+
export function createOtlpFetchExportDelegate<Internal, Response>(
42+
options: OtlpHttpConfiguration,
43+
serializer: ISerializer<Internal, Response>
44+
): IOtlpExportDelegate<Internal> {
45+
return createOtlpNetworkExportDelegate(
46+
options,
47+
serializer,
48+
createRetryingTransport({
49+
transport: createFetchTransport(options),
50+
})
51+
);
52+
}
53+
3754
export function createOtlpSendBeaconExportDelegate<Internal, Response>(
3855
options: OtlpHttpConfiguration,
3956
serializer: ISerializer<Internal, Response>

0 commit comments

Comments
 (0)