From f654e84d23739b9123d76d684e166feea116e863 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Wed, 18 Jan 2023 00:12:58 +0900 Subject: [PATCH 01/19] feat(otlp-exporter-base): Add fetch sender for ServiceWorker environment. --- experimental/CHANGELOG.md | 2 + .../packages/otlp-exporter-base/package.json | 1 + .../browser/OTLPExporterBrowserBase.ts | 33 +++-- .../src/platform/browser/util.ts | 64 ++++++++- .../test/browser/util.test.ts | 132 +++++++++++++++--- 5 files changed, 202 insertions(+), 30 deletions(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 9416823ca2..0665a8cf9c 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(otlp-exporter-base): Add fetch sender for ServiceWorker environment. [#3542](https://github.com/open-telemetry/opentelemetry-js/pull/3542) @sugi + ### :bug: (Bug Fix) ### :books: (Refine Doc) diff --git a/experimental/packages/otlp-exporter-base/package.json b/experimental/packages/otlp-exporter-base/package.json index 52435a0dfd..09d6ee3704 100644 --- a/experimental/packages/otlp-exporter-base/package.json +++ b/experimental/packages/otlp-exporter-base/package.json @@ -69,6 +69,7 @@ "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", + "fetch-mock": "^9.11.0", "istanbul-instrumenter-loader": "3.0.1", "mocha": "10.0.0", "nock": "13.0.11", diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 57556d81a5..f3e1202f65 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -18,9 +18,10 @@ import { OTLPExporterBase } from '../../OTLPExporterBase'; import { OTLPExporterConfigBase } from '../../types'; import * as otlpTypes from '../../types'; import { parseHeaders } from '../../util'; -import { sendWithBeacon, sendWithXhr } from './util'; +import { sendWithBeacon, sendWithFetch, sendWithXhr } from './util'; import { diag } from '@opentelemetry/api'; import { getEnv, baggageUtils } from '@opentelemetry/core'; +import { _globalThis } from '@opentelemetry/core'; /** * Collector Metric Exporter abstract base class @@ -30,16 +31,21 @@ export abstract class OTLPExporterBrowserBase< ServiceRequest > extends OTLPExporterBase { protected _headers: Record; - private _useXHR: boolean = false; + private sendMethod: 'beacon' | 'xhr' | 'fetch' = 'beacon'; /** * @param config */ constructor(config: OTLPExporterConfigBase = {}) { super(config); - this._useXHR = - !!config.headers || typeof navigator.sendBeacon !== 'function'; - if (this._useXHR) { + if (!!config.headers && typeof navigator.sendBeacon === 'function') { + this.sendMethod = 'beacon'; + } else if (typeof XMLHttpRequest === 'function') { + this.sendMethod = 'xhr'; + } else { + this.sendMethod = 'fetch'; + } + if (this.sendMethod !== 'beacon') { this._headers = Object.assign( {}, parseHeaders(config.headers), @@ -53,11 +59,11 @@ export abstract class OTLPExporterBrowserBase< } onInit(): void { - window.addEventListener('unload', this.shutdown); + _globalThis.addEventListener('unload', this.shutdown); } onShutdown(): void { - window.removeEventListener('unload', this.shutdown); + _globalThis.removeEventListener('unload', this.shutdown); } send( @@ -73,7 +79,7 @@ export abstract class OTLPExporterBrowserBase< const body = JSON.stringify(serviceRequest); const promise = new Promise((resolve, reject) => { - if (this._useXHR) { + if (this.sendMethod === 'xhr') { sendWithXhr( body, this.url, @@ -82,7 +88,16 @@ export abstract class OTLPExporterBrowserBase< resolve, reject ); - } else { + } else if (this.sendMethod === 'fetch') { + sendWithFetch( + body, + this.url, + this._headers, + this.timeoutMillis, + resolve, + reject + ); + } else if (this.sendMethod === 'beacon') { sendWithBeacon( body, this.url, diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 8c311fe0ed..7670fc5c8d 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -16,6 +16,11 @@ import { diag } from '@opentelemetry/api'; import { OTLPExporterError } from '../../types'; +const defaultHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', +}; + /** * Send metrics/spans using browser navigator.sendBeacon * @param body @@ -67,11 +72,6 @@ export function sendWithXhr( const xhr = new XMLHttpRequest(); xhr.open('POST', url); - const defaultHeaders = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - Object.entries({ ...defaultHeaders, ...headers, @@ -101,3 +101,57 @@ export function sendWithXhr( } }; } + +/** + * function to send metrics/spans using browser fetch + * used when navigator.sendBeacon and XMLHttpRequest are not available + * @param body + * @param url + * @param headers + * @param onSuccess + * @param onError + */ +export function sendWithFetch( + body: string, + url: string, + headers: Record, + exporterTimeout: number, + onSuccess: () => void, + onError: (error: OTLPExporterError) => void +): void { + const controller = new AbortController(); + const timerId = setTimeout(() => controller.abort(), exporterTimeout); + fetch(url, { + method: 'POST', + headers: { + ...defaultHeaders, + ...headers, + }, + signal: controller.signal, + body, + }) + .then(response => { + if (response.ok) { + response.text().then( + t => diag.debug('Request Success', t), + () => {} + ); + onSuccess(); + } else { + onError( + new OTLPExporterError( + `Request Error (${response.status} ${response.statusText})`, + response.status + ) + ); + } + }) + .catch((e: Error) => { + if (e.name === 'AbortError') { + onError(new OTLPExporterError('Request Timeout')); + } else { + onError(new OTLPExporterError(`Request Fail: ${e.name} ${e.message}`)); + } + }) + .finally(() => clearTimeout(timerId)); +} diff --git a/experimental/packages/otlp-exporter-base/test/browser/util.test.ts b/experimental/packages/otlp-exporter-base/test/browser/util.test.ts index 1dd3b77d58..cc571768a0 100644 --- a/experimental/packages/otlp-exporter-base/test/browser/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/browser/util.test.ts @@ -14,33 +14,24 @@ * limitations under the License. */ +import * as assert from 'assert'; import * as sinon from 'sinon'; -import { sendWithXhr } from '../../src/platform/browser/util'; +import { sendWithFetch, sendWithXhr } from '../../src/platform/browser/util'; import { nextTick } from 'process'; import { ensureHeadersContain } from '../testHelper'; +import { FetchMockStatic, MockCall } from 'fetch-mock/esm/client'; +const fetchMock = require('fetch-mock/esm/client').default as FetchMockStatic; describe('util - browser', () => { - let server: any; const body = ''; const url = ''; - let onSuccessStub: sinon.SinonStub; - let onErrorStub: sinon.SinonStub; - - beforeEach(() => { - onSuccessStub = sinon.stub(); - onErrorStub = sinon.stub(); - server = sinon.fakeServer.create(); - }); - - afterEach(() => { - server.restore(); - sinon.restore(); - }); - describe('when XMLHTTPRequest is used', () => { + let server: any; let expectedHeaders: Record; let clock: sinon.SinonFakeTimers; + let onSuccessStub: sinon.SinonStub; + let onErrorStub: sinon.SinonStub; beforeEach(() => { // fakeTimers is used to replace the next setTimeout which is // located in sendWithXhr function called by the export method @@ -51,6 +42,13 @@ describe('util - browser', () => { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json', }; + onSuccessStub = sinon.stub(); + onErrorStub = sinon.stub(); + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); + sinon.restore(); }); describe('and Content-Type header is set', () => { beforeEach(() => { @@ -156,4 +154,106 @@ describe('util - browser', () => { }); }); }); + + describe('when fetch is used', () => { + let clock: sinon.SinonFakeTimers; + + const assertRequesetHeaders = ( + call: MockCall | undefined, + expected: Record + ) => { + assert.ok(call); + const headers = call[1]?.headers; + assert.ok(headers, 'invalid header'); + ensureHeadersContain( + Object.fromEntries(Object.entries(headers)), + expected + ); + }; + + beforeEach(() => { + // fakeTimers is used to replace the next setTimeout which is + // located in sendWithXhr function called by the export method + clock = sinon.useFakeTimers(); + + fetchMock.mock(url, {}); + }); + afterEach(() => { + fetchMock.restore(); + clock.restore(); + }); + describe('and Content-Type header is set', () => { + beforeEach(done => { + const explicitContentType = { + 'Content-Type': 'application/json', + }; + const exporterTimeout = 10000; + sendWithFetch( + body, + url, + explicitContentType, + exporterTimeout, + done, + done + ); + }); + it('Request Headers should contain "Content-Type" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + 'Content-Type': 'application/json', + }); + }); + it('Request Headers should contain "Accept" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + Accept: 'application/json', + }); + }); + }); + + describe('and empty headers are set', () => { + beforeEach(done => { + const emptyHeaders = {}; + // use default exporter timeout + const exporterTimeout = 10000; + sendWithFetch(body, url, emptyHeaders, exporterTimeout, done, done); + }); + it('Request Headers should contain "Content-Type" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + 'Content-Type': 'application/json', + }); + }); + it('Request Headers should contain "Accept" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + Accept: 'application/json', + }); + }); + }); + describe('and custom headers are set', () => { + let customHeaders: Record; + beforeEach(done => { + customHeaders = { aHeader: 'aValue', bHeader: 'bValue' }; + const exporterTimeout = 10000; + sendWithFetch(body, url, customHeaders, exporterTimeout, done, done); + }); + it('Request Headers should contain "Content-Type" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + 'Content-Type': 'application/json', + }); + }); + it('Request Headers should contain "Accept" header', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), { + Accept: 'application/json', + }); + }); + it('Request Headers should contain custom headers', () => { + assert.ok(fetchMock.called(url)); + assertRequesetHeaders(fetchMock.lastCall(url), customHeaders); + }); + }); + }); }); From d5951df657da5235e2420dae14970a021d816b26 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Fri, 20 Jan 2023 23:40:50 +0900 Subject: [PATCH 02/19] feat(otlp-exporter-base): fix send method detect condition and more tests. --- .../exporter-trace-otlp-http/package.json | 1 + .../browser/CollectorTraceExporter.test.ts | 196 ++++++++++++++ .../browser/CollectorMetricExporter.test.ts | 245 ++++++++++++++++++ .../browser/OTLPExporterBrowserBase.ts | 2 +- .../src/platform/browser/util.ts | 2 +- 5 files changed, 444 insertions(+), 2 deletions(-) diff --git a/experimental/packages/exporter-trace-otlp-http/package.json b/experimental/packages/exporter-trace-otlp-http/package.json index b1e0be41c7..272fccbd38 100644 --- a/experimental/packages/exporter-trace-otlp-http/package.json +++ b/experimental/packages/exporter-trace-otlp-http/package.json @@ -72,6 +72,7 @@ "babel-loader": "8.2.3", "codecov": "3.8.3", "cpx": "1.5.0", + "fetch-mock": "^9.11.0", "istanbul-instrumenter-loader": "3.0.1", "karma": "6.3.16", "karma-chrome-launcher": "3.1.0", diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 81bc6c6b49..d40ce7594a 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -33,6 +33,8 @@ import { OTLPExporterError, } from '@opentelemetry/otlp-exporter-base'; import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; +import { FetchMockStatic, MockCall } from 'fetch-mock/esm/client'; +const fetchMock = require('fetch-mock/esm/client').default as FetchMockStatic; describe('OTLPTraceExporter - web', () => { let collectorTraceExporter: OTLPTraceExporter; @@ -270,6 +272,140 @@ describe('OTLPTraceExporter - web', () => { }); }); }); + + describe('when both "sendBeacon" and "XMLHTTPRequest" are NOT available (service worker env)', () => { + let clock: sinon.SinonFakeTimers; + let stubXhr: sinon.SinonStub; + const url = 'http://foo.bar.com'; + beforeEach(() => { + clock = sinon.useFakeTimers(); + + stubBeacon.value(undefined); + stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined); + fetchMock.mock(url, 200); + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); + }); + afterEach(() => { + sinon.restore(); + clock.restore(); + fetchMock.restore(); + }); + + it('should successfully send the spans using fetch', done => { + collectorTraceExporter.export(spans, () => { + try { + assert.ok(fetchMock.called(url)); + const request = fetchMock.lastCall(url)?.[1]; + assert.ok(request); + assert.strictEqual(request.method, 'POST'); + + const body = request.body?.toString(); + assert.ok(body); + const json = JSON.parse(body) as IExportTraceServiceRequest; + const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + ensureSpanIsCorrect(span1); + + const resource = json.resourceSpans?.[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + ensureWebResourceIsCorrect(resource); + + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + ensureExportTraceServiceRequestIsSet(json); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(); + const spyLoggerError = sinon.stub(); + const nop = () => {}; + const diagLogger: DiagLogger = { + debug: spyLoggerDebug, + error: spyLoggerError, + info: nop, + verbose: nop, + warn: nop, + }; + + diag.setLogger(diagLogger, DiagLogLevel.ALL); + + collectorTraceExporter.export(spans, () => { + try { + assert.strictEqual(fetchMock.calls('*').length, 1); + assert.ok(fetchMock.called(url)); + + assert.strictEqual( + spyLoggerDebug.args[spyLoggerDebug.callCount - 1][0], + 'Request Success' + ); + assert.strictEqual(spyLoggerError.args.length, 0); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should log the error message', done => { + fetchMock.restore(); + fetchMock.mock(url, 400); + collectorTraceExporter.export(spans, result => { + try { + assert.ok(fetchMock.called(url)); + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('Failed to export')); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should send custom headers', done => { + collectorTraceExporter = new OTLPTraceExporter( + Object.assign({}, collectorExporterConfig, { + headers: { + 'My-Custom-Header1': '123', + 'My-Custom-Header2': 'abc', + }, + }) + ); + + collectorTraceExporter.export(spans, () => { + try { + assert.ok(fetchMock.called(url)); + const request = fetchMock.lastCall(url)?.[1]; + assert.ok(request); + assert.ok(request.headers); + const sentHeadersObj = Object.fromEntries( + Object.entries(request.headers) + ); + assert.strictEqual(sentHeadersObj['My-Custom-Header1'], '123'); + assert.strictEqual(sentHeadersObj['My-Custom-Header2'], 'abc'); + + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + }); }); describe('export - common', () => { @@ -432,6 +568,66 @@ describe('OTLPTraceExporter - web', () => { }); }); }); + + describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available', () => { + let clock: sinon.SinonFakeTimers; + let stubXhr: sinon.SinonStub; + beforeEach(() => { + clock = sinon.useFakeTimers(); + + stubBeacon.value(undefined); + stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined); + fetchMock.mock('*', 200); + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); + }); + afterEach(() => { + sinon.restore(); + clock.restore(); + fetchMock.restore(); + }); + + const assertRequesetHeaders = ( + call: MockCall | undefined, + expected: Record + ) => { + assert.ok(call); + const headers = call[1]?.headers; + assert.ok(headers, 'invalid header'); + ensureHeadersContain( + Object.fromEntries(Object.entries(headers)), + expected + ); + }; + + it('should successfully send spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => { + try { + assert.ok(fetchMock.called('*')); + assertRequesetHeaders(fetchMock.lastCall('*'), customHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + it('should log the timeout request error message', done => { + collectorTraceExporter.export(spans, result => { + try { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + done(); + } catch (e) { + done(e); + } + }); + clock.tick(10000); + }); + }); }); describe('export - concurrency limit', () => { diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts index 7dbc7cc889..84f6d59d69 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts @@ -47,6 +47,8 @@ import { import { OTLPMetricExporterOptions } from '../../src'; import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; +import { FetchMockStatic, MockCall } from 'fetch-mock/esm/client'; +const fetchMock = require('fetch-mock/esm/client').default as FetchMockStatic; describe('OTLPMetricExporter - web', () => { let collectorExporter: OTLPMetricExporter; @@ -371,6 +373,191 @@ describe('OTLPMetricExporter - web', () => { }); }); }); + describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available (service worker env)', () => { + let stubXhr: sinon.SinonStub; + const url = 'http://foo.bar.com'; + beforeEach(() => { + stubBeacon.value(undefined); + stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined); + collectorExporter = new OTLPMetricExporter({ + url, + temporalityPreference: AggregationTemporality.CUMULATIVE, + }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + fetchMock.mock(url, 200); + }); + afterEach(() => { + sinon.restore(); + fetchMock.restore(); + }); + + it('should successfully send the metrics using fetch', done => { + collectorExporter.export(metrics, () => { + try { + assert.ok(fetchMock.called(url)); + const request = fetchMock.lastCall(url)?.[1]; + assert.ok(request); + assert.strictEqual(request.method, 'POST'); + + const body = request.body?.toString(); + assert.ok(body); + + const json = JSON.parse(body) as IExportMetricsServiceRequest; + // The order of the metrics is not guaranteed. + const counterIndex = metrics.scopeMetrics[0].metrics.findIndex( + it => it.descriptor.name === 'int-counter' + ); + const observableIndex = metrics.scopeMetrics[0].metrics.findIndex( + it => it.descriptor.name === 'double-observable-gauge2' + ); + const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex( + it => it.descriptor.name === 'int-histogram' + ); + + const metric1 = + json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; + const metric2 = + json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; + const metric3 = + json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; + + assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); + ensureCounterIsCorrect( + metric1, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] + .endTime + ), + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] + .startTime + ) + ); + + assert.ok( + typeof metric2 !== 'undefined', + "second metric doesn't exist" + ); + ensureObservableGaugeIsCorrect( + metric2, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] + .endTime + ), + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] + .startTime + ), + 6, + 'double-observable-gauge2' + ); + + assert.ok( + typeof metric3 !== 'undefined', + "third metric doesn't exist" + ); + ensureHistogramIsCorrect( + metric3, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] + .endTime + ), + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] + .startTime + ), + [0, 100], + [0, 2, 0] + ); + + const resource = json.resourceMetrics[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + ensureWebResourceIsCorrect(resource); + + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + ensureExportMetricsServiceRequestIsSet(json); + + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should log the successful message', done => { + collectorExporter.export(metrics, () => { + try { + assert.strictEqual(fetchMock.calls('*').length, 1); + assert.ok(fetchMock.called(url)); + + assert.strictEqual( + debugStub.args[debugStub.callCount - 1][0], + 'Request Success' + ); + assert.strictEqual(errorStub.args.length, 0); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should log the error message', done => { + fetchMock.restore(); + fetchMock.mock('*', 400); + collectorExporter.export(metrics, result => { + try { + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('Failed to export')); + assert.strictEqual(stubBeacon.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + it('should send custom headers', done => { + collectorExporter = new OTLPMetricExporter({ + url, + temporalityPreference: AggregationTemporality.CUMULATIVE, + headers: { + 'My-Custom-Header1': '123', + 'My-Custom-Header2': 'abc', + }, + }); + collectorExporter.export(metrics, () => { + try { + assert.ok(fetchMock.called(url)); + const request = fetchMock.lastCall(url)?.[1]; + assert.ok(request); + assert.ok(request.headers); + const sentHeadersObj = Object.fromEntries( + Object.entries(request.headers) + ); + assert.strictEqual(sentHeadersObj['My-Custom-Header1'], '123'); + assert.strictEqual(sentHeadersObj['My-Custom-Header2'], 'abc'); + + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + }); }); describe('export with custom headers', () => { @@ -434,6 +621,64 @@ describe('OTLPMetricExporter - web', () => { }); }); }); + + describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available', () => { + let clock: sinon.SinonFakeTimers; + let stubXhr: sinon.SinonStub; + beforeEach(() => { + clock = sinon.useFakeTimers(); + stubBeacon.value(undefined); + stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined); + fetchMock.mock('*', 200); + collectorExporter = new OTLPMetricExporter(collectorExporterConfig); + }); + afterEach(() => { + clock.restore(); + fetchMock.restore(); + }); + + const assertRequesetHeaders = ( + call: MockCall | undefined, + expected: Record + ) => { + assert.ok(call); + const headers = call[1]?.headers; + assert.ok(headers, 'invalid header'); + ensureHeadersContain( + Object.fromEntries(Object.entries(headers)), + expected + ); + }; + + it('should successfully send metrics using XMLHttpRequest', done => { + collectorExporter.export(metrics, () => { + try { + assert.ok(fetchMock.called('*')); + assertRequesetHeaders(fetchMock.lastCall('*'), customHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); + assert.strictEqual(stubXhr.callCount, 0); + done(); + } catch (e) { + done(e); + } + }); + }); + it('should log the timeout request error message', done => { + collectorExporter.export(metrics, result => { + try { + assert.strictEqual(result.code, ExportResultCode.FAILED); + const error = result.error; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + done(); + } catch (e) { + done(e); + } + }); + clock.tick(10000); + }); + }); }); }); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index f3e1202f65..3c2b19ff11 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -38,7 +38,7 @@ export abstract class OTLPExporterBrowserBase< */ constructor(config: OTLPExporterConfigBase = {}) { super(config); - if (!!config.headers && typeof navigator.sendBeacon === 'function') { + if (!config.headers && typeof navigator.sendBeacon === 'function') { this.sendMethod = 'beacon'; } else if (typeof XMLHttpRequest === 'function') { this.sendMethod = 'xhr'; diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 7670fc5c8d..3d965e3e73 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -140,7 +140,7 @@ export function sendWithFetch( } else { onError( new OTLPExporterError( - `Request Error (${response.status} ${response.statusText})`, + `Failed to export with fetch: (${response.status} ${response.statusText})`, response.status ) ); From 7a367406a220b26c1b182a2939c39bf1e41cbc74 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sat, 28 Jan 2023 19:36:27 +0900 Subject: [PATCH 03/19] fix(otlp-exporter-base): improve fetch error handling. --- .../src/platform/browser/util.ts | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 13c1dfc078..e906369585 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -130,28 +130,32 @@ export function sendWithFetch( signal: controller.signal, body, }) - .then(response => { - if (response.ok) { - response.text().then( - t => diag.debug('Request Success', t), - () => {} - ); - onSuccess(); - } else { - onError( - new OTLPExporterError( - `Failed to export with fetch: (${response.status} ${response.statusText})`, - response.status - ) - ); - } - }) - .catch((e: Error) => { - if (e.name === 'AbortError') { - onError(new OTLPExporterError('Request Timeout')); - } else { - onError(new OTLPExporterError(`Request Fail: ${e.name} ${e.message}`)); + .then( + response => { + if (response.ok) { + response.text().then( + t => diag.debug('Request Success', t), + () => {} + ); + onSuccess(); + } else { + onError( + new OTLPExporterError( + `Failed to export with fetch: (${response.status} ${response.statusText})`, + response.status + ) + ); + } + }, + (e: Error) => { + if (e.name === 'AbortError') { + onError(new OTLPExporterError('Request Timeout')); + } else { + onError( + new OTLPExporterError(`Request Fail: ${e.name} ${e.message}`) + ); + } } - }) + ) .finally(() => clearTimeout(timerId)); } From e5f04545e66a7570239d31e4e3334db440f5e270 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Mon, 6 Mar 2023 21:45:46 +0900 Subject: [PATCH 04/19] feat(otlp-exporter-base): implement retry on fetch sender to compat with xhr. --- .../browser/CollectorTraceExporter.test.ts | 104 ++++++++++++++++++ .../src/platform/browser/util.ts | 75 ++++++++++--- 2 files changed, 161 insertions(+), 18 deletions(-) diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 0e74e441ed..8fce44ca9b 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -897,4 +897,108 @@ describe('export with retry - real http request destroyed', () => { }); }).timeout(3000); }); + + describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available', () => { + const url = 'http://localhost:4318/v1/traces'; + let clock: sinon.SinonFakeTimers | undefined; + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + sinon.stub(globalThis, 'XMLHttpRequest').value(undefined); + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); + }); + afterEach(() => { + clock?.restore(); + fetchMock.reset(); + }) + it('should log the timeout request error message when retrying with exponential backoff with jitter', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + clock = sinon.useFakeTimers(); + + let tries = 0; + fetchMock.mock(url, () => { + tries++; + return 503; + }) + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(tries, 1); + done(); + }); + clock.tick(2000); + }).timeout(3000); + + it('should log the timeout request error message when retry-after header is set to 3 seconds', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + clock = sinon.useFakeTimers(); + + let retry = 0; + fetchMock.mock(url, () => { + retry++; + return {status: 503, headers: { 'Retry-After': 3 }} + }); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(retry, 1); + done(); + }); + clock.tick(2000); + }).timeout(3000); + + it('should log the timeout request error message when retry-after header is a date', async () => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let tries = 0; + fetchMock.mock(url, () => { + tries++; + const d = new Date(); + d.setSeconds(d.getSeconds() + 1); + return {status: 503, headers: { 'Retry-After': d.toUTCString() }} + }); + + await new Promise(r => setTimeout(r, 1000 - Date.now() % 1000)) // wait to start export exactly in seconds + return new Promise(resolve => { + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(tries, 2); + resolve(); + }); + }); + }).timeout(3000); + + it('should log the timeout request error message when retry-after header is a date with long delay', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let tries = 0; + fetchMock.mock(url, () => { + tries++; + const d = new Date(); + d.setSeconds(d.getSeconds() + 120); + return {status: 503, headers: { 'Retry-After': d.toUTCString() }} + }); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(tries, 1); + done(); + }); + }).timeout(3000); + }); }); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 0940a740d7..1c6d60ca7b 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -162,7 +162,6 @@ export function sendWithXhr( sendWithRetry(); } - /** * function to send metrics/spans using browser fetch * used when navigator.sendBeacon and XMLHttpRequest are not available @@ -181,26 +180,60 @@ export function sendWithFetch( onError: (error: OTLPExporterError) => void ): void { const controller = new AbortController(); - const timerId = setTimeout(() => controller.abort(), exporterTimeout); - fetch(url, { - method: 'POST', - headers: { - ...defaultHeaders, - ...headers, - }, - signal: controller.signal, - body, - }) - .then( + let cancelRetry: ((e: OTLPExporterError) => void) | undefined; + const exporterTimer = setTimeout(() => { + controller.abort(); + cancelRetry?.(new OTLPExporterError('Request Timeout')); + }, exporterTimeout); + + const fetchWithRetry = ( + retries = DEFAULT_EXPORT_MAX_ATTEMPTS, + minDelay = DEFAULT_EXPORT_INITIAL_BACKOFF + ) => { + return fetch(url, { + method: 'POST', + headers: { + ...defaultHeaders, + ...headers, + }, + signal: controller.signal, + body, + }).then( response => { if (response.ok) { response.text().then( t => diag.debug('Request Success', t), () => {} ); - onSuccess(); + return; + } else if (isExportRetryable(response.status) && retries > 0) { + let retryTime: number; + minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; + + // retry after interval specified in Retry-After header + if (response.headers.has('Retry-After')) { + retryTime = parseRetryAfterToMills( + response.headers.get('Retry-After') + ); + } else { + // exponential backoff with jitter + retryTime = Math.round( + Math.random() * (DEFAULT_EXPORT_MAX_BACKOFF - minDelay) + minDelay + ); + } + + return new Promise((resolve, reject) => { + const retryTimer = setTimeout(() => { + cancelRetry = undefined; + fetchWithRetry(retries - 1, minDelay).then(resolve, reject); + }, retryTime); + cancelRetry = e => { + clearTimeout(retryTimer); + reject(e); + }; + }); } else { - onError( + return Promise.reject( new OTLPExporterError( `Failed to export with fetch: (${response.status} ${response.statusText})`, response.status @@ -210,13 +243,19 @@ export function sendWithFetch( }, (e: Error) => { if (e.name === 'AbortError') { - onError(new OTLPExporterError('Request Timeout')); + return Promise.reject(new OTLPExporterError('Request Timeout')); } else { - onError( + return Promise.reject( new OTLPExporterError(`Request Fail: ${e.name} ${e.message}`) ); } } + ); + }; + fetchWithRetry() + .then( + () => onSuccess(), + e => onError(e) ) - .finally(() => clearTimeout(timerId)); -} \ No newline at end of file + .finally(() => clearTimeout(exporterTimer)); +} From 8a4311e25db521b8ac2a1006f153e80593c44fbc Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Mon, 6 Mar 2023 22:21:28 +0900 Subject: [PATCH 05/19] chore: lint style fix. --- .../test/browser/CollectorTraceExporter.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 8fce44ca9b..f78f39715d 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -909,7 +909,7 @@ describe('export with retry - real http request destroyed', () => { afterEach(() => { clock?.restore(); fetchMock.reset(); - }) + }); it('should log the timeout request error message when retrying with exponential backoff with jitter', done => { spans = []; spans.push(Object.assign({}, mockedReadableSpan)); @@ -919,7 +919,7 @@ describe('export with retry - real http request destroyed', () => { fetchMock.mock(url, () => { tries++; return 503; - }) + }); collectorTraceExporter.export(spans, result => { assert.strictEqual(result.code, core.ExportResultCode.FAILED); @@ -940,7 +940,7 @@ describe('export with retry - real http request destroyed', () => { let retry = 0; fetchMock.mock(url, () => { retry++; - return {status: 503, headers: { 'Retry-After': 3 }} + return { status: 503, headers: { 'Retry-After': 3 } }; }); collectorTraceExporter.export(spans, result => { @@ -963,10 +963,10 @@ describe('export with retry - real http request destroyed', () => { tries++; const d = new Date(); d.setSeconds(d.getSeconds() + 1); - return {status: 503, headers: { 'Retry-After': d.toUTCString() }} + return { status: 503, headers: { 'Retry-After': d.toUTCString() } }; }); - await new Promise(r => setTimeout(r, 1000 - Date.now() % 1000)) // wait to start export exactly in seconds + await new Promise(r => setTimeout(r, 1000 - (Date.now() % 1000))); // wait to start export exactly in seconds return new Promise(resolve => { collectorTraceExporter.export(spans, result => { assert.strictEqual(result.code, core.ExportResultCode.FAILED); @@ -988,7 +988,7 @@ describe('export with retry - real http request destroyed', () => { tries++; const d = new Date(); d.setSeconds(d.getSeconds() + 120); - return {status: 503, headers: { 'Retry-After': d.toUTCString() }} + return { status: 503, headers: { 'Retry-After': d.toUTCString() } }; }); collectorTraceExporter.export(spans, result => { From 692715464912bb639b964cd42a8b36b57159c29c Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:29:25 +0900 Subject: [PATCH 06/19] chore: fix test description. Co-authored-by: Marc Pichler --- .../test/browser/CollectorTraceExporter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index f78f39715d..96ac56397c 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -599,7 +599,7 @@ describe('OTLPTraceExporter - web', () => { ); }; - it('should successfully send spans using XMLHttpRequest', done => { + it('should successfully send spans using fetch', done => { collectorTraceExporter.export(spans, () => { try { assert.ok(fetchMock.called('*')); From a613a5b42cb4fcc328d64746f87282a00c658bd0 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:29:41 +0900 Subject: [PATCH 07/19] chore: fix test description. Co-authored-by: Marc Pichler --- .../test/browser/CollectorMetricExporter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts index 84f6d59d69..17bb066c38 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts @@ -650,7 +650,7 @@ describe('OTLPMetricExporter - web', () => { ); }; - it('should successfully send metrics using XMLHttpRequest', done => { + it('should successfully send metrics using fetch', done => { collectorExporter.export(metrics, () => { try { assert.ok(fetchMock.called('*')); From 11cb4a3c1bc2dd39eb37c0f5573872319af0df93 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:30:41 +0900 Subject: [PATCH 08/19] chore: pin package version. Co-authored-by: Marc Pichler --- experimental/packages/otlp-exporter-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/otlp-exporter-base/package.json b/experimental/packages/otlp-exporter-base/package.json index 7609989c98..9f7f45a226 100644 --- a/experimental/packages/otlp-exporter-base/package.json +++ b/experimental/packages/otlp-exporter-base/package.json @@ -69,7 +69,7 @@ "@types/node": "18.6.5", "@types/sinon": "10.0.13", "codecov": "3.8.3", - "fetch-mock": "^9.11.0", + "fetch-mock": "9.11.0", "istanbul-instrumenter-loader": "3.0.1", "mocha": "10.0.0", "nock": "13.0.11", From af91d48e1be4c1b06badd75dd6eb1ca49e3f8fa9 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:30:51 +0900 Subject: [PATCH 09/19] chore: pin package version. Co-authored-by: Marc Pichler --- experimental/packages/exporter-trace-otlp-http/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/exporter-trace-otlp-http/package.json b/experimental/packages/exporter-trace-otlp-http/package.json index d26337f7ab..930ccd7704 100644 --- a/experimental/packages/exporter-trace-otlp-http/package.json +++ b/experimental/packages/exporter-trace-otlp-http/package.json @@ -72,7 +72,7 @@ "babel-loader": "8.2.3", "codecov": "3.8.3", "cpx": "1.5.0", - "fetch-mock": "^9.11.0", + "fetch-mock": "9.11.0", "istanbul-instrumenter-loader": "3.0.1", "karma": "6.3.16", "karma-chrome-launcher": "3.1.0", From 1dda10676243091c63238bffd56af1e894939d28 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:36:15 +0900 Subject: [PATCH 10/19] chore: fix typo https://github.com/open-telemetry/opentelemetry-js/pull/3542#discussion_r1127632098 --- .../test/browser/CollectorTraceExporter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 96ac56397c..88cee54e75 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -586,7 +586,7 @@ describe('OTLPTraceExporter - web', () => { fetchMock.restore(); }); - const assertRequesetHeaders = ( + const assertRequestHeaders = ( call: MockCall | undefined, expected: Record ) => { @@ -603,7 +603,7 @@ describe('OTLPTraceExporter - web', () => { collectorTraceExporter.export(spans, () => { try { assert.ok(fetchMock.called('*')); - assertRequesetHeaders(fetchMock.lastCall('*'), customHeaders); + assertRequestHeaders(fetchMock.lastCall('*'), customHeaders); assert.strictEqual(stubBeacon.callCount, 0); assert.strictEqual(stubOpen.callCount, 0); assert.strictEqual(stubXhr.callCount, 0); From c52538e07405482d74049bdc52b1e80779b2ee23 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 7 May 2023 19:42:56 +0900 Subject: [PATCH 11/19] chore: merge imports. Co-authored-by: Marc Pichler --- .../src/platform/browser/OTLPExporterBrowserBase.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 3c2b19ff11..9d0967824a 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -20,8 +20,7 @@ import * as otlpTypes from '../../types'; import { parseHeaders } from '../../util'; import { sendWithBeacon, sendWithFetch, sendWithXhr } from './util'; import { diag } from '@opentelemetry/api'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; -import { _globalThis } from '@opentelemetry/core'; +import { getEnv, baggageUtils, _globalThis } from '@opentelemetry/core'; /** * Collector Metric Exporter abstract base class From 975f9f985d715f85fc3f0f75b76392e4afb03b99 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Thu, 19 Oct 2023 23:06:30 +0900 Subject: [PATCH 12/19] fix: stop to check response body and check return code; https://github.com/open-telemetry/opentelemetry-js/pull/3542#discussion_r1219900394 --- .../otlp-exporter-base/src/platform/browser/util.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 1c6d60ca7b..2d2de4c846 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -200,13 +200,9 @@ export function sendWithFetch( body, }).then( response => { - if (response.ok) { - response.text().then( - t => diag.debug('Request Success', t), - () => {} - ); + if (response.status >= 200 && response.status <= 299) { return; - } else if (isExportRetryable(response.status) && retries > 0) { + } else if (response.status && isExportRetryable(response.status) && retries > 0) { let retryTime: number; minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; From da0c60774af9d43653451009b347cb60b31e2779 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Thu, 19 Oct 2023 23:07:47 +0900 Subject: [PATCH 13/19] chore: use Enum https://github.com/open-telemetry/opentelemetry-js/pull/3542#discussion_r1362570060 --- .../browser/OTLPExporterBrowserBase.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 93de7812b6..763e33acff 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -22,6 +22,12 @@ import { sendWithBeacon, sendWithFetch, sendWithXhr } from './util'; import { diag } from '@opentelemetry/api'; import { getEnv, baggageUtils, _globalThis } from '@opentelemetry/core'; +enum SendMethod { + beacon = 1, + xhr = 2, + fetch = 3, +} + /** * Collector Metric Exporter abstract base class */ @@ -30,7 +36,7 @@ export abstract class OTLPExporterBrowserBase< ServiceRequest, > extends OTLPExporterBase { protected _headers: Record; - private sendMethod: 'beacon' | 'xhr' | 'fetch' = 'beacon'; + private sendMethod: SendMethod; /** * @param config @@ -38,13 +44,13 @@ export abstract class OTLPExporterBrowserBase< constructor(config: OTLPExporterConfigBase = {}) { super(config); if (!config.headers && typeof navigator.sendBeacon === 'function') { - this.sendMethod = 'beacon'; + this.sendMethod = SendMethod.beacon } else if (typeof XMLHttpRequest === 'function') { - this.sendMethod = 'xhr'; + this.sendMethod = SendMethod.xhr } else { - this.sendMethod = 'fetch'; + this.sendMethod = SendMethod.fetch; } - if (this.sendMethod !== 'beacon') { + if (this.sendMethod !== SendMethod.beacon) { this._headers = Object.assign( {}, parseHeaders(config.headers), @@ -78,7 +84,7 @@ export abstract class OTLPExporterBrowserBase< const body = JSON.stringify(serviceRequest); const promise = new Promise((resolve, reject) => { - if (this.sendMethod === 'xhr') { + if (this.sendMethod === SendMethod.xhr) { sendWithXhr( body, this.url, @@ -87,7 +93,7 @@ export abstract class OTLPExporterBrowserBase< resolve, reject ); - } else if (this.sendMethod === 'fetch') { + } else if (this.sendMethod === SendMethod.fetch) { sendWithFetch( body, this.url, @@ -96,7 +102,7 @@ export abstract class OTLPExporterBrowserBase< resolve, reject ); - } else if (this.sendMethod === 'beacon') { + } else if (this.sendMethod === SendMethod.beacon) { sendWithBeacon( body, this.url, From f40977352ff2d948abfcdd414d1b2036bf2e12fc Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sat, 4 Nov 2023 22:24:43 +0900 Subject: [PATCH 14/19] chore: add diag message for test. --- .../packages/otlp-exporter-base/src/platform/browser/util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index 2d2de4c846..b9c61e71ab 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -201,6 +201,7 @@ export function sendWithFetch( }).then( response => { if (response.status >= 200 && response.status <= 299) { + diag.debug('Request Success'); return; } else if (response.status && isExportRetryable(response.status) && retries > 0) { let retryTime: number; From 16909ad3aabdc4fe059c5717e61bc112811d0fc3 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sat, 4 Nov 2023 22:28:25 +0900 Subject: [PATCH 15/19] chore: import current package-lock.json (adding fetch-mock) --- package-lock.json | 205 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7bc1150410..c30fc2a49e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1475,6 +1475,7 @@ "codecov": "3.8.3", "cpx": "1.5.0", "cross-var": "1.1.0", + "fetch-mock": "9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", @@ -2986,6 +2987,7 @@ "babel-plugin-istanbul": "6.1.1", "codecov": "3.8.3", "cross-var": "1.1.0", + "fetch-mock": "9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", @@ -17467,6 +17469,91 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "charity", + "url": "https://www.justgiving.com/refugee-support-europe" + }, + "peerDependencies": { + "node-fetch": "*" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/fetch-mock/node_modules/core-js": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz", + "integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/fetch-mock/node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fetch-mock/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/fetch-mock/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -20177,6 +20264,12 @@ "node": ">=8" } }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "node_modules/is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -22094,6 +22187,12 @@ "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -22128,6 +22227,12 @@ "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -27648,6 +27753,16 @@ "node": ">=0.6" } }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -40863,6 +40978,7 @@ "codecov": "3.8.3", "cpx": "1.5.0", "cross-var": "1.1.0", + "fetch-mock": "9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", @@ -41813,6 +41929,7 @@ "babel-plugin-istanbul": "6.1.1", "codecov": "3.8.3", "cross-var": "1.1.0", + "fetch-mock": "9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", @@ -50470,6 +50587,70 @@ "pend": "~1.2.0" } }, + "fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz", + "integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==", + "dev": true + }, + "path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -52542,6 +52723,12 @@ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -54055,6 +54242,12 @@ "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -54089,6 +54282,12 @@ "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -58421,6 +58620,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "dev": true + }, "querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", From 8bda0e1331fa87e048f4e1b0ccd8559102805db9 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sat, 4 Nov 2023 22:28:51 +0900 Subject: [PATCH 16/19] fix: Move entry to unreleased. --- experimental/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 08e3ba0d6e..9dc7bec93f 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(otlp-exporter-base): Add fetch sender for WebWorker and ServiceWorker environment. [#3542](https://github.com/open-telemetry/opentelemetry-js/pull/3542) @sugi + ### :bug: (Bug Fix) * fix(sdk-node): remove the explicit dependency on @opentelemetry/exporter-jaeger that was kept on the previous release @@ -134,7 +136,6 @@ All notable changes to experimental packages in this project will be documented * feat(otlp-metric-exporters): Add User-Agent header to OTLP metric exporters. [#3806](https://github.com/open-telemetry/opentelemetry-js/pull/3806) @JamieDanielson * feat(opencensus-shim): add OpenCensus trace shim [#3809](https://github.com/open-telemetry/opentelemetry-js/pull/3809) @aabmass * feat(exporter-logs-otlp-proto): protobuf exporter for logs. [#3779](https://github.com/open-telemetry/opentelemetry-js/pull/3779) @Abinet18 -* feat(otlp-exporter-base): Add fetch sender for WebWorker and ServiceWorker environment. [#3542](https://github.com/open-telemetry/opentelemetry-js/pull/3542) @sugi ### :bug: (Bug Fix) From 72db4f2fba6e9e8f90a33351b71bdaf04da0940b Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sat, 4 Nov 2023 22:36:21 +0900 Subject: [PATCH 17/19] chore: update submodules to match with upstream/main. --- experimental/packages/otlp-grpc-exporter-base/protos | 2 +- experimental/packages/otlp-proto-exporter-base/protos | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/packages/otlp-grpc-exporter-base/protos b/experimental/packages/otlp-grpc-exporter-base/protos index c5c8b28012..1608f92cf0 160000 --- a/experimental/packages/otlp-grpc-exporter-base/protos +++ b/experimental/packages/otlp-grpc-exporter-base/protos @@ -1 +1 @@ -Subproject commit c5c8b28012583fda55b0cb16f73a820722171d49 +Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 diff --git a/experimental/packages/otlp-proto-exporter-base/protos b/experimental/packages/otlp-proto-exporter-base/protos index c5c8b28012..1608f92cf0 160000 --- a/experimental/packages/otlp-proto-exporter-base/protos +++ b/experimental/packages/otlp-proto-exporter-base/protos @@ -1 +1 @@ -Subproject commit c5c8b28012583fda55b0cb16f73a820722171d49 +Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 From 14975a8415bcdb2cca226e60d1861d45cbdfc384 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Wed, 15 Nov 2023 01:31:47 +0900 Subject: [PATCH 18/19] chore: fix by lint. --- .../test/browser/CollectorMetricExporter.test.ts | 3 ++- .../platform/browser/OTLPExporterBrowserBase.ts | 4 ++-- .../src/platform/browser/util.ts | 6 +++++- .../src/platform/browser/util.ts | 14 ++++++++------ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts index df4028db38..eb3eb74ba9 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts @@ -401,7 +401,8 @@ describe('OTLPMetricExporter - web', () => { assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); ensureCounterIsCorrect( metric1, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime, + metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] + .endTime, metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] .startTime ); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 763e33acff..55b1a4ead4 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -44,9 +44,9 @@ export abstract class OTLPExporterBrowserBase< constructor(config: OTLPExporterConfigBase = {}) { super(config); if (!config.headers && typeof navigator.sendBeacon === 'function') { - this.sendMethod = SendMethod.beacon + this.sendMethod = SendMethod.beacon; } else if (typeof XMLHttpRequest === 'function') { - this.sendMethod = SendMethod.xhr + this.sendMethod = SendMethod.xhr; } else { this.sendMethod = SendMethod.fetch; } diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index b9c61e71ab..480b1219da 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -203,7 +203,11 @@ export function sendWithFetch( if (response.status >= 200 && response.status <= 299) { diag.debug('Request Success'); return; - } else if (response.status && isExportRetryable(response.status) && retries > 0) { + } else if ( + response.status && + isExportRetryable(response.status) && + retries > 0 + ) { let retryTime: number; minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts index 220ff4a526..0e2bb9d16c 100644 --- a/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts +++ b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts @@ -34,7 +34,7 @@ export function prepareSend( ): zipkinTypes.SendFn { let xhrHeaders: Record; const useBeacon = typeof navigator.sendBeacon === 'function' && !headers; - const xhr = typeof XMLHttpRequest === 'function'; + const xhr = typeof XMLHttpRequest === 'function'; if (headers) { xhrHeaders = { Accept: 'application/json', @@ -148,9 +148,11 @@ function sendWithFetch( headers: Record = {} ) { diag.debug(`Zipkin request payload: ${data}`); - fetch(urlStr, {method: 'POST', body: data, headers}).then( - (response) => { - diag.debug(`Zipkin response status code: ${response.status}, body: ${data}`); + fetch(urlStr, { method: 'POST', body: data, headers }).then( + response => { + diag.debug( + `Zipkin response status code: ${response.status}, body: ${data}` + ); if (response.status >= 200 && response.status < 400) { return done({ code: ExportResultCode.SUCCESS }); } else { @@ -162,9 +164,9 @@ function sendWithFetch( }); } }, - (error) => { + error => { globalErrorHandler(new Error(`Zipkin request error: ${error.message}`)); return done({ code: ExportResultCode.FAILED }); } - ) + ); } From 45a8f76ddabb60e80e7eaaabb09668f72123b462 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Sun, 11 Feb 2024 23:02:51 +0900 Subject: [PATCH 19/19] chore: Add fetch mode test for zipkinExporter. Signed-off-by: Tatsuki Sugiura --- package-lock.json | 2 + .../package.json | 1 + .../test/browser/zipkin.test.ts | 60 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/package-lock.json b/package-lock.json index e38d1a9add..6e2aa44ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33964,6 +33964,7 @@ "babel-plugin-istanbul": "6.1.1", "codecov": "3.8.3", "cross-var": "1.1.0", + "fetch-mock": "^9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", @@ -40638,6 +40639,7 @@ "babel-plugin-istanbul": "6.1.1", "codecov": "3.8.3", "cross-var": "1.1.0", + "fetch-mock": "^9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index 1c815a0329..9c5eaef2a2 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -71,6 +71,7 @@ "babel-plugin-istanbul": "6.1.1", "codecov": "3.8.3", "cross-var": "1.1.0", + "fetch-mock": "^9.11.0", "karma": "6.4.2", "karma-chrome-launcher": "3.1.0", "karma-coverage": "2.2.1", diff --git a/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts b/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts index e9d2c083a6..22cc0b862e 100644 --- a/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts +++ b/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts @@ -28,8 +28,11 @@ import { ensureSpanIsCorrect, mockedReadableSpan, } from '../helper'; +import { FetchMockStatic } from 'fetch-mock/esm/client'; +const fetchMock = require('fetch-mock/esm/client').default as FetchMockStatic; const sendBeacon = navigator.sendBeacon; +const xhr = window.XMLHttpRequest; describe('Zipkin Exporter - web', () => { let zipkinExporter: ZipkinExporter; @@ -97,6 +100,30 @@ describe('Zipkin Exporter - web', () => { }); }); + describe('when both sendBeacon and XHR are NOT available', () => { + const url = 'http://localhos/test-' + Math.random(); + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + (window as any).XMLHttpRequest = false; + zipkinExporter = new ZipkinExporter({ ...zipkinConfig, url }); + }); + afterEach(() => { + fetchMock.restore(); + (window as any).XMLHttpRequest = xhr; + (window.navigator as any).sendBeacon = sendBeacon; + }); + + it('should successfully send custom headers using fetch', () => { + fetchMock.mock(url, 200); + zipkinExporter.export(spans, () => {}); + assert.strictEqual(fetchMock.calls(url).length, 1); + const json = JSON.parse( + fetchMock.lastCall(url)?.[1]?.body?.toString() || '' + ) as any; + ensureSpanIsCorrect(json[0]); + }); + }); + describe('should use url defined in environment', () => { let server: any; const endpointUrl = 'http://localhost:9412'; @@ -178,6 +205,39 @@ describe('Zipkin Exporter - web', () => { server.restore(); }); + describe('when both sendBeacon and XHR are NOT available', () => { + const url = 'http://localhos/test-' + Math.random(); + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + (window as any).XMLHttpRequest = false; + zipkinExporter = new ZipkinExporter({ ...zipkinConfig, url }); + }); + afterEach(() => { + fetchMock.restore(); + (window as any).XMLHttpRequest = xhr; + (window.navigator as any).sendBeacon = sendBeacon; + }); + + it('should successfully send custom headers using fetch', done => { + fetchMock.mock(url, 200); + new Promise(r => { + zipkinExporter.export(spans, r); + }).then(() => { + setTimeout(() => { + assert.strictEqual(spyBeacon.callCount, 0); + assert.strictEqual(fetchMock.calls(url).length, 1); + + const request = fetchMock.lastCall(url)?.[1]; + const sentHeadersObj = Object.fromEntries( + Object.entries(request?.headers || {}) + ); + ensureHeadersContain(sentHeadersObj, customHeaders); + done(); + }); + }); + }); + }); + describe('when "sendBeacon" is available', () => { beforeEach(() => { zipkinExporter = new ZipkinExporter(zipkinConfig);