diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 94073f8df09..ed7d4c9b00a 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,9 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :boom: Breaking Changes +* fix(otlp-exporter-base)!: remove xhr transport [#6317](https://github.com/open-telemetry/opentelemetry-js/pull/6317) @cjihrig + * (user-facing) The deprecated XHR-based transport has been removed and replaced with `fetch()`. This change affects users who relied on `XmlHttpRequest` instead of `fetch()` for sending headers with OTLP exports. To maintain compatibility on browsers without a `fetch()` implementation, include a `fetch()` polyfill. + ### :rocket: Features * feat(sdk-logs): export event name from ConsoleLogRecordExporter [#6310](https://github.com/open-telemetry/opentelemetry-js/pull/6310) @aicest diff --git a/experimental/packages/otlp-exporter-base/src/configuration/create-legacy-browser-delegate.ts b/experimental/packages/otlp-exporter-base/src/configuration/create-legacy-browser-delegate.ts index b1c3823e235..9b1724b973c 100644 --- a/experimental/packages/otlp-exporter-base/src/configuration/create-legacy-browser-delegate.ts +++ b/experimental/packages/otlp-exporter-base/src/configuration/create-legacy-browser-delegate.ts @@ -17,7 +17,6 @@ import { ISerializer } from '@opentelemetry/otlp-transformer'; import { createOtlpFetchExportDelegate, createOtlpSendBeaconExportDelegate, - createOtlpXhrExportDelegate, } from '../otlp-browser-http-export-delegate'; import { convertLegacyBrowserHttpOptions } from './convert-legacy-browser-http-options'; import { IOtlpExportDelegate } from '../otlp-export-delegate'; @@ -52,9 +51,7 @@ export function inferExportDelegateToUse( ) { if (!configHeaders && typeof navigator.sendBeacon === 'function') { return createOtlpSendBeaconExportDelegate; - } else if (typeof globalThis.fetch !== 'undefined') { - return createOtlpFetchExportDelegate; - } else { - return createOtlpXhrExportDelegate; } + + return createOtlpFetchExportDelegate; } diff --git a/experimental/packages/otlp-exporter-base/src/index-browser-http.ts b/experimental/packages/otlp-exporter-base/src/index-browser-http.ts index bf10a2be976..6ab65d75ce8 100644 --- a/experimental/packages/otlp-exporter-base/src/index-browser-http.ts +++ b/experimental/packages/otlp-exporter-base/src/index-browser-http.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -export { - createOtlpXhrExportDelegate, - createOtlpSendBeaconExportDelegate, -} from './otlp-browser-http-export-delegate'; +export { createOtlpSendBeaconExportDelegate } from './otlp-browser-http-export-delegate'; export { convertLegacyBrowserHttpOptions } from './configuration/convert-legacy-browser-http-options'; export { createLegacyOtlpBrowserExportDelegate } from './configuration/create-legacy-browser-delegate'; diff --git a/experimental/packages/otlp-exporter-base/src/otlp-browser-http-export-delegate.ts b/experimental/packages/otlp-exporter-base/src/otlp-browser-http-export-delegate.ts index 57d1d87b169..46610a59bab 100644 --- a/experimental/packages/otlp-exporter-base/src/otlp-browser-http-export-delegate.ts +++ b/experimental/packages/otlp-exporter-base/src/otlp-browser-http-export-delegate.ts @@ -17,27 +17,10 @@ import { OtlpHttpConfiguration } from './configuration/otlp-http-configuration'; import { ISerializer } from '@opentelemetry/otlp-transformer'; import { IOtlpExportDelegate } from './otlp-export-delegate'; import { createRetryingTransport } from './retrying-transport'; -import { createXhrTransport } from './transport/xhr-transport'; import { createSendBeaconTransport } from './transport/send-beacon-transport'; import { createOtlpNetworkExportDelegate } from './otlp-network-export-delegate'; import { createFetchTransport } from './transport/fetch-transport'; -/** - * @deprecated use {@link createOtlpFetchExportDelegate} - */ -export function createOtlpXhrExportDelegate( - options: OtlpHttpConfiguration, - serializer: ISerializer -): IOtlpExportDelegate { - return createOtlpNetworkExportDelegate( - options, - serializer, - createRetryingTransport({ - transport: createXhrTransport(options), - }) - ); -} - export function createOtlpFetchExportDelegate( options: OtlpHttpConfiguration, serializer: ISerializer diff --git a/experimental/packages/otlp-exporter-base/src/transport/xhr-transport.ts b/experimental/packages/otlp-exporter-base/src/transport/xhr-transport.ts deleted file mode 100644 index 45ff9fc6f05..00000000000 --- a/experimental/packages/otlp-exporter-base/src/transport/xhr-transport.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { IExporterTransport } from '../exporter-transport'; -import { ExportResponse } from '../export-response'; -import { diag } from '@opentelemetry/api'; -import { - isExportHTTPErrorRetryable, - parseRetryAfterToMills, -} from '../is-export-retryable'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { createFetchTransport } from './fetch-transport'; -import { HeadersFactory } from '../configuration/otlp-http-configuration'; - -/** - * @deprecated favor the fetch transport - * @see {@link createFetchTransport} - */ -export interface XhrRequestParameters { - url: string; - headers: HeadersFactory; -} - -class XhrTransport implements IExporterTransport { - private _parameters: XhrRequestParameters; - - constructor(parameters: XhrRequestParameters) { - this._parameters = parameters; - } - - async send(data: Uint8Array, timeoutMillis: number): Promise { - const headers = await this._parameters.headers(); - const response = await new Promise(resolve => { - const xhr = new XMLHttpRequest(); - xhr.timeout = timeoutMillis; - xhr.open('POST', this._parameters.url); - Object.entries(headers).forEach(([k, v]) => { - xhr.setRequestHeader(k, v); - }); - - xhr.ontimeout = _ => { - resolve({ - status: 'retryable', - error: new Error('XHR request timed out'), - }); - }; - - xhr.onreadystatechange = () => { - if (xhr.status >= 200 && xhr.status <= 299) { - diag.debug('XHR success'); - resolve({ - status: 'success', - }); - } else if (xhr.status && isExportHTTPErrorRetryable(xhr.status)) { - resolve({ - status: 'retryable', - retryInMillis: parseRetryAfterToMills( - xhr.getResponseHeader('Retry-After') - ), - }); - } else if (xhr.status !== 0) { - resolve({ - status: 'failure', - error: new Error('XHR request failed with non-retryable status'), - }); - } - }; - - xhr.onabort = () => { - resolve({ - status: 'failure', - error: new Error('XHR request aborted'), - }); - }; - xhr.onerror = () => { - resolve({ - status: 'retryable', - error: new Error('XHR request encountered a network error'), - }); - }; - - xhr.send(data); - }); - - return response; - } - - shutdown() { - // Intentionally left empty, nothing to do. - } -} - -/** - * @deprecated use {@link createFetchTransport} instead - * - * Creates an exporter transport that uses XHR to send the data - * @param parameters applied to each request made by transport - */ -export function createXhrTransport( - parameters: XhrRequestParameters -): IExporterTransport { - return new XhrTransport(parameters); -} diff --git a/experimental/packages/otlp-exporter-base/test/browser/create-legacy-browser-delegate.test.ts b/experimental/packages/otlp-exporter-base/test/browser/create-legacy-browser-delegate.test.ts index ff0fe985acb..082ac935bba 100644 --- a/experimental/packages/otlp-exporter-base/test/browser/create-legacy-browser-delegate.test.ts +++ b/experimental/packages/otlp-exporter-base/test/browser/create-legacy-browser-delegate.test.ts @@ -19,7 +19,6 @@ import { inferExportDelegateToUse } from '../../src/configuration/create-legacy- import { createOtlpFetchExportDelegate, createOtlpSendBeaconExportDelegate, - createOtlpXhrExportDelegate, } from '../../src/otlp-browser-http-export-delegate'; describe('createLegacyBrowserDelegate', function () { @@ -51,43 +50,5 @@ describe('createLegacyBrowserDelegate', function () { assert.equal(delegate, createOtlpFetchExportDelegate); }); }); - - describe('when fetch is unavailable', function () { - const fetch = window.fetch; - beforeEach(function () { - // fake fetch being unavailable - (window as any).fetch = undefined; - }); - afterEach(() => { - window.fetch = fetch; - }); - - it('uses xhr delegate', function () { - const delegate = inferExportDelegateToUse(undefined); - assert.equal(delegate, createOtlpXhrExportDelegate); - }); - }); - }); - - describe('when fetch is unavailable but beacon and xhr are', function () { - const fetch = window.fetch; - beforeEach(function () { - // fake fetch being unavailable - (window as any).fetch = undefined; - }); - afterEach(function () { - window.fetch = fetch; - }); - - it('uses xhr when beacon is available but headers are provided', function () { - const fetch = window.fetch; - // @ts-expect-error one should not be able to mutate the window but this is a test. - window.fetch = undefined; - - const delegate = inferExportDelegateToUse({ foo: 'bar' }); - assert.equal(delegate, createOtlpXhrExportDelegate); - - window.fetch = fetch; - }); }); }); diff --git a/experimental/packages/otlp-exporter-base/test/browser/xhr-transport.test.ts b/experimental/packages/otlp-exporter-base/test/browser/xhr-transport.test.ts deleted file mode 100644 index 27c65105427..00000000000 --- a/experimental/packages/otlp-exporter-base/test/browser/xhr-transport.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as sinon from 'sinon'; -import * as assert from 'assert'; -import { createXhrTransport } from '../../src/transport/xhr-transport'; -import { - ExportResponseRetryable, - ExportResponseSuccess, - ExportResponseFailure, -} from '../../src'; -import { ensureHeadersContain } from '../testHelper'; - -const testTransportParameters = { - url: 'http://example.test', - headers: async () => ({ - foo: 'foo-value', - bar: 'bar-value', - 'Content-Type': 'application/json', - }), -}; - -const requestTimeout = 1000; -const testPayload = Uint8Array.from([1, 2, 3]); - -function respondWhenRequestExists( - server: sinon.SinonFakeServer, - responder: (request: sinon.SinonFakeXMLHttpRequest) => void -) { - function tryRespond() { - if (server.requests.length > 0) { - responder(server.requests[0]); - } else { - setTimeout(tryRespond, 0); - } - } - setTimeout(tryRespond, 0); -} - -function hasOnTimeout(request: unknown): request is { ontimeout: () => void } { - if (request == null || typeof request != 'object') { - return false; - } - - return 'ontimeout' in request && typeof request['ontimeout'] === 'function'; -} - -function hasOnAbort(request: unknown): request is { onabort: () => void } { - if (request == null || typeof request != 'object') { - return false; - } - - return 'onabort' in request && typeof request['onabort'] === 'function'; -} - -describe('XhrTransport', function () { - afterEach(function () { - sinon.restore(); - }); - - describe('send', function () { - it('returns success when request succeeds', function (done) { - // arrange - const server = sinon.fakeServer.create(); - const transport = createXhrTransport(testTransportParameters); - - let request: sinon.SinonFakeXMLHttpRequest; - respondWhenRequestExists(server, () => { - // this executes after the act block - request = server.requests[0]; - request.respond(200, {}, 'test response'); - }); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'success'); - // currently we don't do anything with the response yet, so it's dropped by the transport. - assert.strictEqual( - (response as ExportResponseSuccess).data, - undefined - ); - assert.strictEqual(request.url, testTransportParameters.url); - assert.strictEqual(request.requestBody, testPayload); - ensureHeadersContain(request.requestHeaders, { - foo: 'foo-value', - bar: 'bar-value', - // ;charset=utf-8 is applied by sinon.fakeServer - 'Content-Type': 'application/json;charset=utf-8', - }); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - }); - - it('returns failure when request fails', function (done) { - // arrange - const server = sinon.fakeServer.create(); - const transport = createXhrTransport(testTransportParameters); - - respondWhenRequestExists(server, () => { - // this executes after the act block - const request = server.requests[0]; - request.respond(404, {}, ''); - }); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'failure'); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - }); - - it('returns retryable when request is retryable', function (done) { - // arrange - const server = sinon.fakeServer.create(); - const transport = createXhrTransport(testTransportParameters); - - respondWhenRequestExists(server, () => { - // this executes after the act block - const request = server.requests[0]; - request.respond(503, { 'Retry-After': 5 }, ''); - }); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'retryable'); - assert.strictEqual( - (response as ExportResponseRetryable).retryInMillis, - 5000 - ); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - }); - - it('returns retryable when request times out', function (done) { - // arrange - // A fake server needed, otherwise the message will not be a timeout but a failure to connect. - const server = sinon.useFakeServer(); - const transport = createXhrTransport(testTransportParameters); - - respondWhenRequestExists(server, () => { - // this executes after the act block - const request = server.requests[0]; - // Manually trigger the ontimeout event, but don't respond. - if (hasOnTimeout(request)) { - request.ontimeout(); - } - }); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'retryable'); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - }); - - it('returns retryable when network error occurs', function (done) { - // arrange - const clock = sinon.useFakeTimers(); - const transport = createXhrTransport(testTransportParameters); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'retryable'); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - clock.tick(requestTimeout + 100); - }); - - it('returns failure when request is aborted', function (done) { - // arrange - const server = sinon.useFakeServer(); - const transport = createXhrTransport(testTransportParameters); - - respondWhenRequestExists(server, () => { - // this executes after the act block - const request = server.requests[0]; - // Manually trigger the onabort event - if (hasOnAbort(request)) { - request.onabort(); - } - }); - - //act - transport.send(testPayload, requestTimeout).then(response => { - // assert - try { - assert.strictEqual(response.status, 'failure'); - assert.strictEqual( - (response as ExportResponseFailure).error.message, - 'XHR request aborted' - ); - } catch (e) { - done(e); - } - done(); - }, done /* catch any rejections */); - }); - }); -});