Skip to content

Commit c3b7940

Browse files
authored
fix: Wait for processing to finish before closing transport (#2005)
* fix: Wait for processing to finish before closing transport * fix: Remove finally * fix: Remove node flush tests * fix: Reenable integration tests * meta: Changelog * fix: Integrations * fix: Make setInterval number * fix: Don't use window
1 parent f68b3ec commit c3b7940

File tree

10 files changed

+161
-109
lines changed

10 files changed

+161
-109
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- [utils] ref: Move `htmlTreeAsString` to `@sentry/browser`
66
- [utils] ref: Remove `Window` typehint `getGlobalObject`
7+
- [core] fix: Make sure that flush/close works as advertised
78

89
## 5.0.6
910

packages/browser/test/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ describe('SentryBrowser', () => {
143143
captureMessage('event222');
144144
captureMessage('event222');
145145

146-
await flush(2000);
146+
await flush(10);
147147

148148
expect(localBeforeSend.calledTwice).to.be.true;
149149
});

packages/browser/test/integration/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function _alt(title, condition) {
6464
return condition ? '⚠️ Skipped: ' + title : title;
6565
}
6666

67-
var frames = ['frame'];
67+
var frames = ['frame', 'loader', 'loader-lazy-no'];
6868

6969
for (var idx in frames) {
7070
(function() {
@@ -911,7 +911,7 @@ for (var idx in frames) {
911911
);
912912
});
913913

914-
it.only("should capture built-in's handlers fn name in mechanism data", function(done) {
914+
it("should capture built-in's handlers fn name in mechanism data", function(done) {
915915
var iframe = this.iframe;
916916

917917
iframeExecute(

packages/core/src/baseclient.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
5858
/** Is the client still processing a call? */
5959
protected _processing: boolean = false;
6060

61+
/** Processing interval */
62+
protected _processingInterval?: number;
63+
6164
/**
6265
* Initializes this client instance.
6366
*
@@ -163,21 +166,22 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
163166
* @inheritDoc
164167
*/
165168
public async flush(timeout?: number): Promise<boolean> {
166-
return (await Promise.all([
167-
this._getBackend()
168-
.getTransport()
169-
.close(timeout),
170-
this._isClientProcessing(),
171-
])).reduce((prev, current) => prev && current);
169+
const clientReady = await this._isClientProcessing(timeout);
170+
if (this._processingInterval) {
171+
clearInterval(this._processingInterval);
172+
}
173+
const transportFlushed = await this._getBackend()
174+
.getTransport()
175+
.close(timeout);
176+
return clientReady && transportFlushed;
172177
}
173178

174179
/**
175180
* @inheritDoc
176181
*/
177182
public async close(timeout?: number): Promise<boolean> {
178-
return this.flush(timeout).finally(() => {
179-
this.getOptions().enabled = false;
180-
});
183+
this.getOptions().enabled = false;
184+
return this.flush(timeout);
181185
}
182186

183187
/**
@@ -200,20 +204,23 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
200204
}
201205

202206
/** Waits for the client to be done with processing. */
203-
protected async _isClientProcessing(counter: number = 0): Promise<boolean> {
207+
protected async _isClientProcessing(timeout?: number): Promise<boolean> {
204208
return new Promise<boolean>(resolve => {
205-
if (this._processing) {
206-
// Safeguard in case of endless recursion
207-
if (counter >= 10) {
208-
resolve(false);
209+
let ticked: number = 0;
210+
const tick: number = 1;
211+
if (this._processingInterval) {
212+
clearInterval(this._processingInterval);
213+
}
214+
this._processingInterval = (setInterval(() => {
215+
if (!this._processing) {
216+
resolve(true);
209217
} else {
210-
setTimeout(async () => {
211-
resolve(await this._isClientProcessing(counter + 1));
212-
}, 10);
218+
ticked += tick;
219+
if (timeout && ticked >= timeout) {
220+
resolve(false);
221+
}
213222
}
214-
} else {
215-
resolve(true);
216-
}
223+
}, tick) as unknown) as number;
217224
});
218225
}
219226

packages/core/test/lib/base.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SentryError } from '@sentry/utils';
55
import { TestBackend } from '../mocks/backend';
66
import { TestClient } from '../mocks/client';
77
import { TestIntegration } from '../mocks/integration';
8+
import { FakeTransport } from '../mocks/transport';
89

910
const PUBLIC_DSN = 'https://username@domain/path';
1011

@@ -427,4 +428,47 @@ describe('BaseClient', () => {
427428
expect(client.getIntegration(TestIntegration)).toBeTruthy();
428429
});
429430
});
431+
432+
describe('flush/close', () => {
433+
test('flush', async () => {
434+
jest.useRealTimers();
435+
expect.assertions(5);
436+
const client = new TestClient({
437+
dsn: PUBLIC_DSN,
438+
enableSend: true,
439+
transport: FakeTransport,
440+
});
441+
442+
const delay = 1;
443+
const transportInstance = (client as any)._getBackend().getTransport() as FakeTransport;
444+
transportInstance.delay = delay;
445+
446+
client.captureMessage('test');
447+
expect(transportInstance).toBeInstanceOf(FakeTransport);
448+
expect(transportInstance.sendCalled).toEqual(1);
449+
expect(transportInstance.sentCount).toEqual(0);
450+
await client.flush(delay);
451+
expect(transportInstance.sentCount).toEqual(1);
452+
expect(transportInstance.sendCalled).toEqual(1);
453+
});
454+
455+
test('close', async () => {
456+
jest.useRealTimers();
457+
expect.assertions(2);
458+
const client = new TestClient({
459+
dsn: PUBLIC_DSN,
460+
enableSend: true,
461+
transport: FakeTransport,
462+
});
463+
464+
const delay = 1;
465+
const transportInstance = (client as any)._getBackend().getTransport() as FakeTransport;
466+
transportInstance.delay = delay;
467+
468+
expect(client.captureMessage('test')).toBeTruthy();
469+
await client.close(delay);
470+
// Sends after close shouldn't work anymore
471+
expect(client.captureMessage('test')).toBeFalsy();
472+
});
473+
});
430474
});

packages/core/test/mocks/backend.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Event, Options } from '@sentry/types';
1+
import { Event, Options, Transport } from '@sentry/types';
22
import { SyncPromise } from '@sentry/utils';
33

44
import { BaseBackend } from '../../src/basebackend';
55

66
export interface TestOptions extends Options {
77
test?: boolean;
88
mockInstallFailure?: boolean;
9+
enableSend?: boolean;
910
}
1011

1112
export class TestBackend extends BaseBackend<TestOptions> {
@@ -19,6 +20,23 @@ export class TestBackend extends BaseBackend<TestOptions> {
1920
TestBackend.instance = this;
2021
}
2122

23+
protected _setupTransport(): Transport {
24+
if (!this._options.dsn) {
25+
// We return the noop transport here in case there is no Dsn.
26+
return super._setupTransport();
27+
}
28+
29+
const transportOptions = this._options.transportOptions
30+
? this._options.transportOptions
31+
: { dsn: this._options.dsn };
32+
33+
if (this._options.transport) {
34+
return new this._options.transport(transportOptions);
35+
}
36+
37+
return super._setupTransport();
38+
}
39+
2240
public eventFromException(exception: any): SyncPromise<Event> {
2341
return SyncPromise.resolve({
2442
exception: {
@@ -38,6 +56,10 @@ export class TestBackend extends BaseBackend<TestOptions> {
3856

3957
public sendEvent(event: Event): void {
4058
this.event = event;
59+
if (this._options.enableSend) {
60+
super.sendEvent(event);
61+
return;
62+
}
4163
// tslint:disable-next-line
4264
TestBackend.sendEventCalled && TestBackend.sendEventCalled(event);
4365
}

packages/core/test/mocks/transport.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Event, Response, Status, Transport } from '@sentry/types';
2+
import { PromiseBuffer } from '@sentry/utils';
3+
4+
async function sleep(delay: number): Promise<void> {
5+
return new Promise(resolve => setTimeout(resolve, delay));
6+
}
7+
8+
export class FakeTransport implements Transport {
9+
/** A simple buffer holding all requests. */
10+
protected readonly _buffer: PromiseBuffer<Response> = new PromiseBuffer(9999);
11+
12+
public sendCalled: number = 0;
13+
public sentCount: number = 0;
14+
public delay: number = 2000;
15+
16+
public async sendEvent(_event: Event): Promise<Response> {
17+
this.sendCalled += 1;
18+
return this._buffer.add(
19+
new Promise(async res => {
20+
await sleep(this.delay);
21+
this.sentCount += 1;
22+
res({ status: Status.Success });
23+
}),
24+
);
25+
}
26+
27+
public async close(timeout?: number): Promise<boolean> {
28+
return this._buffer.drain(timeout);
29+
}
30+
}

packages/integrations/src/captureconsole.ts

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { EventProcessor, Hub, Integration, Severity } from '@sentry/types';
2-
import { getGlobalObject } from '@sentry/utils/misc';
3-
import { fill, normalize } from '@sentry/utils/object';
4-
import { safeJoin } from '@sentry/utils/string';
2+
import { fill, getGlobalObject, normalize, safeJoin } from '@sentry/utils';
53

64
const global = getGlobalObject<Window | NodeJS.Global>();
75

@@ -39,49 +37,46 @@ export class CaptureConsole implements Integration {
3937
return;
4038
}
4139

42-
this._levels.forEach(function(level: string): void {
40+
this._levels.forEach((level: string) => {
4341
if (!(level in global.console)) {
4442
return;
4543
}
4644

47-
fill(global.console, level, function(originalConsoleLevel: () => any): any {
48-
// tslint:disable-next-line:only-arrow-functions
49-
return function(...args: any[]): any {
50-
const hub = getCurrentHub();
45+
fill(global.console, level, (originalConsoleLevel: () => any) => (...args: any[]) => {
46+
const hub = getCurrentHub();
5147

52-
if (hub.getIntegration(CaptureConsole)) {
53-
hub.withScope(scope => {
54-
scope.setLevel(Severity.fromString(level));
55-
scope.setExtra('arguments', normalize(args, 3));
56-
scope.addEventProcessor(event => {
57-
event.logger = 'console';
58-
if (event.sdk) {
59-
event.sdk = {
60-
...event.sdk,
61-
integrations: [...(event.sdk.integrations || []), 'console'],
62-
};
63-
}
64-
return event;
65-
});
48+
if (hub.getIntegration(CaptureConsole)) {
49+
hub.withScope(scope => {
50+
scope.setLevel(Severity.fromString(level));
51+
scope.setExtra('arguments', normalize(args, 3));
52+
scope.addEventProcessor(event => {
53+
event.logger = 'console';
54+
if (event.sdk) {
55+
event.sdk = {
56+
...event.sdk,
57+
integrations: [...(event.sdk.integrations || []), 'console'],
58+
};
59+
}
60+
return event;
61+
});
6662

67-
let message = safeJoin(args, ' ');
68-
if (level === 'assert') {
69-
if (args[0] === false) {
70-
message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`;
71-
scope.setExtra('arguments', normalize(args.slice(1), 3));
72-
hub.captureMessage(message);
73-
}
74-
} else {
63+
let message = safeJoin(args, ' ');
64+
if (level === 'assert') {
65+
if (args[0] === false) {
66+
message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`;
67+
scope.setExtra('arguments', normalize(args.slice(1), 3));
7568
hub.captureMessage(message);
7669
}
77-
});
78-
}
70+
} else {
71+
hub.captureMessage(message);
72+
}
73+
});
74+
}
7975

80-
// this fails for some browsers. :(
81-
if (originalConsoleLevel) {
82-
Function.prototype.apply.call(originalConsoleLevel, global.console, args);
83-
}
84-
};
76+
// this fails for some browsers. :(
77+
if (originalConsoleLevel) {
78+
Function.prototype.apply.call(originalConsoleLevel, global.console, args);
79+
}
8580
});
8681
});
8782
}

packages/integrations/src/debug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types';
2-
import { consoleSandbox } from '@sentry/utils/misc';
2+
import { consoleSandbox } from '@sentry/utils';
33

44
/** JSDoc */
55
interface DebugOptions {

packages/node/test/index.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -33,53 +33,6 @@ describe('SentryNode', () => {
3333
getCurrentHub().popScope();
3434
});
3535

36-
test('flush() with to short timeout', done => {
37-
expect.assertions(1);
38-
jest.useFakeTimers();
39-
const client = new NodeClient({
40-
dsn,
41-
transport: SetTimeoutTransport,
42-
});
43-
getCurrentHub().bindClient(client);
44-
captureMessage('test');
45-
captureMessage('test');
46-
captureMessage('test');
47-
client
48-
.flush(50)
49-
.then(result => {
50-
expect(result).toBeFalsy();
51-
done();
52-
})
53-
.catch(() => {
54-
// test
55-
});
56-
jest.runAllTimers();
57-
});
58-
59-
test('flush() with timeout', done => {
60-
expect.assertions(1);
61-
jest.useFakeTimers();
62-
const client = new NodeClient({
63-
dsn,
64-
transport: SetTimeoutTransport,
65-
});
66-
getCurrentHub().bindClient(client);
67-
captureMessage('test');
68-
captureMessage('test');
69-
captureMessage('test');
70-
jest.runAllTimers();
71-
client
72-
.flush(50)
73-
.then(result => {
74-
expect(result).toBeFalsy();
75-
done();
76-
})
77-
.catch(() => {
78-
// test
79-
});
80-
jest.runAllTimers();
81-
});
82-
8336
describe('getContext() / setContext()', () => {
8437
test('store/load extra', async () => {
8538
configureScope((scope: Scope) => {

0 commit comments

Comments
 (0)