feat(otlp-exporter-base)!: use transport interface in web exporters (#4895)
This commit is contained in:
parent
cd4e2bf8c0
commit
f2a6bcc204
|
@ -17,6 +17,8 @@ All notable changes to experimental packages in this project will be documented
|
|||
* feat(exporter-*-otlp*)!: remove environment-variable specific code from browser exporters
|
||||
* (user-facing) removes the ability to configure browser exporters by using `process.env` polyfills
|
||||
* feat(sdk-node)!: Automatically configure logs exporter [#4740](https://github.com/open-telemetry/opentelemetry-js/pull/4740)
|
||||
* feat(exporter-*-otlp-*)!: use transport interface in browser exporters [#4895](https://github.com/open-telemetry/opentelemetry-js/pull/4895) @pichlermarc
|
||||
* (user-facing) protected `headers` property was intended for internal use has been removed from all exporters
|
||||
|
||||
### :rocket: (Enhancement)
|
||||
|
||||
|
|
|
@ -150,12 +150,16 @@ describe('OTLPTraceExporter - web', () => {
|
|||
|
||||
collectorTraceExporter.export(spans, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
const response: any = spyLoggerDebug.args[2][0];
|
||||
assert.strictEqual(response, 'sendBeacon - can send');
|
||||
assert.strictEqual(spyLoggerError.args.length, 0);
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
const response: any = spyLoggerDebug.args[2][0];
|
||||
assert.strictEqual(response, 'SendBeacon success');
|
||||
assert.strictEqual(spyLoggerError.args.length, 0);
|
||||
|
||||
done();
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -163,9 +167,17 @@ describe('OTLPTraceExporter - web', () => {
|
|||
stubBeacon.returns(false);
|
||||
|
||||
collectorTraceExporter.export(spans, result => {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.ok(result.error?.message.includes('cannot send'));
|
||||
done();
|
||||
try {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.ok(
|
||||
result.error,
|
||||
'Expected Error, but no Error was present on the result'
|
||||
);
|
||||
assert.match(result.error?.message, /SendBeacon failed/);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -179,8 +191,8 @@ describe('OTLPTraceExporter - web', () => {
|
|||
clock = sinon.useFakeTimers();
|
||||
|
||||
(window.navigator as any).sendBeacon = false;
|
||||
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
|
||||
server = sinon.fakeServer.create();
|
||||
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
|
||||
});
|
||||
afterEach(() => {
|
||||
server.restore();
|
||||
|
@ -189,15 +201,15 @@ describe('OTLPTraceExporter - web', () => {
|
|||
it('should successfully send the spans using XMLHttpRequest', done => {
|
||||
collectorTraceExporter.export(spans, () => {});
|
||||
|
||||
queueMicrotask(() => {
|
||||
queueMicrotask(async () => {
|
||||
const request = server.requests[0];
|
||||
assert.strictEqual(request.method, 'POST');
|
||||
assert.strictEqual(request.url, 'http://foo.bar.com');
|
||||
|
||||
const body = request.requestBody;
|
||||
const body = request.requestBody as Blob;
|
||||
const decoder = new TextDecoder();
|
||||
const json = JSON.parse(
|
||||
decoder.decode(body)
|
||||
decoder.decode(await body.arrayBuffer())
|
||||
) as IExportTraceServiceRequest;
|
||||
const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0];
|
||||
|
||||
|
@ -235,28 +247,36 @@ describe('OTLPTraceExporter - web', () => {
|
|||
queueMicrotask(() => {
|
||||
const request = server.requests[0];
|
||||
request.respond(200);
|
||||
const response: any = spyLoggerDebug.args[2][0];
|
||||
assert.strictEqual(response, 'xhr success');
|
||||
assert.strictEqual(spyLoggerError.args.length, 0);
|
||||
assert.strictEqual(stubBeacon.callCount, 0);
|
||||
|
||||
clock.restore();
|
||||
done();
|
||||
try {
|
||||
const response: any = spyLoggerDebug.args[2][0];
|
||||
assert.strictEqual(response, 'XHR success');
|
||||
assert.strictEqual(spyLoggerError.args.length, 0);
|
||||
assert.strictEqual(stubBeacon.callCount, 0);
|
||||
clock.restore();
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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'));
|
||||
try {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.deepStrictEqual(
|
||||
result.error?.message,
|
||||
'XHR request failed with non-retryable status'
|
||||
);
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
queueMicrotask(() => {
|
||||
const request = server.requests[0];
|
||||
request.respond(400);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -421,17 +441,20 @@ describe('OTLPTraceExporter - web', () => {
|
|||
it('should log the timeout request error message', done => {
|
||||
const responseSpy = sinon.spy();
|
||||
collectorTraceExporter.export(spans, responseSpy);
|
||||
clock.tick(10000);
|
||||
clock.tick(20000);
|
||||
clock.restore();
|
||||
|
||||
setTimeout(() => {
|
||||
const result = responseSpy.args[0][0] as core.ExportResult;
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(error.message, 'Request Timeout');
|
||||
|
||||
done();
|
||||
try {
|
||||
const result = responseSpy.args[0][0] as core.ExportResult;
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(error.message, 'XHR request timed out');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -455,15 +478,19 @@ describe('OTLPTraceExporter - web', () => {
|
|||
|
||||
setTimeout(() => {
|
||||
// Expect 4 failures
|
||||
assert.strictEqual(failures.length, 4);
|
||||
failures.forEach(([result]) => {
|
||||
assert.strictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.strictEqual(
|
||||
result.error!.message,
|
||||
'Concurrent export limit reached'
|
||||
);
|
||||
});
|
||||
done();
|
||||
try {
|
||||
assert.strictEqual(failures.length, 4);
|
||||
failures.forEach(([result]) => {
|
||||
assert.strictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.strictEqual(
|
||||
result.error!.message,
|
||||
'Concurrent export limit reached'
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -514,26 +541,33 @@ describe('export with retry - real http request destroyed', () => {
|
|||
(window.navigator as any).sendBeacon = false;
|
||||
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
|
||||
});
|
||||
it('should log the timeout request error message when retrying with exponential backoff with jitter', done => {
|
||||
it('should log the retryable request error message when retrying with exponential backoff with jitter', done => {
|
||||
spans = [];
|
||||
spans.push(Object.assign({}, mockedReadableSpan));
|
||||
|
||||
let retry = 0;
|
||||
let calls = 0;
|
||||
server.respondWith(
|
||||
'http://localhost:4318/v1/traces',
|
||||
function (xhr: any) {
|
||||
retry++;
|
||||
calls++;
|
||||
xhr.respond(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(retry, 1);
|
||||
done();
|
||||
try {
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Export failed with retryable status'
|
||||
);
|
||||
assert.strictEqual(calls, 6);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
|
@ -541,22 +575,29 @@ describe('export with retry - real http request destroyed', () => {
|
|||
spans = [];
|
||||
spans.push(Object.assign({}, mockedReadableSpan));
|
||||
|
||||
let retry = 0;
|
||||
let calls = 0;
|
||||
server.respondWith(
|
||||
'http://localhost:4318/v1/traces',
|
||||
function (xhr: any) {
|
||||
retry++;
|
||||
xhr.respond(503, { 'Retry-After': 3 });
|
||||
calls++;
|
||||
xhr.respond(503, { 'Retry-After': 0.1 });
|
||||
}
|
||||
);
|
||||
|
||||
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();
|
||||
try {
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Export failed with retryable status'
|
||||
);
|
||||
assert.strictEqual(calls, 6);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}).timeout(3000);
|
||||
it('should log the timeout request error message when retry-after header is a date', done => {
|
||||
|
@ -569,18 +610,25 @@ describe('export with retry - real http request destroyed', () => {
|
|||
function (xhr: any) {
|
||||
retry++;
|
||||
const d = new Date();
|
||||
d.setSeconds(d.getSeconds() + 1);
|
||||
d.setSeconds(d.getSeconds() + 0.1);
|
||||
xhr.respond(503, { 'Retry-After': d });
|
||||
}
|
||||
);
|
||||
|
||||
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, 2);
|
||||
done();
|
||||
try {
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Export failed with retryable status'
|
||||
);
|
||||
assert.strictEqual(retry, 6);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}).timeout(3000);
|
||||
it('should log the timeout request error message when retry-after header is a date with long delay', done => {
|
||||
|
@ -599,12 +647,19 @@ describe('export with retry - real http request destroyed', () => {
|
|||
);
|
||||
|
||||
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();
|
||||
try {
|
||||
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
|
||||
const error = result.error as OTLPExporterError;
|
||||
assert.ok(error !== undefined);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Export failed with retryable status'
|
||||
);
|
||||
assert.strictEqual(retry, 1);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
}).timeout(3000);
|
||||
});
|
||||
|
|
|
@ -191,9 +191,9 @@ describe('OTLPMetricExporter - web', () => {
|
|||
|
||||
collectorExporter.export(metrics, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
queueMicrotask(() => {
|
||||
const response: any = debugStub.args[2][0];
|
||||
assert.strictEqual(response, 'sendBeacon - can send');
|
||||
assert.strictEqual(response, 'SendBeacon success');
|
||||
assert.strictEqual(errorStub.args.length, 0);
|
||||
|
||||
done();
|
||||
|
@ -204,9 +204,17 @@ describe('OTLPMetricExporter - web', () => {
|
|||
stubBeacon.returns(false);
|
||||
|
||||
collectorExporter.export(metrics, result => {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.ok(result.error?.message.includes('cannot send'));
|
||||
done();
|
||||
try {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.ok(
|
||||
result.error,
|
||||
'Expected Error, but no Error was present on the result'
|
||||
);
|
||||
assert.match(result.error?.message, /SendBeacon failed/);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -232,7 +240,7 @@ describe('OTLPMetricExporter - web', () => {
|
|||
it('should successfully send the metrics using XMLHttpRequest', done => {
|
||||
collectorExporter.export(metrics, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
queueMicrotask(async () => {
|
||||
const request = server.requests[0];
|
||||
assert.strictEqual(request.method, 'POST');
|
||||
assert.strictEqual(request.url, 'http://foo.bar.com');
|
||||
|
@ -240,7 +248,7 @@ describe('OTLPMetricExporter - web', () => {
|
|||
const body = request.requestBody;
|
||||
const decoder = new TextDecoder();
|
||||
const json = JSON.parse(
|
||||
decoder.decode(body)
|
||||
decoder.decode(await body.arrayBuffer())
|
||||
) as IExportMetricsServiceRequest;
|
||||
// The order of the metrics is not guaranteed.
|
||||
const counterIndex = metrics.scopeMetrics[0].metrics.findIndex(
|
||||
|
@ -310,12 +318,12 @@ describe('OTLPMetricExporter - web', () => {
|
|||
it('should log the successful message', done => {
|
||||
collectorExporter.export(metrics, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
queueMicrotask(() => {
|
||||
const request = server.requests[0];
|
||||
request.respond(200);
|
||||
|
||||
const response: any = debugStub.args[2][0];
|
||||
assert.strictEqual(response, 'xhr success');
|
||||
assert.strictEqual(response, 'XHR success');
|
||||
assert.strictEqual(errorStub.args.length, 0);
|
||||
|
||||
assert.strictEqual(stubBeacon.callCount, 0);
|
||||
|
@ -325,13 +333,19 @@ describe('OTLPMetricExporter - web', () => {
|
|||
|
||||
it('should log the error message', done => {
|
||||
collectorExporter.export(metrics, result => {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.ok(result.error?.message.includes('Failed to export'));
|
||||
assert.strictEqual(stubBeacon.callCount, 0);
|
||||
try {
|
||||
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
|
||||
assert.deepStrictEqual(
|
||||
result.error?.message,
|
||||
'XHR request failed with non-retryable status'
|
||||
);
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
queueMicrotask(() => {
|
||||
const request = server.requests[0];
|
||||
request.respond(400);
|
||||
});
|
||||
|
@ -339,7 +353,7 @@ describe('OTLPMetricExporter - web', () => {
|
|||
it('should send custom headers', done => {
|
||||
collectorExporter.export(metrics, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
queueMicrotask(() => {
|
||||
const request = server.requests[0];
|
||||
request.respond(200);
|
||||
|
||||
|
|
|
@ -15,13 +15,15 @@
|
|||
*/
|
||||
|
||||
import { OTLPExporterBase } from '../../OTLPExporterBase';
|
||||
import { OTLPExporterConfigBase } from '../../types';
|
||||
import * as otlpTypes from '../../types';
|
||||
import { OTLPExporterConfigBase, OTLPExporterError } from '../../types';
|
||||
import { parseHeaders } from '../../util';
|
||||
import { sendWithBeacon, sendWithXhr } from './util';
|
||||
import { diag } from '@opentelemetry/api';
|
||||
import { getEnv, baggageUtils } from '@opentelemetry/core';
|
||||
import { ISerializer } from '@opentelemetry/otlp-transformer';
|
||||
import { IExporterTransport } from '../../exporter-transport';
|
||||
import { createXhrTransport } from './xhr-transport';
|
||||
import { createSendBeaconTransport } from './send-beacon-transport';
|
||||
import { createRetryingTransport } from '../../retrying-transport';
|
||||
|
||||
/**
|
||||
* Collector Metric Exporter abstract base class
|
||||
|
@ -30,10 +32,8 @@ export abstract class OTLPExporterBrowserBase<
|
|||
ExportItem,
|
||||
ServiceResponse,
|
||||
> extends OTLPExporterBase<OTLPExporterConfigBase, ExportItem> {
|
||||
protected _headers: Record<string, string>;
|
||||
private _useXHR: boolean = false;
|
||||
private _contentType: string;
|
||||
private _serializer: ISerializer<ExportItem[], ServiceResponse>;
|
||||
private _transport: IExporterTransport;
|
||||
|
||||
/**
|
||||
* @param config
|
||||
|
@ -47,19 +47,28 @@ export abstract class OTLPExporterBrowserBase<
|
|||
) {
|
||||
super(config);
|
||||
this._serializer = serializer;
|
||||
this._contentType = contentType;
|
||||
this._useXHR =
|
||||
const useXhr =
|
||||
!!config.headers || typeof navigator.sendBeacon !== 'function';
|
||||
if (this._useXHR) {
|
||||
this._headers = Object.assign(
|
||||
{},
|
||||
parseHeaders(config.headers),
|
||||
baggageUtils.parseKeyPairsIntoRecord(
|
||||
getEnv().OTEL_EXPORTER_OTLP_HEADERS
|
||||
)
|
||||
);
|
||||
if (useXhr) {
|
||||
this._transport = createRetryingTransport({
|
||||
transport: createXhrTransport({
|
||||
headers: Object.assign(
|
||||
{},
|
||||
parseHeaders(config.headers),
|
||||
baggageUtils.parseKeyPairsIntoRecord(
|
||||
getEnv().OTEL_EXPORTER_OTLP_HEADERS
|
||||
),
|
||||
{ 'Content-Type': contentType }
|
||||
),
|
||||
url: this.url,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this._headers = {};
|
||||
// sendBeacon has no way to signal retry, so we do not wrap it in a RetryingTransport
|
||||
this._transport = createSendBeaconTransport({
|
||||
url: this.url,
|
||||
blobType: contentType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,39 +77,35 @@ export abstract class OTLPExporterBrowserBase<
|
|||
onShutdown(): void {}
|
||||
|
||||
send(
|
||||
items: ExportItem[],
|
||||
objects: ExportItem[],
|
||||
onSuccess: () => void,
|
||||
onError: (error: otlpTypes.OTLPExporterError) => void
|
||||
onError: (error: OTLPExporterError) => void
|
||||
): void {
|
||||
if (this._shutdownOnce.isCalled) {
|
||||
diag.debug('Shutdown already started. Cannot send objects');
|
||||
return;
|
||||
}
|
||||
const body = this._serializer.serializeRequest(items) ?? new Uint8Array();
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
if (this._useXHR) {
|
||||
sendWithXhr(
|
||||
body,
|
||||
this.url,
|
||||
{
|
||||
...this._headers,
|
||||
'Content-Type': this._contentType,
|
||||
},
|
||||
this.timeoutMillis,
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
} else {
|
||||
sendWithBeacon(
|
||||
body,
|
||||
this.url,
|
||||
{ type: this._contentType },
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
}
|
||||
}).then(onSuccess, onError);
|
||||
const data = this._serializer.serializeRequest(objects);
|
||||
|
||||
if (data == null) {
|
||||
onError(new Error('Could not serialize message'));
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this._transport
|
||||
.send(data, this.timeoutMillis)
|
||||
.then(response => {
|
||||
if (response.status === 'success') {
|
||||
onSuccess();
|
||||
} else if (response.status === 'failure' && response.error) {
|
||||
onError(response.error);
|
||||
} else if (response.status === 'retryable') {
|
||||
onError(new OTLPExporterError('Export failed with retryable status'));
|
||||
} else {
|
||||
onError(new OTLPExporterError('Export failed with unknown error'));
|
||||
}
|
||||
}, onError);
|
||||
|
||||
this._sendingPromises.push(promise);
|
||||
const popPromise = () => {
|
||||
|
|
|
@ -15,4 +15,3 @@
|
|||
*/
|
||||
|
||||
export { OTLPExporterBrowserBase } from './OTLPExporterBrowserBase';
|
||||
export { sendWithXhr } from './util';
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IExporterTransport } from '../../exporter-transport';
|
||||
import { ExportResponse } from '../../export-response';
|
||||
import { diag } from '@opentelemetry/api';
|
||||
|
||||
export interface SendBeaconParameters {
|
||||
url: string;
|
||||
/**
|
||||
* for instance 'application/x-protobuf'
|
||||
*/
|
||||
blobType: string;
|
||||
}
|
||||
|
||||
class SendBeaconTransport implements IExporterTransport {
|
||||
constructor(private _params: SendBeaconParameters) {}
|
||||
send(data: Uint8Array): Promise<ExportResponse> {
|
||||
return new Promise<ExportResponse>(resolve => {
|
||||
if (
|
||||
navigator.sendBeacon(
|
||||
this._params.url,
|
||||
new Blob([data], { type: this._params.blobType })
|
||||
)
|
||||
) {
|
||||
// no way to signal retry, treat everything as success
|
||||
diag.debug('SendBeacon success');
|
||||
resolve({
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
status: 'failure',
|
||||
error: new Error('SendBeacon failed'),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
// Intentionally left empty, nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
export function createSendBeaconTransport(
|
||||
parameters: SendBeaconParameters
|
||||
): IExporterTransport {
|
||||
return new SendBeaconTransport(parameters);
|
||||
}
|
|
@ -1,163 +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 { diag } from '@opentelemetry/api';
|
||||
import { OTLPExporterError } from '../../types';
|
||||
import {
|
||||
DEFAULT_EXPORT_MAX_ATTEMPTS,
|
||||
DEFAULT_EXPORT_INITIAL_BACKOFF,
|
||||
DEFAULT_EXPORT_BACKOFF_MULTIPLIER,
|
||||
DEFAULT_EXPORT_MAX_BACKOFF,
|
||||
isExportRetryable,
|
||||
parseRetryAfterToMills,
|
||||
} from '../../util';
|
||||
|
||||
/**
|
||||
* Send metrics/spans using browser navigator.sendBeacon
|
||||
* @param body
|
||||
* @param url
|
||||
* @param blobPropertyBag
|
||||
* @param onSuccess
|
||||
* @param onError
|
||||
*/
|
||||
export function sendWithBeacon(
|
||||
body: Uint8Array,
|
||||
url: string,
|
||||
blobPropertyBag: BlobPropertyBag,
|
||||
onSuccess: () => void,
|
||||
onError: (error: OTLPExporterError) => void
|
||||
): void {
|
||||
if (navigator.sendBeacon(url, new Blob([body], blobPropertyBag))) {
|
||||
diag.debug('sendBeacon - can send', body);
|
||||
onSuccess();
|
||||
} else {
|
||||
const error = new OTLPExporterError(`sendBeacon - cannot send ${body}`);
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* function to send metrics/spans using browser XMLHttpRequest
|
||||
* used when navigator.sendBeacon is not available
|
||||
* @param body
|
||||
* @param url
|
||||
* @param headers
|
||||
* @param onSuccess
|
||||
* @param onError
|
||||
*/
|
||||
export function sendWithXhr(
|
||||
body: Uint8Array,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
exporterTimeout: number,
|
||||
onSuccess: () => void,
|
||||
onError: (error: OTLPExporterError) => void
|
||||
): void {
|
||||
let retryTimer: ReturnType<typeof setTimeout>;
|
||||
let xhr: XMLHttpRequest;
|
||||
let reqIsDestroyed = false;
|
||||
|
||||
const exporterTimer = setTimeout(() => {
|
||||
clearTimeout(retryTimer);
|
||||
reqIsDestroyed = true;
|
||||
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
const err = new OTLPExporterError('Request Timeout');
|
||||
onError(err);
|
||||
} else {
|
||||
xhr.abort();
|
||||
}
|
||||
}, exporterTimeout);
|
||||
|
||||
const sendWithRetry = (
|
||||
retries = DEFAULT_EXPORT_MAX_ATTEMPTS,
|
||||
minDelay = DEFAULT_EXPORT_INITIAL_BACKOFF
|
||||
) => {
|
||||
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);
|
||||
});
|
||||
|
||||
xhr.send(body);
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE && reqIsDestroyed === false) {
|
||||
if (xhr.status >= 200 && xhr.status <= 299) {
|
||||
diag.debug('xhr success', body);
|
||||
onSuccess();
|
||||
clearTimeout(exporterTimer);
|
||||
clearTimeout(retryTimer);
|
||||
} else if (xhr.status && isExportRetryable(xhr.status) && retries > 0) {
|
||||
let retryTime: number;
|
||||
minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay;
|
||||
|
||||
// retry after interval specified in Retry-After header
|
||||
if (xhr.getResponseHeader('Retry-After')) {
|
||||
retryTime = parseRetryAfterToMills(
|
||||
xhr.getResponseHeader('Retry-After')!
|
||||
);
|
||||
} else {
|
||||
// exponential backoff with jitter
|
||||
retryTime = Math.round(
|
||||
Math.random() * (DEFAULT_EXPORT_MAX_BACKOFF - minDelay) + minDelay
|
||||
);
|
||||
}
|
||||
|
||||
retryTimer = setTimeout(() => {
|
||||
sendWithRetry(retries - 1, minDelay);
|
||||
}, retryTime);
|
||||
} else {
|
||||
const error = new OTLPExporterError(
|
||||
`Failed to export with XHR (status: ${xhr.status})`,
|
||||
xhr.status
|
||||
);
|
||||
onError(error);
|
||||
clearTimeout(exporterTimer);
|
||||
clearTimeout(retryTimer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
if (reqIsDestroyed) {
|
||||
const err = new OTLPExporterError('Request Timeout');
|
||||
onError(err);
|
||||
}
|
||||
clearTimeout(exporterTimer);
|
||||
clearTimeout(retryTimer);
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
if (reqIsDestroyed) {
|
||||
const err = new OTLPExporterError('Request Timeout');
|
||||
onError(err);
|
||||
}
|
||||
clearTimeout(exporterTimer);
|
||||
clearTimeout(retryTimer);
|
||||
};
|
||||
};
|
||||
|
||||
sendWithRetry();
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IExporterTransport } from '../../exporter-transport';
|
||||
import { ExportResponse } from '../../export-response';
|
||||
import { isExportRetryable, parseRetryAfterToMills } from '../../util';
|
||||
import { diag } from '@opentelemetry/api';
|
||||
|
||||
export interface XhrRequestParameters {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
class XhrTransport implements IExporterTransport {
|
||||
constructor(private _parameters: XhrRequestParameters) {}
|
||||
|
||||
send(data: Uint8Array, timeoutMillis: number): Promise<ExportResponse> {
|
||||
return new Promise<ExportResponse>(resolve => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = timeoutMillis;
|
||||
xhr.open('POST', this._parameters.url);
|
||||
Object.entries(this._parameters.headers).forEach(([k, v]) => {
|
||||
xhr.setRequestHeader(k, v);
|
||||
});
|
||||
|
||||
xhr.ontimeout = _ => {
|
||||
resolve({
|
||||
status: 'failure',
|
||||
error: new Error('XHR request timed out'),
|
||||
});
|
||||
};
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.status >= 200 && xhr.status <= 299) {
|
||||
diag.debug('XHR success');
|
||||
resolve({
|
||||
status: 'success',
|
||||
});
|
||||
} else if (xhr.status && isExportRetryable(xhr.status)) {
|
||||
resolve({
|
||||
status: 'retryable',
|
||||
retryInMillis: parseRetryAfterToMills(
|
||||
xhr.getResponseHeader('Retry-After')
|
||||
),
|
||||
});
|
||||
} else if (xhr.status !== 0) {
|
||||
resolve({
|
||||
status: 'failure',
|
||||
error: new Error('XHR request failed with non-retryable status'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
resolve({
|
||||
status: 'failure',
|
||||
error: new Error('XHR request aborted'),
|
||||
});
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
resolve({
|
||||
status: 'failure',
|
||||
error: new Error('XHR request errored'),
|
||||
});
|
||||
};
|
||||
|
||||
xhr.send(
|
||||
new Blob([data], { type: this._parameters.headers['Content-Type'] })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
// Intentionally left empty, nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an exporter transport that uses XHR to send the data
|
||||
* @param parameters applied to each request made by transport
|
||||
*/
|
||||
export function createXhrTransport(
|
||||
parameters: XhrRequestParameters
|
||||
): IExporterTransport {
|
||||
return new XhrTransport(parameters);
|
||||
}
|
|
@ -19,4 +19,4 @@ export {
|
|||
OTLPExporterNodeConfigBase,
|
||||
CompressionAlgorithm,
|
||||
} from './node';
|
||||
export { OTLPExporterBrowserBase, sendWithXhr } from './browser';
|
||||
export { OTLPExporterBrowserBase } from './browser';
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { createSendBeaconTransport } from '../../src/platform/browser/send-beacon-transport';
|
||||
import * as assert from 'assert';
|
||||
|
||||
describe('SendBeaconTransport', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('send', function () {
|
||||
it('returns failure when sendBeacon fails', async function () {
|
||||
// arrange
|
||||
const sendStub = sinon.stub(navigator, 'sendBeacon').returns(false);
|
||||
const transport = createSendBeaconTransport({
|
||||
url: 'http://example.test',
|
||||
blobType: 'application/json',
|
||||
});
|
||||
|
||||
// act
|
||||
const result = await transport.send(Uint8Array.from([1, 2, 3]), 1000);
|
||||
|
||||
// assert
|
||||
sinon.assert.calledOnceWithMatch(
|
||||
sendStub,
|
||||
'http://example.test',
|
||||
sinon.match
|
||||
.instanceOf(Blob)
|
||||
.and(
|
||||
sinon.match(
|
||||
actual => actual.type === 'application/json',
|
||||
'Expected Blob type to match.'
|
||||
)
|
||||
)
|
||||
);
|
||||
assert.strictEqual(result.status, 'failure');
|
||||
});
|
||||
|
||||
it('returns success when sendBeacon succeeds', async function () {
|
||||
// arrange
|
||||
const sendStub = sinon.stub(navigator, 'sendBeacon').returns(true);
|
||||
const transport = createSendBeaconTransport({
|
||||
url: 'http://example.test',
|
||||
blobType: 'application/json',
|
||||
});
|
||||
|
||||
// act
|
||||
const result = await transport.send(Uint8Array.from([1, 2, 3]), 1000);
|
||||
|
||||
// assert
|
||||
sinon.assert.calledOnceWithMatch(
|
||||
sendStub,
|
||||
'http://example.test',
|
||||
sinon.match
|
||||
.instanceOf(Blob)
|
||||
.and(
|
||||
sinon.match(
|
||||
actual => actual.type === 'application/json',
|
||||
'Expected Blob type to match.'
|
||||
)
|
||||
)
|
||||
);
|
||||
assert.strictEqual(result.status, 'success');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,159 +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 { nextTick } from 'process';
|
||||
import { ensureHeadersContain } from '../testHelper';
|
||||
|
||||
describe('util - browser', () => {
|
||||
let server: any;
|
||||
const body = new Uint8Array();
|
||||
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<string, string>;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
beforeEach(() => {
|
||||
// fakeTimers is used to replace the next setTimeout which is
|
||||
// located in sendWithXhr function called by the export method
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
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',
|
||||
};
|
||||
const exporterTimeout = 10000;
|
||||
sendWithXhr(
|
||||
body,
|
||||
url,
|
||||
explicitContentType,
|
||||
exporterTimeout,
|
||||
onSuccessStub,
|
||||
onErrorStub
|
||||
);
|
||||
});
|
||||
it('Request Headers should contain "Content-Type" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('Request Headers should contain "Accept" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and empty headers are set', () => {
|
||||
beforeEach(() => {
|
||||
const emptyHeaders = {};
|
||||
// use default exporter timeout
|
||||
const exporterTimeout = 10000;
|
||||
sendWithXhr(
|
||||
body,
|
||||
url,
|
||||
emptyHeaders,
|
||||
exporterTimeout,
|
||||
onSuccessStub,
|
||||
onErrorStub
|
||||
);
|
||||
});
|
||||
it('Request Headers should contain "Content-Type" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('Request Headers should contain "Accept" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and custom headers are set', () => {
|
||||
let customHeaders: Record<string, string>;
|
||||
beforeEach(() => {
|
||||
customHeaders = { aHeader: 'aValue', bHeader: 'bValue' };
|
||||
const exporterTimeout = 10000;
|
||||
sendWithXhr(
|
||||
body,
|
||||
url,
|
||||
customHeaders,
|
||||
exporterTimeout,
|
||||
onSuccessStub,
|
||||
onErrorStub
|
||||
);
|
||||
});
|
||||
it('Request Headers should contain "Content-Type" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('Request Headers should contain "Accept" header', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, expectedHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('Request Headers should contain custom headers', done => {
|
||||
nextTick(() => {
|
||||
const { requestHeaders } = server.requests[0];
|
||||
ensureHeadersContain(requestHeaders, customHeaders);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import * as assert from 'assert';
|
||||
import { createXhrTransport } from '../../src/platform/browser/xhr-transport';
|
||||
import {
|
||||
ExportResponseRetryable,
|
||||
ExportResponseFailure,
|
||||
ExportResponseSuccess,
|
||||
} from '../../src';
|
||||
import { ensureHeadersContain } from '../testHelper';
|
||||
|
||||
const testTransportParameters = {
|
||||
url: 'http://example.test',
|
||||
headers: {
|
||||
foo: 'foo-value',
|
||||
bar: 'bar-value',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const requestTimeout = 1000;
|
||||
const testPayload = Uint8Array.from([1, 2, 3]);
|
||||
|
||||
describe('XhrTransport', function () {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('send', function () {
|
||||
it('returns success when request succeeds', function (done) {
|
||||
// arrange
|
||||
const server = sinon.fakeServer.create();
|
||||
const transport = createXhrTransport(testTransportParameters);
|
||||
|
||||
let request: sinon.SinonFakeXMLHttpRequest;
|
||||
queueMicrotask(() => {
|
||||
// this executes after the act block
|
||||
request = server.requests[0];
|
||||
request.respond(200, {}, 'test response');
|
||||
});
|
||||
|
||||
//act
|
||||
transport.send(testPayload, requestTimeout).then(response => {
|
||||
// assert
|
||||
try {
|
||||
assert.strictEqual(response.status, 'success');
|
||||
// currently we don't do anything with the response yet, so it's dropped by the transport.
|
||||
assert.strictEqual(
|
||||
(response as ExportResponseSuccess).data,
|
||||
undefined
|
||||
);
|
||||
assert.strictEqual(request.url, testTransportParameters.url);
|
||||
assert.strictEqual(
|
||||
(request.requestBody as unknown as Blob).type,
|
||||
'application/json'
|
||||
);
|
||||
ensureHeadersContain(request.requestHeaders, {
|
||||
foo: 'foo-value',
|
||||
bar: 'bar-value',
|
||||
// ;charset=utf-8 is applied by sinon.fakeServer
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
});
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
}, done /* catch any rejections */);
|
||||
});
|
||||
|
||||
it('returns failure when request fails', function (done) {
|
||||
// arrange
|
||||
const server = sinon.fakeServer.create();
|
||||
const transport = createXhrTransport(testTransportParameters);
|
||||
|
||||
queueMicrotask(() => {
|
||||
// this executes after the act block
|
||||
const request = server.requests[0];
|
||||
request.respond(404, {}, '');
|
||||
});
|
||||
|
||||
//act
|
||||
transport.send(testPayload, requestTimeout).then(response => {
|
||||
// assert
|
||||
try {
|
||||
assert.strictEqual(response.status, 'failure');
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
}, done /* catch any rejections */);
|
||||
});
|
||||
|
||||
it('returns retryable when request is retryable', function (done) {
|
||||
// arrange
|
||||
const server = sinon.fakeServer.create();
|
||||
const transport = createXhrTransport(testTransportParameters);
|
||||
|
||||
queueMicrotask(() => {
|
||||
// this executes after the act block
|
||||
const request = server.requests[0];
|
||||
request.respond(503, { 'Retry-After': 5 }, '');
|
||||
});
|
||||
|
||||
//act
|
||||
transport.send(testPayload, requestTimeout).then(response => {
|
||||
// assert
|
||||
try {
|
||||
assert.strictEqual(response.status, 'retryable');
|
||||
assert.strictEqual(
|
||||
(response as ExportResponseRetryable).retryInMillis,
|
||||
5000
|
||||
);
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
}, done /* catch any rejections */);
|
||||
});
|
||||
|
||||
it('returns failure when request times out', function (done) {
|
||||
// arrange
|
||||
// A fake server needed, otherwise the message will not be a timeout but a failure to connect.
|
||||
sinon.useFakeServer();
|
||||
const clock = sinon.useFakeTimers();
|
||||
const transport = createXhrTransport(testTransportParameters);
|
||||
|
||||
//act
|
||||
transport.send(testPayload, requestTimeout).then(response => {
|
||||
// assert
|
||||
try {
|
||||
assert.strictEqual(response.status, 'failure');
|
||||
assert.strictEqual(
|
||||
(response as ExportResponseFailure).error.message,
|
||||
'XHR request timed out'
|
||||
);
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
}, done /* catch any rejections */);
|
||||
clock.tick(requestTimeout + 100);
|
||||
});
|
||||
|
||||
it('returns failure when no server exists', function (done) {
|
||||
// arrange
|
||||
const clock = sinon.useFakeTimers();
|
||||
const transport = createXhrTransport(testTransportParameters);
|
||||
|
||||
//act
|
||||
transport.send(testPayload, requestTimeout).then(response => {
|
||||
// assert
|
||||
try {
|
||||
assert.strictEqual(response.status, 'failure');
|
||||
assert.strictEqual(
|
||||
(response as ExportResponseFailure).error.message,
|
||||
'XHR request errored'
|
||||
);
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
done();
|
||||
}, done /* catch any rejections */);
|
||||
clock.tick(requestTimeout + 100);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue