diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 8603131571d..98081eb030b 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -21,6 +21,7 @@ feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telem * fix(exporter-prometheus): add missing `@opentelemetry/semantic-conventions` dependency [#6330](https://github.com/open-telemetry/opentelemetry-js/pull/6330) @omizha * fix(otlp-transformer): correctly handle Uint8Array attribute values when serializing to JSON [#6348](https://github.com/open-telemetry/opentelemetry-js/pull/6348) @pichlermarc +* fix(otlp-exporter-base): fix unwanted instrumentation of the fetch exports when context is not propagated [#6353](https://github.com/open-telemetry/opentelemetry-js/pull/6353) @david-luna ### :books: Documentation diff --git a/experimental/packages/otlp-exporter-base/src/transport/fetch-transport.ts b/experimental/packages/otlp-exporter-base/src/transport/fetch-transport.ts index 715ac7ed910..634f6c92a5b 100644 --- a/experimental/packages/otlp-exporter-base/src/transport/fetch-transport.ts +++ b/experimental/packages/otlp-exporter-base/src/transport/fetch-transport.ts @@ -38,10 +38,23 @@ class FetchTransport implements IExporterTransport { async send(data: Uint8Array, timeoutMillis: number): Promise { const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), timeoutMillis); + // Fetch API may be wrapped by an instrumentation like `@opentelemetry/instrumentation-fetch`. + // In that case the instrumentation would create a new Span for this request + // because the context manager cannot keep the context after `await` calls. + // This creates an indirect endless loop Export -> Span -> Export + // By using the `__original` function the instrumentation can't intercept the call + // and no Span will be created breaking the vicious cycle + let fetchApi = globalThis.fetch; + // @ts-expect-error -- fetch could be wrapped + if (typeof fetchApi.__original === 'function') { + // @ts-expect-error -- fetch could be wrapped + fetchApi = fetchApi.__original; + } + try { const isBrowserEnvironment = !!globalThis.location; const url = new URL(this._parameters.url); - const response = await fetch(url.href, { + const response = await fetchApi(url.href, { method: 'POST', headers: await this._parameters.headers(), body: data, diff --git a/experimental/packages/otlp-exporter-base/test/browser/fetch-transport.test.ts b/experimental/packages/otlp-exporter-base/test/browser/fetch-transport.test.ts index 2fdea561a57..afe01f2cdef 100644 --- a/experimental/packages/otlp-exporter-base/test/browser/fetch-transport.test.ts +++ b/experimental/packages/otlp-exporter-base/test/browser/fetch-transport.test.ts @@ -41,6 +41,30 @@ describe('FetchTransport', function () { }); describe('send', function () { + it('it uses global fetch API and is not affected by patching', function (done) { + // arrange + const fetchStub = sinon + .stub(globalThis, 'fetch') + .resolves(new Response('test response', { status: 200 })); + const transport = createFetchTransport(testTransportParameters); + // We patch fetch simulating what an instrumentation would do + const patchedStub = sinon.stub().callsFake(fetchStub); + globalThis.fetch = patchedStub; + (globalThis.fetch as any).__original = fetchStub; + + //act + transport.send(testPayload, requestTimeout).then(response => { + // assert + try { + assert.strictEqual(response.status, 'success'); + sinon.assert.notCalled(patchedStub); + sinon.assert.called(fetchStub); + } catch (e) { + done(e); + } + done(); + }, done /* catch any rejections */); + }); it('returns success when request succeeds', function (done) { // arrange const fetchStub = sinon