Skip to content

Commit 551021f

Browse files
HazATkamilogorek
authored andcommitted
feat: Add simple request queue with close function (#1504)
* ref: Rename send on transport to captureEvent * feat: Add basic queue implementation * feat: Add queue to backends and client + reason of skip * fix: Tests * fix: Typo * fix: Integration tests * Fixed integration tests * fix: Rename queue to transportbuffer
1 parent fe631bf commit 551021f

23 files changed

+288
-37
lines changed

packages/browser/src/backend.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Backend, logger, Options, SentryError } from '@sentry/core';
1+
import { Backend, logger, Options, SentryError, TransportBuffer } from '@sentry/core';
22
import { SentryEvent, SentryEventHint, SentryResponse, Severity, Status, Transport } from '@sentry/types';
33
import { isDOMError, isDOMException, isError, isErrorEvent, isPlainObject } from '@sentry/utils/is';
44
import { supportsBeacon, supportsFetch } from '@sentry/utils/supports';
@@ -40,6 +40,9 @@ export class BrowserBackend implements Backend {
4040
/** Cached transport used internally. */
4141
private transport?: Transport;
4242

43+
/** A simple buffer holding all requests. */
44+
private readonly buffer: TransportBuffer<SentryResponse> = new TransportBuffer();
45+
4346
/**
4447
* @inheritDoc
4548
*/
@@ -141,8 +144,8 @@ export class BrowserBackend implements Backend {
141144
public async sendEvent(event: SentryEvent): Promise<SentryResponse> {
142145
if (!this.options.dsn) {
143146
logger.warn(`Event has been skipped because no Dsn is configured.`);
144-
// We do nothing in case there is no Dsn
145-
return { status: Status.Skipped };
147+
// We do nothing in case there is no DSN
148+
return { status: Status.Skipped, reason: `Event has been skipped because no Dsn is configured.` };
146149
}
147150

148151
if (!this.transport) {
@@ -161,7 +164,7 @@ export class BrowserBackend implements Backend {
161164
}
162165
}
163166

164-
return this.transport.send(event);
167+
return this.buffer.add(this.transport.captureEvent(event));
165168
}
166169

167170
/**
@@ -177,4 +180,11 @@ export class BrowserBackend implements Backend {
177180
public storeScope(): void {
178181
// Noop
179182
}
183+
184+
/**
185+
* @inheritDoc
186+
*/
187+
public async close(timeout?: number): Promise<boolean> {
188+
return this.buffer.drain(timeout);
189+
}
180190
}

packages/browser/src/transports/base.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export abstract class BaseTransport implements Transport {
1515
/**
1616
* @inheritDoc
1717
*/
18-
public async send(_: SentryEvent): Promise<SentryResponse> {
19-
throw new SentryError('Transport Class has to implement `send` method');
18+
public async captureEvent(_: SentryEvent): Promise<SentryResponse> {
19+
throw new SentryError('Transport Class has to implement `captureEvent` method');
2020
}
2121
}

packages/browser/src/transports/beacon.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class BeaconTransport extends BaseTransport {
1010
/**
1111
* @inheritDoc
1212
*/
13-
public async send(event: SentryEvent): Promise<SentryResponse> {
13+
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
1414
const data = serialize(event);
1515

1616
const result = global.navigator.sendBeacon(this.url, data);

packages/browser/src/transports/fetch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class FetchTransport extends BaseTransport {
1111
/**
1212
* @inheritDoc
1313
*/
14-
public async send(event: SentryEvent): Promise<SentryResponse> {
14+
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
1515
const defaultOptions: RequestInit = {
1616
body: serialize(event),
1717
method: 'POST',

packages/browser/src/transports/xhr.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class XHRTransport extends BaseTransport {
77
/**
88
* @inheritDoc
99
*/
10-
public async send(event: SentryEvent): Promise<SentryResponse> {
10+
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
1111
return new Promise<SentryResponse>((resolve, reject) => {
1212
const request = new XMLHttpRequest();
1313

packages/browser/test/backend.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { BrowserBackend } from '../src/backend';
44
import { BaseTransport } from '../src/transports';
55

66
class SimpleTransport extends BaseTransport {
7-
public async send(_: SentryEvent): Promise<SentryResponse> {
7+
public async captureEvent(_: SentryEvent): Promise<SentryResponse> {
88
return {
99
status: Status.fromHttpCode(200),
1010
};
@@ -34,7 +34,7 @@ describe('BrowserBackend', () => {
3434
}
3535
});
3636

37-
it('should call send() on provided transport', async () => {
37+
it('should call captureEvent() on provided transport', async () => {
3838
backend = new BrowserBackend({ dsn, transport: SimpleTransport });
3939
const status = await backend.sendEvent(testEvent);
4040
expect(status.status).equal(Status.Success);

packages/browser/test/integration/frame.html

+9-3
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,25 @@
110110

111111
// stub transport so we don't actually transmit any data
112112
function DummyTransport() { }
113-
DummyTransport.prototype.send = function (event) {
113+
DummyTransport.prototype.captureEvent = function (event) {
114114
// console.log(JSON.stringify(event, null, 2));
115115
sentryData.push(event);
116-
return {
116+
return Promise.resolve({
117117
status: 'success'
118-
}
118+
})
119119
}
120120

121121
Sentry.init({
122122
dsn: 'https://[email protected]/1',
123123
attachStacktrace: true,
124124
transport: DummyTransport,
125125
beforeBreadcrumb: function (breadcrumb) {
126+
// Filter console logs as we use them for debugging *a lot* and they are not *that* important
127+
if (breadcrumb.category === 'console') { return null }
128+
129+
// overlyComplicatedDebuggingMechanism 'aka' console.log driven debugging
130+
// console.log(JSON.stringify(breadcrumb, null, 2));
131+
126132
// Filter internal Karma requests
127133
if (
128134
breadcrumb.type === 'http' &&

packages/browser/test/integration/test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ describe('integration', function() {
766766
done,
767767
function() {
768768
var xhr = new XMLHttpRequest();
769-
xhr.open('GET', 'https://public@example.com/1/store');
769+
xhr.open('GET', 'https://example.com/api/1/store/');
770770
xhr.send('{"message":"someMessage","level":"warning"}');
771771
setTimeout(done);
772772
},

packages/browser/test/transports/base.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ const testDsn = 'https://[email protected]/42';
66
class SimpleTransport extends BaseTransport {}
77

88
describe('BaseTransport', () => {
9-
it('doesnt provide send() implementation', async () => {
9+
it('doesnt provide captureEvent() implementation', async () => {
1010
const transport = new SimpleTransport({ dsn: testDsn });
1111

1212
try {
13-
await transport.send({});
13+
await transport.captureEvent({});
1414
} catch (e) {
15-
expect(e.message).equal('Transport Class has to implement `send` method');
15+
expect(e.message).equal('Transport Class has to implement `captureEvent` method');
1616
}
1717
});
1818

packages/browser/test/transports/beacon.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ describe('BeaconTransport', () => {
2929
expect(transport.url).equal(transportUrl);
3030
});
3131

32-
describe('send()', async () => {
32+
describe('captureEvent()', async () => {
3333
it('sends a request to Sentry servers', async () => {
3434
sendBeacon.returns(true);
3535

36-
return transport.send(payload).then(res => {
36+
return transport.captureEvent(payload).then(res => {
3737
expect(res.status).equal(Status.Success);
3838
expect(sendBeacon.calledOnce).equal(true);
3939
expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true);
@@ -43,7 +43,7 @@ describe('BeaconTransport', () => {
4343
it('rejects with failed status', async () => {
4444
sendBeacon.returns(false);
4545

46-
return transport.send(payload).catch(res => {
46+
return transport.captureEvent(payload).catch(res => {
4747
expect(res.status).equal(Status.Failed);
4848
expect(sendBeacon.calledOnce).equal(true);
4949
expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true);

packages/browser/test/transports/fetch.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ describe('FetchTransport', () => {
2929
expect(transport.url).equal(transportUrl);
3030
});
3131

32-
describe('send()', async () => {
32+
describe('captureEvent()', async () => {
3333
it('sends a request to Sentry servers', async () => {
3434
const response = { status: 200 };
3535

3636
fetch.returns(Promise.resolve(response));
3737

38-
return transport.send(payload).then(res => {
38+
return transport.captureEvent(payload).then(res => {
3939
expect(res.status).equal(Status.Success);
4040
expect(fetch.calledOnce).equal(true);
4141
expect(
@@ -53,7 +53,7 @@ describe('FetchTransport', () => {
5353

5454
fetch.returns(Promise.reject(response));
5555

56-
return transport.send(payload).catch(res => {
56+
return transport.captureEvent(payload).catch(res => {
5757
expect(res.status).equal(403);
5858
expect(fetch.calledOnce).equal(true);
5959
expect(

packages/browser/test/transports/xhr.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ describe('XHRTransport', () => {
3030
expect(transport.url).equal(transportUrl);
3131
});
3232

33-
describe('send()', async () => {
33+
describe('captureEvent()', async () => {
3434
it('sends a request to Sentry servers', async () => {
3535
server.respondWith('POST', transportUrl, [200, {}, '']);
3636

37-
return transport.send(payload).then(res => {
37+
return transport.captureEvent(payload).then(res => {
3838
expect(res.status).equal(Status.Success);
3939
const request = server.requests[0];
4040
expect(server.requests.length).equal(1);
@@ -46,7 +46,7 @@ describe('XHRTransport', () => {
4646
it('rejects with non-200 status code', done => {
4747
server.respondWith('POST', transportUrl, [403, {}, '']);
4848

49-
transport.send(payload).catch(res => {
49+
transport.captureEvent(payload).catch(res => {
5050
expect(res.status).equal(403);
5151

5252
const request = server.requests[0];

packages/core/src/base.ts

+7
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,11 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
318318

319319
return response;
320320
}
321+
322+
/**
323+
* @inheritDoc
324+
*/
325+
public async close(timeout?: number): Promise<boolean> {
326+
return this.backend.close(timeout);
327+
}
321328
}

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export { API } from './api';
55
export { BackendClass, BaseClient } from './base';
66
export { Dsn } from './dsn';
77
export { SentryError } from './error';
8+
export { TransportBuffer } from './transportbuffer';
89
export { Backend, Client, LogLevel, Options } from './interfaces';
910
export { initAndBind, ClientClass } from './sdk';

packages/core/src/interfaces.ts

+17
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,15 @@ export interface Client<O extends Options = Options> {
196196

197197
/** Returns the current options. */
198198
getOptions(): O;
199+
200+
/**
201+
* A promise that resolves whenever the transport buffer is empty.
202+
* If you provide a timeout and the buffer takes longer to drain the promise returns false.
203+
* Calls {@link Backend.close}.
204+
*
205+
* @param timeout Maximum time in ms the client should wait.
206+
*/
207+
close(timeout?: number): Promise<boolean>;
199208
}
200209

201210
/**
@@ -256,4 +265,12 @@ export interface Backend {
256265
* @param scope The scope to store.
257266
*/
258267
storeScope(scope: Scope): void;
268+
269+
/**
270+
* A promise that resolves whenever the transport buffer is empty.
271+
* If you provide a timeout and the buffer takes longer to drain the promise returns false.
272+
*
273+
* @param timeout Maximum time in ms the client should wait.
274+
*/
275+
close(timeout?: number): Promise<boolean>;
259276
}

packages/core/src/transportbuffer.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/** A simple queue that holds promises. */
2+
export class TransportBuffer<T> {
3+
/** Internal set of queued Promises */
4+
private readonly buffer: Set<Promise<T>> = new Set();
5+
6+
/**
7+
* Add a promise to the queue.
8+
*
9+
* @param task Can be any Promise<T>
10+
* @returns The original promise.
11+
*/
12+
public async add(task: Promise<T>): Promise<T> {
13+
this.buffer.add(task);
14+
task.then(() => this.buffer.delete(task)).catch(() => this.buffer.delete(task));
15+
return task;
16+
}
17+
18+
/**
19+
* This function returns the number of unresolved promises in the queue.
20+
*/
21+
public length(): number {
22+
return this.buffer.size;
23+
}
24+
25+
/**
26+
* This will drain the whole queue, returns true if queue is empty or drained.
27+
* If timeout is provided and the queue takes longer to drain, the promise still resolves but with false.
28+
*
29+
* @param timeout Number in ms to wait until it resolves with false.
30+
*/
31+
public async drain(timeout?: number): Promise<boolean> {
32+
return new Promise<boolean>(resolve => {
33+
const capturedSetTimeout = setTimeout(() => {
34+
if (timeout && timeout > 0) {
35+
resolve(false);
36+
}
37+
}, timeout);
38+
Promise.all(this.buffer.values())
39+
.then(() => {
40+
clearTimeout(capturedSetTimeout);
41+
resolve(true);
42+
})
43+
.catch(() => {
44+
resolve(true);
45+
});
46+
});
47+
}
48+
}

0 commit comments

Comments
 (0)