Skip to content

Commit 4fb6808

Browse files
msonnbclaude
andcommitted
feat(core)!: Isolate throwing user callbacks instead of capturing them as events
Wraps `beforeSend`, `beforeSendTransaction`, event processors, `tracesSampler`, `beforeBreadcrumb`, `beforeSendLog` and `beforeSendMetric` in `safeCallback`. A throwing or rejecting callback now drops the event/breadcrumb/log/metric (with a client report where one exists) instead of escaping into the calling code or being captured as an `internal` error event. `tracesSampler` falls back to the parent sampling decision, then `tracesSampleRate`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9792842 commit 4fb6808

17 files changed

Lines changed: 556 additions & 109 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
transport: loggingTransport,
7+
beforeSend() {
8+
throw new Error('beforeSend failed');
9+
},
10+
});
11+
12+
Sentry.captureException(new Error('this should get dropped because beforeSend throws'));
13+
14+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
15+
Sentry.flush();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { afterAll, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('records a client report and no extra error event when beforeSend throws', async () => {
9+
await createRunner(__dirname, 'scenario.ts')
10+
.unignore('client_report')
11+
.expect({
12+
client_report: {
13+
discarded_events: [
14+
{
15+
category: 'error',
16+
quantity: 1,
17+
reason: 'before_send',
18+
},
19+
],
20+
},
21+
})
22+
.start()
23+
.completed();
24+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
transport: loggingTransport,
7+
});
8+
9+
Sentry.addEventProcessor(() => {
10+
throw new Error('event processor failed');
11+
});
12+
13+
Sentry.captureException(new Error('this should get dropped because the event processor throws'));
14+
15+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
16+
Sentry.flush();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { afterAll, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('records a client report and no extra error event when an event processor throws', async () => {
9+
await createRunner(__dirname, 'scenario.ts')
10+
.unignore('client_report')
11+
.expect({
12+
client_report: {
13+
discarded_events: [
14+
{
15+
category: 'error',
16+
quantity: 1,
17+
reason: 'event_processor',
18+
},
19+
],
20+
},
21+
})
22+
.start()
23+
.completed();
24+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
transport: loggingTransport,
7+
tracesSampler: () => {
8+
throw new Error('tracesSampler failed');
9+
},
10+
});
11+
12+
Sentry.startSpan({ name: 'this should not be sampled because tracesSampler throws' }, () => {
13+
// no-op
14+
});
15+
16+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
17+
Sentry.flush();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { afterAll, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('records a client report and no error event when tracesSampler throws', async () => {
9+
await createRunner(__dirname, 'scenario.ts')
10+
.unignore('client_report')
11+
.expect({
12+
client_report: {
13+
discarded_events: [
14+
{
15+
category: 'span',
16+
quantity: 1,
17+
reason: 'sample_rate',
18+
},
19+
],
20+
},
21+
})
22+
.start()
23+
.completed();
24+
});

‎packages/core/src/breadcrumbs.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getClient, getIsolationScope } from './currentScopes';
22
import type { Breadcrumb, BreadcrumbHint } from './types/breadcrumb';
33
import { consoleSandbox } from './utils/debug-logger';
4+
import { safeCallback } from './utils/safeCallback';
45
import { dateTimestampInSeconds } from './utils/time';
56

67
/**
@@ -28,7 +29,11 @@ export function addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): vo
2829
const timestamp = dateTimestampInSeconds();
2930
const mergedBreadcrumb = { timestamp, ...breadcrumb };
3031
const finalBreadcrumb = beforeBreadcrumb
31-
? consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint))
32+
? safeCallback(
33+
'The `beforeBreadcrumb` callback threw an error, dropping the breadcrumb:',
34+
() => consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)),
35+
() => null,
36+
)
3237
: mergedBreadcrumb;
3338

3439
if (finalBreadcrumb === null) return;

‎packages/core/src/client.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { parseSampleRate } from './utils/parseSampleRate';
5050
import { prepareEvent } from './utils/prepareEvent';
5151
import { makePromiseBuffer, type PromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer';
5252
import { safeMathRandom } from './utils/randomSafeContext';
53+
import { safeCallback } from './utils/safeCallback';
5354
import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span';
5455
import { safeUnref } from './utils/timer';
5556
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
@@ -1738,7 +1739,12 @@ function processBeforeSend(
17381739
let processedEvent = event;
17391740

17401741
if (isErrorEvent(processedEvent) && beforeSend) {
1741-
return beforeSend(processedEvent, hint);
1742+
const errorEvent = processedEvent;
1743+
return safeCallback(
1744+
'The `beforeSend` callback threw an error, dropping the event:',
1745+
() => beforeSend(errorEvent, hint),
1746+
() => null,
1747+
);
17421748
}
17431749

17441750
if (isTransactionEvent(processedEvent)) {
@@ -1809,7 +1815,11 @@ function processBeforeSend(
18091815
spanCountBeforeProcessing: spanCountBefore,
18101816
};
18111817
}
1812-
return beforeSendTransaction(processedEvent as TransactionEvent, hint);
1818+
return safeCallback(
1819+
'The `beforeSendTransaction` callback threw an error, dropping the event:',
1820+
() => beforeSendTransaction(processedEvent as TransactionEvent, hint),
1821+
() => null,
1822+
);
18131823
}
18141824
}
18151825

‎packages/core/src/eventProcessors.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Event, EventHint } from './types/event';
33
import type { EventProcessor } from './types/eventprocessor';
44
import { debug } from './utils/debug-logger';
55
import { isThenable } from './utils/is';
6+
import { safeCallback } from './utils/safeCallback';
67
import { rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise';
78

89
/**
@@ -34,9 +35,15 @@ function _notifyEventProcessors(
3435
return event;
3536
}
3637

37-
const result = processor({ ...event }, hint);
38+
const processorName = `Event processor "${processor.id || '?'}"`;
3839

39-
DEBUG_BUILD && result === null && debug.log(`Event processor "${processor.id || '?'}" dropped event`);
40+
const result = safeCallback(
41+
`${processorName} threw an error, dropping event:`,
42+
() => processor({ ...event }, hint),
43+
() => null,
44+
);
45+
46+
DEBUG_BUILD && result === null && debug.log(`${processorName} dropped event`);
4047

4148
if (isThenable(result)) {
4249
return result.then(final => _notifyEventProcessors(final, hint, processors, index + 1));

‎packages/core/src/logs/internal.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Integration } from '../types/integration';
88
import type { Log, SerializedLog } from '../types/log';
99
import { consoleSandbox, debug } from '../utils/debug-logger';
1010
import { isParameterizedString } from '../utils/is';
11+
import { safeCallback } from '../utils/safeCallback';
1112
import { getCombinedScopeData } from '../utils/scopeData';
1213
import { getActiveSpan } from '../utils/spanUtils';
1314
import { timestampInSeconds } from '../utils/time';
@@ -142,8 +143,14 @@ export function _INTERNAL_captureLog(
142143

143144
client.emit('beforeCaptureLog', processedLog);
144145

145-
// We need to wrap this in `consoleSandbox` to avoid recursive calls to `beforeSendLog`
146-
const log = beforeSendLog ? consoleSandbox(() => beforeSendLog(processedLog)) : processedLog;
146+
const log = beforeSendLog
147+
? safeCallback(
148+
'The `beforeSendLog` callback threw an error, dropping the log:',
149+
// We need to wrap this in `consoleSandbox` to avoid recursive calls to `beforeSendLog`
150+
() => consoleSandbox(() => beforeSendLog(processedLog)),
151+
() => null,
152+
)
153+
: processedLog;
147154
if (!log) {
148155
client.recordDroppedEvent('before_send', 'log_item', 1);
149156
DEBUG_BUILD && debug.warn('beforeSendLog returned null, log will not be captured.');

0 commit comments

Comments
 (0)