From 6e6843829cda10e702a1646ca1a3ea2d8c4da981 Mon Sep 17 00:00:00 2001 From: Nikolaos Achilles Date: Tue, 27 Jul 2021 15:45:51 +0300 Subject: [PATCH] fix: refacrtoring and solves #2321 --- .../browser/CollectorExporterBrowserBase.ts | 26 +- .../src/platform/browser/util.ts | 10 +- .../src/types.ts | 4 + .../browser/CollectorMetricExporter.test.ts | 25 +- .../browser/CollectorTraceExporter.test.ts | 1096 +++++++++++++---- .../test/browser/util.test.ts | 128 -- 6 files changed, 928 insertions(+), 361 deletions(-) delete mode 100644 packages/opentelemetry-exporter-collector/test/browser/util.test.ts diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts index 3c5ed1fbf1..081a3ee092 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts @@ -33,6 +33,12 @@ export abstract class CollectorExporterBrowserBase< ExportItem, ServiceRequest > { + private DEFAULT_HEADERS = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + private DEFAULT_BLOB_TYPE = { type: 'application/json' }; + protected _headers: Record; private _useXHR: boolean = false; @@ -43,16 +49,20 @@ export abstract class CollectorExporterBrowserBase< super(config); this._useXHR = !!config.headers || typeof navigator.sendBeacon !== 'function'; + if (this._useXHR) { - this._headers = Object.assign( - {}, - parseHeaders(config.headers), - baggageUtils.parseKeyPairsIntoRecord( + this._headers = { + ...this.DEFAULT_HEADERS, + ...parseHeaders(config.headers), + ...baggageUtils.parseKeyPairsIntoRecord( getEnv().OTEL_EXPORTER_OTLP_HEADERS - ) - ); + ), + }; } else { - this._headers = {}; + this._headers = { + ...this.DEFAULT_BLOB_TYPE, + ...parseHeaders({ type: config.blobType }), + }; } } @@ -94,7 +104,7 @@ export abstract class CollectorExporterBrowserBase< if (this._useXHR) { sendWithXhr(body, this.url, this._headers, _onSuccess, _onError); } else { - sendWithBeacon(body, this.url, _onSuccess, _onError); + sendWithBeacon(body, this.url, this._headers, _onSuccess, _onError); } }); this._sendingPromises.push(promise); diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/util.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/util.ts index 5c57517235..24f6e498df 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/util.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/util.ts @@ -25,10 +25,12 @@ import * as collectorTypes from '../../types'; export function sendWithBeacon( body: string, url: string, + headers: Record, onSuccess: () => void, onError: (error: collectorTypes.CollectorExporterError) => void ) { - if (navigator.sendBeacon(url, body)) { + const blob = new Blob([body], headers); + if (navigator.sendBeacon(url, blob)) { diag.debug('sendBeacon - can send', body); onSuccess(); } else { @@ -56,13 +58,7 @@ export function sendWithXhr( const xhr = new XMLHttpRequest(); xhr.open('POST', url); - const defaultHeaders = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }; - Object.entries({ - ...defaultHeaders, ...headers, }).forEach(([k, v]) => { xhr.setRequestHeader(k, v); diff --git a/packages/opentelemetry-exporter-collector/src/types.ts b/packages/opentelemetry-exporter-collector/src/types.ts index e8ae3ff65e..4068e3b181 100644 --- a/packages/opentelemetry-exporter-collector/src/types.ts +++ b/packages/opentelemetry-exporter-collector/src/types.ts @@ -341,6 +341,10 @@ export interface ExportServiceError { * Collector Exporter base config */ export interface CollectorExporterConfigBase { + /** + * type of the data contained in the Blob + */ + blobType?: string; headers?: Partial>; hostname?: string; attributes?: SpanAttributes; diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts index d5208d20a0..c8586cd1ee 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts @@ -64,8 +64,8 @@ describe('CollectorMetricExporter - web', () => { }, 'double-observer2' ); - const recorder: Metric & - ValueRecorder = mockValueRecorder(); + const recorder: Metric & ValueRecorder = + mockValueRecorder(); counter.add(1); recorder.record(7); recorder.record(14); @@ -93,10 +93,11 @@ describe('CollectorMetricExporter - web', () => { it('should successfully send metrics using sendBeacon', done => { collectorExporter.export(metrics, () => {}); - setTimeout(() => { + setTimeout(async () => { const args = stubBeacon.args[0]; const url = args[0]; - const body = args[1]; + const blob: Blob = args[1]; + const body = await blob.text(); const json = JSON.parse( body ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; @@ -158,6 +159,22 @@ describe('CollectorMetricExporter - web', () => { }); }); + it('should successfully send blob type using sendBeacon', done => { + collectorExporter.export(metrics, () => {}); + const expectedType = 'application/json'; + + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; + + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); + }); + it('should log the successful message', done => { // Need to stub/spy on the underlying logger as the "diag" instance is global const spyLoggerDebug = sinon.stub(diag, 'debug'); diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts index f0b8e6a1d2..400b362ea1 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts @@ -51,301 +51,969 @@ describe('CollectorTraceExporter - web', () => { }); describe('export', () => { - beforeEach(() => { - collectorExporterConfig = { - hostname: 'foo', - attributes: {}, - url: 'http://foo.bar.com', - }; - }); - describe('when "sendBeacon" is available', () => { + // typeof navigator.sendBeacon is function beforeEach(() => { - collectorTraceExporter = new CollectorTraceExporter( - collectorExporterConfig - ); + collectorExporterConfig = { + hostname: 'foo', + attributes: {}, + url: 'http://foo.bar.com', + }; }); + describe('and user defines headers', () => { + let server: any; + let userDefinedHeaders: Record; + beforeEach(() => { + userDefinedHeaders = { + foo: 'bar', + bar: 'baz', + }; + collectorExporterConfig = { + ...collectorExporterConfig, + headers: { ...userDefinedHeaders }, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); + }); - it('should successfully send the spans using sendBeacon', done => { - collectorTraceExporter.export(spans, () => {}); + it('should successfully send user defined headers using XMLHttpRequest', done => { + const expectedHeaders = { + ...userDefinedHeaders, + }; - setTimeout(() => { - const args = stubBeacon.args[0]; - const url = args[0]; - const body = args[1]; - const json = JSON.parse( - body - ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; - const span1 = - json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + collectorTraceExporter.export(spans, () => {}); - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - if (span1) { - ensureSpanIsCorrect(span1); - } + setTimeout(() => { + const [{ requestHeaders }] = server.requests; - const resource = json.resourceSpans[0].resource; - assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); - if (resource) { - ensureWebResourceIsCorrect(resource); - } + ensureHeadersContain(requestHeaders, expectedHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); - assert.strictEqual(url, 'http://foo.bar.com'); - assert.strictEqual(stubBeacon.callCount, 1); + done(); + }); + }); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + 'Content-Type': 'application/json;charset=utf-8', + }; + + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); - assert.strictEqual(stubOpen.callCount, 0); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); + }); + describe('and user does not define headers', () => { + beforeEach(() => { + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + it('should successfully send the spans using sendBeacon', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(async () => { + const args = stubBeacon.args[0]; + const url = args[0]; + const blob: Blob = args[1]; + const body = await blob.text(); + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(stubBeacon.callCount, 1); + + assert.strictEqual(stubOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); + + it('should successfully send expected blob type using sendBeacon', done => { + const expectedType = 'application/json'; + collectorTraceExporter.export(spans, () => {}); - ensureExportTraceServiceRequestIsSet(json); + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; - done(); + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); }); }); + describe('and user sets headers as undefined', () => { + beforeEach(() => { + collectorExporterConfig = { + ...collectorExporterConfig, + headers: undefined, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + it('should successfully send the spans using sendBeacon', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(async () => { + const args = stubBeacon.args[0]; + const url = args[0]; + const blob: Blob = args[1]; + const body = await blob.text(); + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(stubBeacon.callCount, 1); + + assert.strictEqual(stubOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - it('should log the successful message', done => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const spyLoggerDebug = sinon.stub(diag, 'debug'); - const spyLoggerError = sinon.stub(diag, 'error'); - stubBeacon.returns(true); + it('should successfully send expected blob type using sendBeacon', done => { + const expectedType = 'application/json'; + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; + + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); + }); + }); + describe('and user defines blob type', () => { + let userDefineBlobType: string; + beforeEach(() => { + userDefineBlobType = 'plain/text'; + collectorExporterConfig = { + ...collectorExporterConfig, + blobType: userDefineBlobType, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + it('should successfully send the spans using sendBeacon', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(async () => { + const args = stubBeacon.args[0]; + const url = args[0]; + const blob: Blob = args[1]; + const body = await blob.text(); + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(stubBeacon.callCount, 1); + + assert.strictEqual(stubOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - collectorTraceExporter.export(spans, () => {}); + it('should successfully send user defined blob type using sendBeacon', done => { + const expectedType = userDefineBlobType; + collectorTraceExporter.export(spans, () => {}); - setTimeout(() => { - const response: any = spyLoggerDebug.args[1][0]; - assert.strictEqual(response, 'sendBeacon - can send'); - assert.strictEqual(spyLoggerError.args.length, 0); + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; - done(); + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); }); }); + describe('and user does not define blob type', () => { + beforeEach(() => { + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + it('should successfully send the spans using sendBeacon', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(async () => { + const args = stubBeacon.args[0]; + const url = args[0]; + const blob: Blob = args[1]; + const body = await blob.text(); + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(stubBeacon.callCount, 1); + + assert.strictEqual(stubOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); + + it('should successfully send expected blob type using sendBeacon', done => { + const expectedType = 'application/json'; + collectorTraceExporter.export(spans, () => {}); - it('should log the error message', done => { - stubBeacon.returns(false); + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; - collectorTraceExporter.export(spans, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('cannot send')); - done(); + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); }); }); - }); + describe('and user defines blob type as undefined', () => { + beforeEach(() => { + collectorExporterConfig = { + ...collectorExporterConfig, + blobType: undefined, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + it('should successfully send the spans using sendBeacon', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(async () => { + const args = stubBeacon.args[0]; + const url = args[0]; + const blob: Blob = args[1]; + const body = await blob.text(); + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(stubBeacon.callCount, 1); + + assert.strictEqual(stubOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); + + it('should successfully send expected blob type using sendBeacon', done => { + const expectedType = 'application/json'; + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const args = stubBeacon.args[0]; + const blob: Blob = args[1]; + const { type } = blob; + + assert.strictEqual(type, expectedType); + assert.strictEqual(stubBeacon.callCount, 1); + assert.strictEqual(stubOpen.callCount, 0); + done(); + }); + }); + }); + describe('should be able to handle logs', () => { + // Need to stub/spy on the underlying logger as the "diag" instance is global + let spyLoggerDebug: sinon.SinonStub; + let spyLoggerError: sinon.SinonStub; + beforeEach(() => { + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + spyLoggerDebug = sinon.stub(diag, 'debug'); + spyLoggerError = sinon.stub(diag, 'error'); + }); + it('when successful with can-send message', done => { + stubBeacon.returns(true); + + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'sendBeacon - can send'); + assert.strictEqual(spyLoggerError.args.length, 0); + + done(); + }); + }); + it('when errored with cannot send message', done => { + stubBeacon.returns(false); + + collectorTraceExporter.export(spans, result => { + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('cannot send')); + done(); + }); + }); + }); + }); describe('when "sendBeacon" is NOT available', () => { let server: any; beforeEach(() => { + collectorExporterConfig = { + hostname: 'foo', + attributes: {}, + url: 'http://foo.bar.com', + }; (window.navigator as any).sendBeacon = false; - collectorTraceExporter = new CollectorTraceExporter( - collectorExporterConfig - ); server = sinon.fakeServer.create(); }); afterEach(() => { server.restore(); }); + describe('and user defines headers', () => { + let server: any; + let userDefinedHeaders: Record; + beforeEach(() => { + userDefinedHeaders = { + foo: 'bar', + bar: 'baz', + }; + + collectorExporterConfig = { + ...collectorExporterConfig, + headers: { ...userDefinedHeaders }, + }; + + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + server = sinon.fakeServer.create(); + }); - it('should successfully send the spans using XMLHttpRequest', done => { - collectorTraceExporter.export(spans, () => {}); + afterEach(() => { + server.restore(); + }); - setTimeout(() => { - const request = server.requests[0]; - assert.strictEqual(request.method, 'POST'); - assert.strictEqual(request.url, 'http://foo.bar.com'); + it('Request Headers should contain user defined headers', done => { + const expectedHeaders = { ...userDefinedHeaders }; + collectorTraceExporter.export(spans, () => {}); - const body = request.requestBody; - const json = JSON.parse( - body - ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; - const span1 = - json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + setTimeout(() => { + const [{ requestHeaders }] = server.requests; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - if (span1) { - ensureSpanIsCorrect(span1); - } + ensureHeadersContain(requestHeaders, expectedHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); - const resource = json.resourceSpans[0].resource; - assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); - if (resource) { - ensureWebResourceIsCorrect(resource); - } + done(); + }); + }); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); - assert.strictEqual(stubBeacon.callCount, 0); + setTimeout(() => { + const [{ requestHeaders }] = server.requests; - ensureExportTraceServiceRequestIsSet(json); + ensureHeadersContain(requestHeaders, expectedHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); - done(); + done(); + }); }); - }); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); - it('should log the successful message', done => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const spyLoggerDebug = sinon.stub(diag, 'debug'); - const spyLoggerError = sinon.stub(diag, 'error'); + setTimeout(() => { + const [{ requestHeaders }] = server.requests; + + ensureHeadersContain(requestHeaders, expectedHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); - collectorTraceExporter.export(spans, () => {}); + done(); + }); + }); + }); + describe('and user does not define headers', () => { + beforeEach(() => { + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); - setTimeout(() => { - const request = server.requests[0]; - request.respond(200); + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(stubBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - const response: any = spyLoggerDebug.args[1][0]; - assert.strictEqual(response, 'xhr success'); - assert.strictEqual(spyLoggerError.args.length, 0); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + // ;charset=utf-8 is applied by sinon.fakeServer + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); - assert.strictEqual(stubBeacon.callCount, 0); - done(); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); }); }); + describe('and user sets headers as undefined', () => { + beforeEach(() => { + collectorExporterConfig = { + ...collectorExporterConfig, + headers: undefined, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); + + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(stubBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - it('should log the error message', done => { - collectorTraceExporter.export(spans, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('Failed to export')); - done(); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + // ;charset=utf-8 is applied by sinon.fakeServer + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); }); - setTimeout(() => { - const request = server.requests[0]; - request.respond(400); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); }); }); + describe('and user defines blob type', () => { + let userDefineBlobType: string; + beforeEach(() => { + userDefineBlobType = 'plain/text'; + collectorExporterConfig = { + ...collectorExporterConfig, + blobType: userDefineBlobType, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); - it('should send custom headers', done => { - collectorTraceExporter.export(spans, () => {}); + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(stubBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - setTimeout(() => { - const request = server.requests[0]; - request.respond(200); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + // ;charset=utf-8 is applied by sinon.fakeServer + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); - assert.strictEqual(stubBeacon.callCount, 0); - done(); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); }); }); - }); - }); + describe('and user does not define blob type', () => { + beforeEach(() => { + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); - describe('export with custom headers', () => { - let server: any; - const customHeaders = { - foo: 'bar', - bar: 'baz', - }; - - beforeEach(() => { - collectorExporterConfig = { - headers: customHeaders, - }; - server = sinon.fakeServer.create(); - }); + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(stubBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - afterEach(() => { - server.restore(); - }); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + // ;charset=utf-8 is applied by sinon.fakeServer + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); - describe('when "sendBeacon" is available', () => { - beforeEach(() => { - collectorTraceExporter = new CollectorTraceExporter( - collectorExporterConfig - ); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); }); - it('should successfully send custom headers using XMLHTTPRequest', done => { - collectorTraceExporter.export(spans, () => {}); + describe('and user defines blob type as undefined', () => { + beforeEach(() => { + collectorExporterConfig = { + ...collectorExporterConfig, + blobType: undefined, + }; + collectorTraceExporter = new CollectorTraceExporter( + collectorExporterConfig + ); + }); - setTimeout(() => { - const [{ requestHeaders }] = server.requests; + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + + const resource = json.resourceSpans[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(stubBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); + it('Request Headers should contain "Content-Type" header', done => { + const expectedHeaders = { + // ;charset=utf-8 is applied by sinon.fakeServer + 'Content-Type': 'application/json;charset=utf-8', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); + }); - done(); + it('Request Headers should contain "Accept" header', done => { + const expectedHeaders = { + Accept: 'application/json', + }; + collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const { requestHeaders } = server.requests[0]; + ensureHeadersContain(requestHeaders, expectedHeaders); + done(); + }); }); }); - }); + describe('should be able to handle logs', () => { + it('should log the successful message', done => { + // Need to stub/spy on the underlying logger as the "diag" instance is global + const spyLoggerDebug = sinon.stub(diag, 'debug'); + const spyLoggerError = sinon.stub(diag, 'error'); - describe('when "sendBeacon" is NOT available', () => { - beforeEach(() => { - (window.navigator as any).sendBeacon = false; - collectorTraceExporter = new CollectorTraceExporter( - collectorExporterConfig - ); - }); + collectorTraceExporter.export(spans, () => {}); - it('should successfully send spans using XMLHttpRequest', done => { - collectorTraceExporter.export(spans, () => {}); + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); - setTimeout(() => { - const [{ requestHeaders }] = server.requests; + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'xhr success'); + assert.strictEqual(spyLoggerError.args.length, 0); - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); + assert.strictEqual(stubBeacon.callCount, 0); + done(); + }); + }); - done(); + it('should log the error message', done => { + collectorTraceExporter.export(spans, result => { + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('Failed to export')); + done(); + }); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(400); + }); }); }); }); }); -}); -describe('CollectorTraceExporter - browser (getDefaultUrl)', () => { - it('should default to v1/trace', done => { - const collectorExporter = new CollectorTraceExporter({}); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:55681/v1/traces' - ); - done(); + describe('CollectorTraceExporter - browser (getDefaultUrl)', () => { + it('should default to v1/trace', done => { + const collectorExporter = new CollectorTraceExporter({}); + setTimeout(() => { + assert.strictEqual( + collectorExporter['url'], + 'http://localhost:55681/v1/traces' + ); + done(); + }); }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new CollectorTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorTraceExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); }); }); -}); -describe('when configuring via environment', () => { - const envSource = window as any; - it('should use url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new CollectorTraceExporter(); - assert.strictEqual( - collectorExporter.url, - envSource.OTEL_EXPORTER_OTLP_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.traces'; - const collectorExporter = new CollectorTraceExporter(); - assert.strictEqual( - collectorExporter.url, - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const collectorExporter = new CollectorTraceExporter({ headers: {} }); - // @ts-expect-error access internal property for testing - assert.strictEqual(collectorExporter._headers.foo, 'bar'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new CollectorTraceExporter({ headers: {} }); - // @ts-expect-error access internal property for testing - assert.strictEqual(collectorExporter._headers.foo, 'boo'); - // @ts-expect-error access internal property for testing - assert.strictEqual(collectorExporter._headers.bar, 'foo'); - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; + describe('when configuring via environment', () => { + const envSource = window as any; + it('should use url defined in env', () => { + envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; + const collectorExporter = new CollectorTraceExporter(); + assert.strictEqual( + collectorExporter.url, + envSource.OTEL_EXPORTER_OTLP_ENDPOINT + ); + envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; + }); + it('should override global exporter url with signal url defined in env', () => { + envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; + envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.traces'; + const collectorExporter = new CollectorTraceExporter(); + assert.strictEqual( + collectorExporter.url, + envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + ); + envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; + envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; + }); + it('should use headers defined via env', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; + const collectorExporter = new CollectorTraceExporter({ headers: {} }); + // @ts-expect-error access internal property for testing + assert.strictEqual(collectorExporter._headers.foo, 'bar'); + envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; + }); + it('should override global headers config with signal headers defined via env', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; + const collectorExporter = new CollectorTraceExporter({ headers: {} }); + // @ts-expect-error access internal property for testing + assert.strictEqual(collectorExporter._headers.foo, 'boo'); + // @ts-expect-error access internal property for testing + assert.strictEqual(collectorExporter._headers.bar, 'foo'); + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; + envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; + }); }); }); diff --git a/packages/opentelemetry-exporter-collector/test/browser/util.test.ts b/packages/opentelemetry-exporter-collector/test/browser/util.test.ts deleted file mode 100644 index ecb6c4c801..0000000000 --- a/packages/opentelemetry-exporter-collector/test/browser/util.test.ts +++ /dev/null @@ -1,128 +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 { sendWithXhr } from '../../src/platform/browser/util'; -import { ensureHeadersContain } from '../helper'; - -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 expectedHeaders: Record; - beforeEach(()=>{ - expectedHeaders = { - // ;charset=utf-8 is applied by sinon.fakeServer - 'Content-Type': 'application/json;charset=utf-8', - 'Accept': 'application/json', - } - }); - describe('and Content-Type header is set', () => { - beforeEach(()=>{ - const explicitContentType = { - 'Content-Type': 'application/json', - }; - sendWithXhr(body, url, explicitContentType, onSuccessStub, onErrorStub); - }); - it('Request Headers should contain "Content-Type" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - }); - - describe('and empty headers are set', () => { - beforeEach(()=>{ - const emptyHeaders = {}; - sendWithXhr(body, url, emptyHeaders, onSuccessStub, onErrorStub); - }); - it('Request Headers should contain "Content-Type" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - }); - describe('and custom headers are set', () => { - let customHeaders: Record; - beforeEach(()=>{ - customHeaders = { aHeader: 'aValue', bHeader: 'bValue' }; - sendWithXhr(body, url, customHeaders, onSuccessStub, onErrorStub); - }); - it('Request Headers should contain "Content-Type" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - done(); - }); - }); - it('Request Headers should contain custom headers', done => { - - setTimeout(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, customHeaders); - done(); - }); - }); - }); - }); -});