Skip to content

Commit 7414e62

Browse files
committed
Add rate limits
1 parent 1ac4549 commit 7414e62

File tree

6 files changed

+48
-33
lines changed

6 files changed

+48
-33
lines changed

dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ setTimeout(() => {
1111
Sentry.init({
1212
dsn: process.env.SENTRY_DSN,
1313
release: '1.0',
14-
integrations: [eventLoopBlockIntegration({ maxBlockedEvents: 2 })],
14+
integrations: [eventLoopBlockIntegration({ maxEventsPerHour: 2 })],
1515
});
1616

1717
setTimeout(() => {

dev-packages/node-integration-tests/suites/thread-blocked-native/stop-and-start.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ function longWorkIgnored() {
2929
}
3030

3131
setTimeout(() => {
32-
threadBlocked.stopWorker();
32+
threadBlocked.stop();
3333

3434
setTimeout(() => {
3535
longWorkIgnored();
3636

3737
setTimeout(() => {
38-
threadBlocked.startWorker();
38+
threadBlocked.start();
3939

4040
setTimeout(() => {
4141
longWork();

dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('Thread Blocked Native', { timeout: 30_000 }, () => {
122122
.completed();
123123
});
124124

125-
test('multiple events via maxBlockedEvents', async () => {
125+
test('multiple events via maxEventsPerHour', async () => {
126126
await createRunner(__dirname, 'basic-multiple.mjs')
127127
.withMockSentryServer()
128128
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic-multiple') })

packages/node-native/src/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export interface ThreadBlockedIntegrationOptions {
1010
*/
1111
threshold: number;
1212
/**
13-
* Maximum number of blocked events to send.
13+
* Maximum number of blocked events to send per clock hour.
1414
*
1515
* Defaults to 1.
1616
*/
17-
maxBlockedEvents: number;
17+
maxEventsPerHour: number;
1818
/**
1919
* Tags to include with blocked events.
2020
*/

packages/node-native/src/event-loop-block-integration.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { POLL_RATIO } from './common';
99

1010
const { isPromise } = types;
1111

12-
const DEFAULT_THRESHOLD = 1_000;
12+
const DEFAULT_THRESHOLD_MS = 1_000;
1313

1414
function log(message: string, ...args: unknown[]): void {
15-
logger.log(`[Thread Blocked] ${message}`, ...args);
15+
logger.log(`[Sentry Block Event Loop] ${message}`, ...args);
1616
}
1717

1818
/**
@@ -32,15 +32,15 @@ async function getContexts(client: NodeClient): Promise<Contexts> {
3232

3333
const INTEGRATION_NAME = 'ThreadBlocked';
3434

35-
type ThreadBlockedInternal = { startWorker: () => void; stopWorker: () => void };
35+
type ThreadBlockedInternal = { start: () => void; stop: () => void };
3636

3737
const _eventLoopBlockIntegration = ((options: Partial<ThreadBlockedIntegrationOptions> = {}) => {
3838
let worker: Promise<() => void> | undefined;
3939
let client: NodeClient | undefined;
4040

4141
return {
4242
name: INTEGRATION_NAME,
43-
startWorker: () => {
43+
start: () => {
4444
if (worker) {
4545
return;
4646
}
@@ -49,7 +49,7 @@ const _eventLoopBlockIntegration = ((options: Partial<ThreadBlockedIntegrationOp
4949
worker = _startWorker(client, options);
5050
}
5151
},
52-
stopWorker: () => {
52+
stop: () => {
5353
if (worker) {
5454
// eslint-disable-next-line @typescript-eslint/no-floating-promises
5555
worker.then(stop => {
@@ -58,12 +58,11 @@ const _eventLoopBlockIntegration = ((options: Partial<ThreadBlockedIntegrationOp
5858
});
5959
}
6060
},
61-
async afterAllSetup(initClient: NodeClient) {
61+
afterAllSetup(initClient: NodeClient) {
6262
client = initClient;
6363

6464
registerThread();
65-
66-
this.startWorker();
65+
this.start();
6766
},
6867
} as Integration & ThreadBlockedInternal;
6968
}) satisfies IntegrationFn;
@@ -140,8 +139,8 @@ async function _startWorker(
140139
dist: initOptions.dist,
141140
sdkMetadata,
142141
appRootPath: integrationOptions.appRootPath,
143-
threshold: integrationOptions.threshold || DEFAULT_THRESHOLD,
144-
maxBlockedEvents: integrationOptions.maxBlockedEvents || 1,
142+
threshold: integrationOptions.threshold || DEFAULT_THRESHOLD_MS,
143+
maxEventsPerHour: integrationOptions.maxEventsPerHour || 1,
145144
staticTags: integrationOptions.staticTags || {},
146145
contexts,
147146
};
@@ -207,13 +206,13 @@ export function disableBlockedDetectionForCallback<T>(callback: () => T | Promis
207206
return callback();
208207
}
209208

210-
integration.stopWorker();
209+
integration.stop();
211210

212211
const result = callback();
213212
if (isPromise(result)) {
214-
return result.finally(() => integration.startWorker());
213+
return result.finally(() => integration.start());
215214
}
216215

217-
integration.startWorker();
216+
integration.start();
218217
return result;
219218
}

packages/node-native/src/event-loop-block-watchdog.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const {
2424
dist,
2525
dsn,
2626
environment,
27-
maxBlockedEvents,
27+
maxEventsPerHour,
2828
release,
2929
sdkMetadata,
3030
staticTags: tags,
@@ -33,22 +33,48 @@ const {
3333

3434
const pollInterval = threshold / POLL_RATIO
3535
const triggeredThreads = new Set<string>();
36-
let sentAnrEvents = 0;
3736

3837
function log(...msg: unknown[]): void {
3938
if (debug) {
4039
// eslint-disable-next-line no-console
41-
console.log('[Sentry Blocked Watchdog]', ...msg);
40+
console.log('[Sentry Block Event Loop Watchdog]', ...msg);
4241
}
4342
}
4443

44+
function createRateLimiter(maxEventsPerHour: number): () => boolean {
45+
let currentHour = 0;
46+
let currentCount = 0;
47+
48+
return function isRateLimited(): boolean {
49+
const hour = new Date().getHours();
50+
51+
if (hour !== currentHour) {
52+
currentHour = hour;
53+
currentCount = 0;
54+
}
55+
56+
if (currentCount >= maxEventsPerHour) {
57+
if (currentCount === maxEventsPerHour) {
58+
currentCount += 1;
59+
log(`Rate limit reached: ${currentCount} events in this hour`);
60+
}
61+
return true;
62+
}
63+
64+
currentCount += 1;
65+
return false;
66+
};
67+
68+
}
69+
4570
const url = getEnvelopeEndpointWithUrlEncodedAuth(dsn, tunnel, sdkMetadata.sdk);
4671
const transport = makeNodeTransport({
4772
url,
4873
recordDroppedEvent: () => {
4974
//
5075
},
5176
});
77+
const isRateLimited = createRateLimiter(maxEventsPerHour);
5278

5379
async function sendAbnormalSession(serializedSession: Session | undefined): Promise<void> {
5480
if (!serializedSession) {
@@ -193,12 +219,10 @@ function getExceptionAndThreads(
193219
}
194220

195221
async function sendAnrEvent(crashedThreadId: string): Promise<void> {
196-
if (sentAnrEvents >= maxBlockedEvents) {
222+
if (isRateLimited()) {
197223
return;
198224
}
199225

200-
sentAnrEvents += 1;
201-
202226
const threads = captureStackTrace<ThreadState>();
203227
const crashedThread = threads[crashedThreadId];
204228

@@ -235,14 +259,6 @@ async function sendAnrEvent(crashedThreadId: string): Promise<void> {
235259

236260
await transport.send(envelope);
237261
await transport.flush(2000);
238-
239-
if (sentAnrEvents >= maxBlockedEvents) {
240-
// Delay for 5 seconds so that stdio can flush if the main event loop ever restarts.
241-
// This is mainly for the benefit of logging or debugging.
242-
setTimeout(() => {
243-
process.exit(0);
244-
}, 5_000);
245-
}
246262
}
247263

248264
setInterval(async () => {

0 commit comments

Comments
 (0)