diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send-throws/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send-throws/test.ts index e30038efc57b..80d5813b5f88 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send-throws/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send-throws/test.ts @@ -14,7 +14,7 @@ test('records a client report and no extra error event when beforeSend throws', { category: 'error', quantity: 1, - reason: 'before_send', + reason: 'callback_error', }, ], }, diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/test.ts index 370cccc35410..0996c0841cf2 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/test.ts @@ -14,7 +14,7 @@ test('records a client report and no extra error event when an event processor t { category: 'error', quantity: 1, - reason: 'event_processor', + reason: 'callback_error', }, ], }, @@ -32,7 +32,7 @@ test('records a client report and no extra error event when an async event proce { category: 'error', quantity: 1, - reason: 'event_processor', + reason: 'callback_error', }, ], }, diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts index df6cbb3195b2..e511fc97e8a9 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts @@ -14,7 +14,7 @@ test('records a client report and no error event when tracesSampler throws', asy { category: 'span', quantity: 1, - reason: 'sample_rate', + reason: 'callback_error', }, ], }, diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 2de21ba37b41..83cc51c99f17 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -50,7 +50,7 @@ import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; import { makePromiseBuffer, type PromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; import { safeMathRandom } from './utils/randomSafeContext'; -import { safeCallback } from './utils/safeCallback'; +import { CALLBACK_ERROR, safeCallback } from './utils/safeCallback'; import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; import { safeUnref } from './utils/timer'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; @@ -1522,6 +1522,14 @@ export abstract class Client { const parsedSampleRate = typeof sampleRate === 'undefined' ? undefined : parseSampleRate(sampleRate); const dataCategory = getDataCategoryByType(event.type); + const recordDroppedEvent = (reason: EventDropReason): void => { + this.recordDroppedEvent(reason, dataCategory); + if (isTransaction) { + // the transaction itself counts as one span, plus all the child spans that are added + this.recordDroppedEvent(reason, 'span', 1 + (event.spans || []).length); + } + }; + return this._prepareEvent(event, hint, currentScope, isolationScope) .then(prepared => { if (prepared === null) { @@ -1539,13 +1547,7 @@ export abstract class Client { }) .then(processedEvent => { if (processedEvent === null) { - this.recordDroppedEvent('before_send', dataCategory); - if (isTransaction) { - const spans = event.spans || []; - // the transaction itself counts as one span, plus all the child spans that are added - const spanCount = 1 + spans.length; - this.recordDroppedEvent('before_send', 'span', spanCount); - } + recordDroppedEvent('before_send'); throw _makeDoNotSendEventError(`${beforeSendLabel} returned \`null\`, will not send event.`); } @@ -1587,6 +1589,11 @@ export abstract class Client { return processedEvent; }) .then(null, reason => { + if (reason === CALLBACK_ERROR) { + recordDroppedEvent('callback_error'); + throw _makeDoNotSendEventError('A user callback threw an error, will not send event.'); + } + if (_isDoNotSendEventError(reason) || _isInternalError(reason)) { throw reason; } @@ -1702,17 +1709,13 @@ function _validateBeforeSendResult( ): PromiseLike | Event | null { const invalidValueError = `${beforeSendLabel} must return \`null\` or a valid event.`; if (isThenable(beforeSendResult)) { - return beforeSendResult.then( - event => { - if (!isPlainObject(event) && event !== null) { - throw _makeInternalError(invalidValueError); - } - return event; - }, - e => { - throw _makeInternalError(`${beforeSendLabel} rejected with ${e}`); - }, - ); + // A rejection can only be `CALLBACK_ERROR` here, as `safeCallback` already handled the user callback rejecting + return beforeSendResult.then(event => { + if (!isPlainObject(event) && event !== null) { + throw _makeInternalError(invalidValueError); + } + return event; + }); } else if (!isPlainObject(beforeSendResult) && beforeSendResult !== null) { throw _makeInternalError(invalidValueError); } @@ -1743,7 +1746,9 @@ function processBeforeSend( return safeCallback( DEBUG_BUILD ? 'The `beforeSend` callback threw an error, dropping the event:' : '', () => beforeSend(errorEvent, hint), - () => null, + () => { + throw CALLBACK_ERROR; + }, ); } @@ -1818,7 +1823,9 @@ function processBeforeSend( return safeCallback( DEBUG_BUILD ? 'The `beforeSendTransaction` callback threw an error, dropping the event:' : '', () => beforeSendTransaction(processedEvent as TransactionEvent, hint), - () => null, + () => { + throw CALLBACK_ERROR; + }, ); } } diff --git a/packages/core/src/eventProcessors.ts b/packages/core/src/eventProcessors.ts index ef25375d7716..78946463f1b9 100644 --- a/packages/core/src/eventProcessors.ts +++ b/packages/core/src/eventProcessors.ts @@ -3,11 +3,12 @@ import type { Event, EventHint } from './types/event'; import type { EventProcessor } from './types/eventprocessor'; import { debug } from './utils/debug-logger'; import { isThenable } from './utils/is'; -import { safeCallback } from './utils/safeCallback'; +import { CALLBACK_ERROR, safeCallback } from './utils/safeCallback'; import { rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise'; /** * Process an array of event processors, returning the processed event (or `null` if the event was dropped). + * Rejects with `CALLBACK_ERROR` if a processor throws. */ export function notifyEventProcessors( processors: EventProcessor[], @@ -40,7 +41,9 @@ function _notifyEventProcessors( const result = safeCallback( DEBUG_BUILD ? `${processorName} threw an error, dropping event:` : '', () => processor({ ...event }, hint), - () => null, + () => { + throw CALLBACK_ERROR; + }, ); DEBUG_BUILD && result === null && debug.log(`${processorName} dropped event`); diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 3ad106dfbb32..64bbabad80e6 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -8,7 +8,7 @@ import type { Integration } from '../types/integration'; import type { Log, SerializedLog } from '../types/log'; import { consoleSandbox, debug } from '../utils/debug-logger'; import { isParameterizedString } from '../utils/is'; -import { safeCallback } from '../utils/safeCallback'; +import { CALLBACK_ERROR, safeCallback } from '../utils/safeCallback'; import { getCombinedScopeData } from '../utils/scopeData'; import { getActiveSpan } from '../utils/spanUtils'; import { timestampInSeconds } from '../utils/time'; @@ -144,13 +144,17 @@ export function _INTERNAL_captureLog( client.emit('beforeCaptureLog', processedLog); const log = beforeSendLog - ? safeCallback( + ? safeCallback( DEBUG_BUILD ? 'The `beforeSendLog` callback threw an error, dropping the log:' : '', // We need to wrap this in `consoleSandbox` to avoid recursive calls to `beforeSendLog` () => consoleSandbox(() => beforeSendLog(processedLog)), - () => null, + () => CALLBACK_ERROR, ) : processedLog; + if (log === CALLBACK_ERROR) { + client.recordDroppedEvent('callback_error', 'log_item', 1); + return; + } if (!log) { client.recordDroppedEvent('before_send', 'log_item', 1); DEBUG_BUILD && debug.warn('beforeSendLog returned null, log will not be captured.'); diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 621992b2ed70..443a5c508e80 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -8,7 +8,7 @@ import type { Integration } from '../types/integration'; import type { Metric, SerializedMetric } from '../types/metric'; import type { User } from '../types/user'; import { debug } from '../utils/debug-logger'; -import { safeCallback } from '../utils/safeCallback'; +import { CALLBACK_ERROR, safeCallback } from '../utils/safeCallback'; import { getCombinedScopeData } from '../utils/scopeData'; import { getActiveSpan } from '../utils/spanUtils'; import { timestampInSeconds } from '../utils/time'; @@ -183,13 +183,18 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal client.emit('processMetric', enrichedMetric); const processedMetric = beforeSendMetric - ? safeCallback( + ? safeCallback( DEBUG_BUILD ? 'The `beforeSendMetric` callback threw an error, dropping the metric:' : '', () => beforeSendMetric(enrichedMetric), - () => null, + () => CALLBACK_ERROR, ) : enrichedMetric; + if (processedMetric === CALLBACK_ERROR) { + client.recordDroppedEvent('callback_error', 'metric', 1); + return; + } + if (!processedMetric) { client.recordDroppedEvent('before_send', 'metric', 1); DEBUG_BUILD && debug.log('`beforeSendMetric` returned `null`, will not send metric.'); diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 2b9e696fc3fe..41268d658af6 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -6,6 +6,14 @@ import { hasSpansEnabled } from '../utils/hasSpansEnabled'; import { parseSampleRate } from '../utils/parseSampleRate'; import { safeCallback } from '../utils/safeCallback'; +interface SamplingDecision { + sampled: boolean; + sampleRate?: number; + localSampleRateWasApplied?: boolean; + /** Set when the span was dropped for a reason other than the sampling decision itself. */ + dropReason?: 'callback_error'; +} + /** * Makes a sampling decision for the given options. * @@ -16,15 +24,17 @@ export function sampleSpan( options: Pick, samplingContext: SamplingContext, sampleRand: number, -): [sampled: boolean, sampleRate?: number, localSampleRateWasApplied?: boolean] { +): SamplingDecision { // nothing to do if span recording is not enabled if (!hasSpansEnabled(options)) { - return [false]; + return { sampled: false }; } const resolved = resolveSampleRate(options, samplingContext); if (!resolved) { - return [false]; + // `hasSpansEnabled` guarantees either `tracesSampleRate` or `tracesSampler` is set, so the only way to end up + // without a sample rate is a throwing `tracesSampler` with nothing to fall back to. + return { sampled: false, dropReason: 'callback_error' }; } const [sampleRate, localSampleRateWasApplied] = resolved; @@ -39,7 +49,7 @@ export function sampleSpan( sampleRate, )} of type ${JSON.stringify(typeof sampleRate)}.`, ); - return [false]; + return { sampled: false }; } // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped @@ -52,7 +62,7 @@ export function sampleSpan( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); - return [false, parsedSampleRate, localSampleRateWasApplied]; + return { sampled: false, sampleRate: parsedSampleRate, localSampleRateWasApplied }; } // We always compare the sample rand for the current execution context against the chosen sample rate. @@ -69,7 +79,7 @@ export function sampleSpan( ); } - return [shouldSample, parsedSampleRate, localSampleRateWasApplied]; + return { sampled: shouldSample, sampleRate: parsedSampleRate, localSampleRateWasApplied }; } /** diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index b7ac40595830..f7584ef49353 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -495,8 +495,8 @@ function _startRootSpan( const currentPropagationContext = scope.getPropagationContext(); const _isTracingSuppressed = isTracingSuppressed(scope); - const [sampled, sampleRate, localSampleRateWasApplied] = _isTracingSuppressed - ? [false] + const { sampled, sampleRate, localSampleRateWasApplied, dropReason } = _isTracingSuppressed + ? { sampled: false } : sampleSpan( options, { @@ -522,7 +522,7 @@ function _startRootSpan( if (!sampled && client && !_isTracingSuppressed) { DEBUG_BUILD && debug.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.'); - client.recordDroppedEvent('sample_rate', hasSpanStreamingEnabled(client) ? 'span' : 'transaction'); + client.recordDroppedEvent(dropReason || 'sample_rate', hasSpanStreamingEnabled(client) ? 'span' : 'transaction'); } setCapturedScopesOnSpan(rootSpan, scope, isolationScope); diff --git a/packages/core/src/types/clientreport.ts b/packages/core/src/types/clientreport.ts index 154b58c5705e..d9966813ba8e 100644 --- a/packages/core/src/types/clientreport.ts +++ b/packages/core/src/types/clientreport.ts @@ -2,6 +2,7 @@ import type { DataCategory } from './datacategory'; export type EventDropReason = | 'before_send' + | 'callback_error' | 'event_processor' | 'network_error' | 'queue_overflow' diff --git a/packages/core/src/utils/safeCallback.ts b/packages/core/src/utils/safeCallback.ts index 5b9079ca7c5d..e01e97037e28 100644 --- a/packages/core/src/utils/safeCallback.ts +++ b/packages/core/src/utils/safeCallback.ts @@ -2,6 +2,13 @@ import { DEBUG_BUILD } from '../debug-build'; import { debug } from './debug-logger'; import { isThenable } from './is'; +/** + * Lets a `safeCallback` fallback signal "the callback failed" as opposed to "the callback returned `null`", + * so the call site can report the drop as `callback_error`. Return it from synchronous call sites; throw it + * to abort a promise chain. + */ +export const CALLBACK_ERROR = Symbol.for('SentryCallbackError'); + /** * Invokes a user-provided callback (e.g. `beforeSend`, `tracesSampler`, an integration hook) so that * neither a synchronous throw nor a rejected promise escapes into the caller. On failure the error is diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index e43c35d2fe63..fae5ff453e18 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -2222,7 +2222,7 @@ describe('Client', () => { expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('event_processor', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); expect(debugErrorSpy).toHaveBeenCalledWith('Event processor "?" threw an error, dropping event:', exception); }); @@ -2244,7 +2244,7 @@ describe('Client', () => { expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('event_processor', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); expect(debugErrorSpy).toHaveBeenCalledWith('Event processor "?" threw an error, dropping event:', exception); }); @@ -2277,7 +2277,7 @@ describe('Client', () => { expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('event_processor', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); }); test('a rejecting event processor stops the processor chain', async () => { @@ -2312,7 +2312,7 @@ describe('Client', () => { expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('event_processor', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); }); test('client-level event processor that throws on all events does not capture a new event', () => { @@ -2348,7 +2348,7 @@ describe('Client', () => { expect(beforeSend).toHaveBeenCalledTimes(1); expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); expect(recordDroppedEventSpy).toHaveBeenCalledTimes(1); expect(debugErrorSpy).toHaveBeenCalledWith( 'The `beforeSend` callback threw an error, dropping the event:', @@ -2373,7 +2373,7 @@ describe('Client', () => { expect(beforeSend).toHaveBeenCalledTimes(1); expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'error'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'error'); expect(debugErrorSpy).toHaveBeenCalledWith( 'The `beforeSend` callback threw an error, dropping the event:', exception, @@ -2416,8 +2416,8 @@ describe('Client', () => { expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'transaction'); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span', 3); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'transaction'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'span', 3); expect(debugErrorSpy).toHaveBeenCalledWith( 'The `beforeSendTransaction` callback threw an error, dropping the event:', exception, diff --git a/packages/core/test/lib/eventProcessors.test.ts b/packages/core/test/lib/eventProcessors.test.ts index 5570788cdcaf..0f3a84ba12f2 100644 --- a/packages/core/test/lib/eventProcessors.test.ts +++ b/packages/core/test/lib/eventProcessors.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { notifyEventProcessors } from '../../src/eventProcessors'; +import { CALLBACK_ERROR } from '../../src/utils/safeCallback'; import type { EventProcessor } from '../../src/types/eventprocessor'; import * as debugLoggerModule from '../../src/utils/debug-logger'; @@ -24,7 +25,7 @@ describe('notifyEventProcessors', () => { expect(later).not.toHaveBeenCalled(); }); - it('drops the event when a processor throws synchronously', async () => { + it('rejects with `CALLBACK_ERROR` when a processor throws synchronously', async () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error'); const error = new Error('boom'); const throwing: EventProcessor = () => { @@ -33,21 +34,21 @@ describe('notifyEventProcessors', () => { throwing.id = 'Throwing'; const later = vi.fn(event => event); - const result = await notifyEventProcessors([throwing, later], { message: 'hello' }, {}); + await expect(notifyEventProcessors([throwing, later], { message: 'hello' }, {})).rejects.toBe(CALLBACK_ERROR); - expect(result).toBeNull(); expect(later).not.toHaveBeenCalled(); expect(debugErrorSpy).toHaveBeenCalledWith('Event processor "Throwing" threw an error, dropping event:', error); }); - it('drops the event when a processor rejects', async () => { + it('rejects with `CALLBACK_ERROR` when a processor rejects', async () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error'); const error = new Error('boom'); const later = vi.fn(event => event); - const result = await notifyEventProcessors([() => Promise.reject(error), later], { message: 'hello' }, {}); + await expect(notifyEventProcessors([() => Promise.reject(error), later], { message: 'hello' }, {})).rejects.toBe( + CALLBACK_ERROR, + ); - expect(result).toBeNull(); expect(later).not.toHaveBeenCalled(); expect(debugErrorSpy).toHaveBeenCalledWith('Event processor "?" threw an error, dropping event:', error); }); diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index d34df4ba16e6..fa3797d7dfac 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -389,7 +389,7 @@ describe('_INTERNAL_captureLog', () => { expect(() => _INTERNAL_captureLog({ level: 'info', message: 'test message' }, scope)).not.toThrow(); expect(beforeSendLog).toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'log_item', 1); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'log_item', 1); expect(debugErrorSpy).toHaveBeenCalledWith( 'The `beforeSendLog` callback threw an error, dropping the log:', exception, diff --git a/packages/core/test/lib/metrics/internal.test.ts b/packages/core/test/lib/metrics/internal.test.ts index 95e2a4ccea97..3efda3259dfb 100644 --- a/packages/core/test/lib/metrics/internal.test.ts +++ b/packages/core/test/lib/metrics/internal.test.ts @@ -385,7 +385,7 @@ describe('_INTERNAL_captureMetric', () => { expect(() => _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope })).not.toThrow(); expect(beforeSendMetric).toHaveBeenCalled(); - expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'metric', 1); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('callback_error', 'metric', 1); expect(debugErrorSpy).toHaveBeenCalledWith( 'The `beforeSendMetric` callback threw an error, dropping the metric:', exception, diff --git a/packages/core/test/lib/tracing/sampling.test.ts b/packages/core/test/lib/tracing/sampling.test.ts index 5caa3ea35470..1ef40b367bfc 100644 --- a/packages/core/test/lib/tracing/sampling.test.ts +++ b/packages/core/test/lib/tracing/sampling.test.ts @@ -14,35 +14,36 @@ describe('sampleSpan', () => { it('inherits the parent sampling decision', () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error'); - expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {}, parentSampled: true }, 0.5)).toEqual([ - true, - 1, - undefined, - ]); - expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {}, parentSampled: false }, 0.5)).toEqual([ - false, - 0, - undefined, - ]); + expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {}, parentSampled: true }, 0.5)).toEqual({ + sampled: true, + sampleRate: 1, + }); + expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {}, parentSampled: false }, 0.5)).toEqual({ + sampled: false, + sampleRate: 0, + }); expect(debugErrorSpy).toHaveBeenCalledWith(expectedMessage, exception); }); it('falls back to `tracesSampleRate` without a parent decision', () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error'); - expect(sampleSpan({ tracesSampler, tracesSampleRate: 0.6 }, { name: 'test', attributes: {} }, 0.5)).toEqual([ - true, - 0.6, - true, - ]); + expect(sampleSpan({ tracesSampler, tracesSampleRate: 0.6 }, { name: 'test', attributes: {} }, 0.5)).toEqual({ + sampled: true, + sampleRate: 0.6, + localSampleRateWasApplied: true, + }); expect(debugErrorSpy).toHaveBeenCalledWith(expectedMessage, exception); }); - it('does not sample when there is nothing to fall back to', () => { + it('does not sample and reports a `callback_error` drop when there is nothing to fall back to', () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error'); const debugWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn'); - expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {} }, 0.5)).toEqual([false]); + expect(sampleSpan({ tracesSampler }, { name: 'test', attributes: {} }, 0.5)).toEqual({ + sampled: false, + dropReason: 'callback_error', + }); expect(debugErrorSpy).toHaveBeenCalledWith(expectedMessage, exception); expect(debugWarnSpy).not.toHaveBeenCalled(); });