Skip to content

Commit 1d64ace

Browse files
kamilogorekHazAT
authored andcommitted
fix: Rewrite transport and queue to improve memory consumption (#1795)
* fix: Limit concurrent FS calls to prevent memory fragmentation * fix: Move limiter to node, Add frameContextLines option * Update .gitignore * feat: Pass string into transport, Use request buffer directly in transport * feat: Use buffer in transport * feat: Add all changes for transport and buffers * fix: Linter * fix: Wording * fix: Don't break the API * feat: Add simple lru cache for readFiles * meta: Code Review * meta: Linter errros * fix: Changelog + docs * fix: Browser integration tests * fix: Also cache if the file wasn't readable * fix: Linter
1 parent 1fb4808 commit 1d64ace

File tree

40 files changed

+571
-331
lines changed

40 files changed

+571
-331
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Unreleased
44

5+
- [core] feat: Deprecate `captureEvent`, prefer `sendEvent` for transports. `sendEvent` now takes a string (body)
6+
instead of `Event` object.
7+
- [core] feat: Use correct buffer for requests in transports
8+
- [node] feat: Add file cache for providing pre/post context in frames
9+
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
10+
`7` lines pre/post
11+
- [core]: ref: Change way how transports are initialized
12+
- [core]: ref: Rename `RequestBuffer` to `PromiseBuffer`, also introduce limit
13+
514
## 4.4.2
615

716
- [node] Port memory-leak tests from raven-node

packages/browser/src/backend.ts

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { BaseBackend, Options, SentryError } from '@sentry/core';
2-
import { SentryEvent, SentryEventHint, SentryResponse, Severity, Status } from '@sentry/types';
2+
import { SentryEvent, SentryEventHint, Severity, Transport } from '@sentry/types';
33
import { isDOMError, isDOMException, isError, isErrorEvent, isPlainObject } from '@sentry/utils/is';
4-
import { logger } from '@sentry/utils/logger';
54
import { supportsBeacon, supportsFetch } from '@sentry/utils/supports';
65
import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers';
76
import { computeStackTrace } from './tracekit';
@@ -46,6 +45,27 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
4645
return true;
4746
}
4847

48+
/**
49+
* @inheritdoc
50+
*/
51+
protected setupTransport(): Transport {
52+
if (!this.options.dsn) {
53+
// We return the noop transport here in case there is no Dsn.
54+
return super.setupTransport();
55+
}
56+
57+
const transportOptions = this.options.transportOptions ? this.options.transportOptions : { dsn: this.options.dsn };
58+
59+
if (this.options.transport) {
60+
return new this.options.transport(transportOptions);
61+
} else if (supportsBeacon()) {
62+
return new BeaconTransport(transportOptions);
63+
} else if (supportsFetch()) {
64+
return new FetchTransport(transportOptions);
65+
}
66+
return new XHRTransport(transportOptions);
67+
}
68+
4969
/**
5070
* @inheritDoc
5171
*/
@@ -126,33 +146,4 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
126146

127147
return event;
128148
}
129-
130-
/**
131-
* @inheritDoc
132-
*/
133-
public async sendEvent(event: SentryEvent): Promise<SentryResponse> {
134-
if (!this.options.dsn) {
135-
logger.warn(`Event has been skipped because no Dsn is configured.`);
136-
// We do nothing in case there is no DSN
137-
return { status: Status.Skipped, reason: `Event has been skipped because no Dsn is configured.` };
138-
}
139-
140-
if (!this.transport) {
141-
const transportOptions = this.options.transportOptions
142-
? this.options.transportOptions
143-
: { dsn: this.options.dsn };
144-
145-
if (this.options.transport) {
146-
this.transport = new this.options.transport({ dsn: this.options.dsn });
147-
} else if (supportsBeacon()) {
148-
this.transport = new BeaconTransport(transportOptions);
149-
} else if (supportsFetch()) {
150-
this.transport = new FetchTransport(transportOptions);
151-
} else {
152-
this.transport = new XHRTransport(transportOptions);
153-
}
154-
}
155-
156-
return this.transport.captureEvent(event);
157-
}
158149
}
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { API, SentryError } from '@sentry/core';
2-
import { SentryEvent, SentryResponse, Transport, TransportOptions } from '@sentry/types';
1+
import { API, PromiseBuffer, SentryError } from '@sentry/core';
2+
import { SentryResponse, Transport, TransportOptions } from '@sentry/types';
33

44
/** Base Transport class implementation */
55
export abstract class BaseTransport implements Transport {
@@ -8,14 +8,24 @@ export abstract class BaseTransport implements Transport {
88
*/
99
public url: string;
1010

11+
/** A simple buffer holding all requests. */
12+
protected readonly buffer: PromiseBuffer<SentryResponse> = new PromiseBuffer(30);
13+
1114
public constructor(public options: TransportOptions) {
1215
this.url = new API(this.options.dsn).getStoreEndpointWithUrlEncodedAuth();
1316
}
1417

1518
/**
1619
* @inheritDoc
1720
*/
18-
public async captureEvent(_: SentryEvent): Promise<SentryResponse> {
19-
throw new SentryError('Transport Class has to implement `captureEvent` method');
21+
public async sendEvent(_: string): Promise<SentryResponse> {
22+
throw new SentryError('Transport Class has to implement `sendEvent` method');
23+
}
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public async close(timeout?: number): Promise<boolean> {
29+
return this.buffer.drain(timeout);
2030
}
2131
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { SentryEvent, SentryResponse, Status } from '@sentry/types';
1+
import { SentryResponse, Status } from '@sentry/types';
22
import { getGlobalObject } from '@sentry/utils/misc';
3-
import { serialize } from '@sentry/utils/object';
43
import { BaseTransport } from './base';
54

65
const global = getGlobalObject() as Window;
@@ -10,13 +9,13 @@ export class BeaconTransport extends BaseTransport {
109
/**
1110
* @inheritDoc
1211
*/
13-
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
14-
const data = serialize(event);
12+
public async sendEvent(body: string): Promise<SentryResponse> {
13+
const result = global.navigator.sendBeacon(this.url, body);
1514

16-
const result = global.navigator.sendBeacon(this.url, data);
17-
18-
return {
19-
status: result ? Status.Success : Status.Failed,
20-
};
15+
return this.buffer.add(
16+
Promise.resolve({
17+
status: result ? Status.Success : Status.Failed,
18+
}),
19+
);
2120
}
2221
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { SentryEvent, SentryResponse, Status } from '@sentry/types';
1+
import { SentryResponse, Status } from '@sentry/types';
22
import { getGlobalObject } from '@sentry/utils/misc';
3-
import { serialize } from '@sentry/utils/object';
43
import { supportsReferrerPolicy } from '@sentry/utils/supports';
54
import { BaseTransport } from './base';
65

@@ -11,9 +10,9 @@ export class FetchTransport extends BaseTransport {
1110
/**
1211
* @inheritDoc
1312
*/
14-
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
13+
public async sendEvent(body: string): Promise<SentryResponse> {
1514
const defaultOptions: RequestInit = {
16-
body: serialize(event),
15+
body,
1716
method: 'POST',
1817
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
1918
// https://caniuse.com/#feat=referrer-policy
@@ -22,10 +21,10 @@ export class FetchTransport extends BaseTransport {
2221
referrerPolicy: (supportsReferrerPolicy() ? 'origin' : '') as ReferrerPolicy,
2322
};
2423

25-
const response = await global.fetch(this.url, defaultOptions);
26-
27-
return {
28-
status: Status.fromHttpCode(response.status),
29-
};
24+
return this.buffer.add(
25+
global.fetch(this.url, defaultOptions).then(response => ({
26+
status: Status.fromHttpCode(response.status),
27+
})),
28+
);
3029
}
3130
}
Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
1-
import { SentryEvent, SentryResponse, Status } from '@sentry/types';
2-
import { serialize } from '@sentry/utils/object';
1+
import { SentryResponse, Status } from '@sentry/types';
32
import { BaseTransport } from './base';
43

54
/** `XHR` based transport */
65
export class XHRTransport extends BaseTransport {
76
/**
87
* @inheritDoc
98
*/
10-
public async captureEvent(event: SentryEvent): Promise<SentryResponse> {
11-
return new Promise<SentryResponse>((resolve, reject) => {
12-
const request = new XMLHttpRequest();
9+
public async sendEvent(body: string): Promise<SentryResponse> {
10+
return this.buffer.add(
11+
new Promise<SentryResponse>((resolve, reject) => {
12+
const request = new XMLHttpRequest();
1313

14-
request.onreadystatechange = () => {
15-
if (request.readyState !== 4) {
16-
return;
17-
}
14+
request.onreadystatechange = () => {
15+
if (request.readyState !== 4) {
16+
return;
17+
}
1818

19-
if (request.status === 200) {
20-
resolve({
21-
status: Status.fromHttpCode(request.status),
22-
});
23-
}
19+
if (request.status === 200) {
20+
resolve({
21+
status: Status.fromHttpCode(request.status),
22+
});
23+
}
2424

25-
reject(request);
26-
};
25+
reject(request);
26+
};
2727

28-
request.open('POST', this.url);
29-
request.send(serialize(event));
30-
});
28+
request.open('POST', this.url);
29+
request.send(body);
30+
}),
31+
);
3132
}
3233
}

packages/browser/test/backend.test.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import { expect } from 'chai';
2-
import { SentryEvent, SentryResponse, Status } from '../src';
2+
import { Status } from '../src';
33
import { BrowserBackend } from '../src/backend';
4-
import { BaseTransport } from '../src/transports';
5-
6-
class SimpleTransport extends BaseTransport {
7-
public async captureEvent(_: SentryEvent): Promise<SentryResponse> {
8-
return {
9-
status: Status.fromHttpCode(200),
10-
};
11-
}
12-
}
4+
import { SimpleTransport } from './mocks/simpletransport';
135

146
const dsn = 'https://[email protected]/42';
157
const testEvent = {
@@ -34,7 +26,7 @@ describe('BrowserBackend', () => {
3426
}
3527
});
3628

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

packages/browser/test/index.test.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { expect } from 'chai';
2-
import { SinonSpy, spy, stub } from 'sinon';
2+
import { SinonSpy, spy } from 'sinon';
33
import {
44
addBreadcrumb,
5-
BrowserBackend,
65
BrowserClient,
76
captureEvent,
87
captureException,
@@ -13,20 +12,21 @@ import {
1312
Integrations,
1413
Scope,
1514
SentryEvent,
16-
Status,
1715
} from '../src';
16+
import { SimpleTransport } from './mocks/simpletransport';
1817

1918
const dsn = 'https://[email protected]/4291';
2019

2120
declare var global: any;
2221

2322
describe('SentryBrowser', () => {
24-
const beforeSend: SinonSpy = spy();
23+
const beforeSend: SinonSpy = spy((event: SentryEvent) => event);
2524

2625
before(() => {
2726
init({
2827
beforeSend,
2928
dsn,
29+
transport: SimpleTransport,
3030
});
3131
});
3232

@@ -69,16 +69,6 @@ describe('SentryBrowser', () => {
6969
});
7070

7171
describe('breadcrumbs', () => {
72-
let s: sinon.SinonStub;
73-
74-
beforeEach(() => {
75-
s = stub(BrowserBackend.prototype, 'sendEvent').returns(Promise.resolve({ status: Status.Success }));
76-
});
77-
78-
afterEach(() => {
79-
s.restore();
80-
});
81-
8272
it('should record breadcrumbs', async () => {
8373
addBreadcrumb({ message: 'test1' });
8474
addBreadcrumb({ message: 'test2' });
@@ -90,16 +80,6 @@ describe('SentryBrowser', () => {
9080
});
9181

9282
describe('capture', () => {
93-
let s: sinon.SinonStub;
94-
95-
beforeEach(() => {
96-
s = stub(BrowserBackend.prototype, 'sendEvent').returns(Promise.resolve({ status: Status.Success }));
97-
});
98-
99-
afterEach(() => {
100-
s.restore();
101-
});
102-
10383
it('should capture an exception', async () => {
10484
try {
10585
throw new Error('test');

packages/browser/test/integration/init.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ window.sentryBreadcrumbs = [];
2222

2323
// stub transport so we don't actually transmit any data
2424
function DummyTransport() {}
25-
DummyTransport.prototype.captureEvent = function(event) {
25+
DummyTransport.prototype.sendEvent = function(event) {
2626
// console.log(JSON.stringify(event, null, 2));
27-
sentryData.push(event);
27+
sentryData.push(JSON.parse(event));
2828
done(sentryData);
2929
return Promise.resolve({
3030
status: 'success',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { SentryResponse, Status } from '../../src';
2+
import { BaseTransport } from '../../src/transports';
3+
4+
export class SimpleTransport extends BaseTransport {
5+
public async sendEvent(_: string): Promise<SentryResponse> {
6+
return this.buffer.add(
7+
Promise.resolve({
8+
status: Status.fromHttpCode(200),
9+
}),
10+
);
11+
}
12+
}

0 commit comments

Comments
 (0)