-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(core): Sample errors after beforeSend while preserving session updates
#22671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
342bd89
b002e45
db044d0
a7cb866
f711e80
ae2228e
6cc6285
b653230
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,14 @@ | ||
| import { expect } from '@playwright/test'; | ||
| import { sentryTest } from '../../../../utils/fixtures'; | ||
| import { countEnvelopes } from '../../../../utils/helpers'; | ||
|
|
||
| sentryTest('parses a string sample rate', async ({ getLocalTestUrl, page }) => { | ||
| sentryTest('drops error events when sampleRate is the string "0"', async ({ getLocalTestUrl, page }) => { | ||
| const url = await getLocalTestUrl({ testDir: __dirname }); | ||
| const errorCountPromise = countEnvelopes(page, { envelopeType: 'event', timeout: 2000 }); | ||
|
|
||
| await page.goto(url); | ||
|
|
||
| await page.waitForFunction('window._testDone'); | ||
| await page.evaluate('window.Sentry.getClient().flush()'); | ||
|
|
||
| const count = await page.evaluate('window._errorCount'); | ||
|
|
||
| expect(count).toStrictEqual(0); | ||
| expect(await errorCountPromise).toBe(0); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import * as Sentry from '@sentry/browser'; | ||
|
|
||
| window.Sentry = Sentry; | ||
|
|
||
| Sentry.init({ | ||
| traceLifecycle: 'static', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '0.1', | ||
| sampleRate: 0, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| document.getElementById('throw-error').addEventListener('click', () => { | ||
| throw new Error('unhandled crash'); | ||
| }); | ||
|
|
||
| document.getElementById('capture-exception').addEventListener('click', () => { | ||
| Sentry.captureException(new Error('handled capture')); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| </head> | ||
| <body> | ||
| <button id="throw-error">Throw Error</button> | ||
| <button id="capture-exception">Capture Exception</button> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { expect } from '@playwright/test'; | ||
| import { sentryTest } from '../../../utils/fixtures'; | ||
| import { countEnvelopes, waitForSession } from '../../../utils/helpers'; | ||
|
|
||
| sentryTest( | ||
| 'marks session as unhandled when unhandled error is sampled out by sampleRate', | ||
| async ({ getLocalTestUrl, page }) => { | ||
| const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
|
||
| const pageloadSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); | ||
| await page.goto(url); | ||
| const pageloadSession = await pageloadSessionPromise; | ||
|
|
||
| const updatedSessionPromise = waitForSession(page, s => !s.init && s.status !== 'ok'); | ||
| const errorCountPromise = countEnvelopes(page, { envelopeType: 'event', timeout: 2000 }); | ||
| await page.locator('#throw-error').click(); | ||
| const updatedSession = await updatedSessionPromise; | ||
| const errorCount = await errorCountPromise; | ||
|
|
||
| // The error event is not sent — it was sampled out | ||
| expect(errorCount).toBe(0); | ||
|
|
||
| // But the session update is still sent, reflecting the crash | ||
| expect(updatedSession.sid).toBe(pageloadSession.sid); | ||
| expect(updatedSession.errors).toBe(1); | ||
| expect(updatedSession.status).toBe('unhandled'); | ||
| }, | ||
| ); | ||
|
|
||
| sentryTest( | ||
| 'marks session as errored when handled exception is sampled out by sampleRate', | ||
| async ({ getLocalTestUrl, page }) => { | ||
| const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
|
||
| const pageloadSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok'); | ||
| await page.goto(url); | ||
| const pageloadSession = await pageloadSessionPromise; | ||
|
|
||
| const updatedSessionPromise = waitForSession(page, s => !s.init); | ||
| const errorCountPromise = countEnvelopes(page, { envelopeType: 'event', timeout: 2000 }); | ||
| await page.locator('#capture-exception').click(); | ||
| const updatedSession = await updatedSessionPromise; | ||
| const errorCount = await errorCountPromise; | ||
|
|
||
| // The error event is not sent — it was sampled out | ||
| expect(errorCount).toBe(0); | ||
|
|
||
| // But the session update is still sent, recording the error | ||
| expect(updatedSession.sid).toBe(pageloadSession.sid); | ||
| expect(updatedSession.errors).toBe(1); | ||
| expect(updatedSession.status).toBe('ok'); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,7 +52,6 @@ import { makePromiseBuffer, type PromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from | |
| import { safeMathRandom } from './utils/randomSafeContext'; | ||
| import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; | ||
| import { showSpanDropWarning } from './utils/spanUtils'; | ||
| import { rejectedSyncPromise } from './utils/syncpromise'; | ||
| import { safeUnref } from './utils/timer'; | ||
| import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; | ||
| import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions'; | ||
|
|
@@ -1449,15 +1448,6 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
| // 0.0 === 0% events are sent | ||
| // Sampling for transaction happens somewhere else | ||
| const parsedSampleRate = typeof sampleRate === 'undefined' ? undefined : parseSampleRate(sampleRate); | ||
| if (isError && typeof parsedSampleRate === 'number' && safeMathRandom() > parsedSampleRate) { | ||
| this.recordDroppedEvent('sample_rate', 'error'); | ||
| return rejectedSyncPromise( | ||
| _makeDoNotSendEventError( | ||
| `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| const dataCategory = getDataCategoryByType(event.type); | ||
|
|
||
| return this._prepareEvent(event, hint, currentScope, isolationScope) | ||
|
|
@@ -1492,6 +1482,13 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
| this._updateSessionFromEvent(session, processedEvent); | ||
| } | ||
|
|
||
| if (isError && typeof parsedSampleRate === 'number' && safeMathRandom() > parsedSampleRate) { | ||
| this.recordDroppedEvent('sample_rate', 'error'); | ||
| throw _makeDoNotSendEventError( | ||
| `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, | ||
| ); | ||
| } | ||
|
Comment on lines
+1487
to
+1490
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixThis change was intentional to support session tracking. A potential improvement could be a two-phase processing system: handle session-relevant data, make the sampling decision, and only then execute the full, expensive processing pipeline for events that will be sent, if the spec allows. Prompt for AI Agent |
||
|
|
||
| if (isTransaction) { | ||
| const spanCountBefore = processedEvent.sdkProcessingMetadata?.spanCountBeforeProcessing || 0; | ||
| const spanCountAfter = processedEvent.spans ? processedEvent.spans.length : 0; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug:
Sentry.lastEventId()is now set for events that are dropped bysampleRate, breaking user feedback integrations likeshowReportDialog()which rely on a sent event.Severity: MEDIUM
Suggested Fix
Move the
setLastEventIdcall to after thesampleRatecheck within the_prepareEventfunction. This ensuresSentry.lastEventId()is only set for events that are actually sent, restoring the expected behavior for features that rely on it.Prompt for AI Agent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a known limitation. I made it more obvious: b653230