Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
quantity: 4,
reason: 'sample_rate',
},
{
category: 'span',
quantity: expect.any(Number),
reason: 'sample_rate',
},
],
});
// exact number depends on performance observer emissions
expect(clientReport.discarded_events[1].quantity).toBeGreaterThanOrEqual(10);
});

await sentryTest.step('Wait for transactions to be discarded', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,15 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
quantity: 2,
reason: 'sample_rate',
},
{
category: 'span',
quantity: expect.any(Number),
reason: 'sample_rate',
},
],
});
// exact number depends on performance observer emissions
expect(clientReport.discarded_events[1].quantity).toBeGreaterThanOrEqual(3);
});

await sentryTest.step('Navigate to another page with meta tags', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
quantity: 1,
reason: 'sample_rate',
},
{
category: 'span',
quantity: 1,
reason: 'sample_rate',
},
],
});
});
Expand All @@ -81,6 +86,11 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
quantity: 1,
reason: 'sample_rate',
},
{
category: 'span',
quantity: 1,
reason: 'sample_rate',
},
],
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ describe('negative sampling (static)', () => {
});

createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
test('records sample_rate outcome for root span/transaction', async () => {
test('records sample_rate outcomes for the transaction and all of its spans', async () => {
// `/health` and `/ok` go through the same middleware, so the `/ok` transaction tells us how many
// spans the dropped `/health` transaction had. The count differs per runtime (e.g. Bun creates
// no Express spans), so derive it instead of hardcoding it.
let okSpanCount: number | undefined;
let droppedSpanCount: number | undefined;

const runner = createRunner()
.unignore('client_report')
// The `GET /ok` transaction is sent as soon as its span ends, while the negatively-sampled
Expand All @@ -16,19 +22,18 @@ describe('negative sampling (static)', () => {
// match unordered instead of asserting a fixed sequence.
.unordered()
.expect({
transaction: {
transaction: 'GET /ok',
transaction: transaction => {
expect(transaction.transaction).toBe('GET /ok');
okSpanCount = 1 + (transaction.spans?.length ?? 0);
},
})
.expect({
client_report: {
discarded_events: [
{
category: 'transaction',
quantity: 1,
reason: 'sample_rate',
},
],
client_report: clientReport => {
expect(clientReport.discarded_events).toEqual([
{ category: 'transaction', quantity: 1, reason: 'sample_rate' },
{ category: 'span', quantity: expect.any(Number), reason: 'sample_rate' },
]);
droppedSpanCount = clientReport.discarded_events[1]!.quantity;
},
})
.start();
Expand All @@ -40,6 +45,8 @@ describe('negative sampling (static)', () => {
expect((res2 as { status: string }).status).toBe('ok');

await runner.completed();

expect(droppedSpanCount).toBe(okSpanCount);
});
});
});
31 changes: 9 additions & 22 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1522,19 +1522,24 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
// Sampling for transaction happens somewhere else
const parsedSampleRate = typeof sampleRate === 'undefined' ? undefined : parseSampleRate(sampleRate);
const dataCategory = getDataCategoryByType(event.type);
// Spans that event processors removed are already recorded in `prepareEvent`, so all later
// span outcomes are relative to the span count of the prepared event.
let preparedSpanCount = 0;

return this._prepareEvent(event, hint, currentScope, isolationScope)
.then(prepared => {
if (prepared === null) {
throw _makeDoNotSendEventError('An event processor returned `null`, will not send event.');
}

preparedSpanCount = prepared.spans?.length || 0;

const isInternalException = (hint.data as { __sentry__: boolean })?.__sentry__ === true;
if (isInternalException) {
return prepared;
}

const result = processBeforeSend(this, options, prepared, hint, () => {
const result = processBeforeSend(options, prepared, hint, () => {
beforeSendDropReason = 'callback_error';
});
return _validateBeforeSendResult(result, beforeSendLabel);
Expand All @@ -1543,9 +1548,8 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
if (processedEvent === null) {
this.recordDroppedEvent(beforeSendDropReason, dataCategory);
if (isTransaction) {
const spans = event.spans || [];
// the transaction itself counts as one span, plus all the child spans that are added
this.recordDroppedEvent(beforeSendDropReason, 'span', 1 + spans.length);
this.recordDroppedEvent(beforeSendDropReason, 'span', 1 + preparedSpanCount);
}
const dropMessage = beforeSendDropReason === 'callback_error' ? 'threw an error' : 'returned `null`';
throw _makeDoNotSendEventError(`${beforeSendLabel} ${dropMessage}, will not send event.`);
Expand All @@ -1564,10 +1568,8 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
}

if (isTransaction) {
const spanCountBefore = processedEvent.sdkProcessingMetadata?.spanCountBeforeProcessing || 0;
const spanCountAfter = processedEvent.spans ? processedEvent.spans.length : 0;

const droppedSpanCount = spanCountBefore - spanCountAfter;
// Covers child spans dropped by `ignoreSpans` as well as spans removed by `beforeSendTransaction`
const droppedSpanCount = preparedSpanCount - (processedEvent.spans?.length || 0);
if (droppedSpanCount > 0) {
this.recordDroppedEvent('before_send', 'span', droppedSpanCount);
}
Expand Down Expand Up @@ -1721,7 +1723,6 @@ function _validateBeforeSendResult(
* Process the matching `beforeSendXXX` callback.
*/
function processBeforeSend(
client: Client,
options: ClientOptions,
event: Event,
hint: EventHint,
Expand Down Expand Up @@ -1798,25 +1799,11 @@ function processBeforeSend(
}
}

const droppedSpans = processedEvent.spans.length - processedSpans.length;
if (droppedSpans) {
client.recordDroppedEvent('before_send', 'span', droppedSpans);
}

processedEvent.spans = processedSpans;
}
}

if (beforeSendTransaction) {
if (processedEvent.spans) {
// We store the # of spans before processing in SDK metadata,
// so we can compare it afterwards to determine how many spans were dropped
const spanCountBefore = processedEvent.spans.length;
processedEvent.sdkProcessingMetadata = {
...event.sdkProcessingMetadata,
spanCountBeforeProcessing: spanCountBefore,
};
}
return safeCallback(
DEBUG_BUILD ? 'The `beforeSendTransaction` callback threw an error, dropping the event:' : '',
() => beforeSendTransaction(processedEvent as TransactionEvent, hint),
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export interface SdkProcessingMetadata {
dynamicSamplingContext?: Partial<DynamicSamplingContext>;
capturedSpanScope?: Scope;
capturedSpanIsolationScope?: Scope;
spanCountBeforeProcessing?: number;
ipAddress?: string;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,8 @@ export class SentrySpan implements Span {
return;
}

// The `sample_rate` outcome was already recorded when the span was started (see `_startRootSpan`).
DEBUG_BUILD && debug.log('[Tracing] Discarding standalone span because its trace was not chosen to be sampled.');
client.recordDroppedEvent('sample_rate', 'span');

return;
}

Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,14 @@ 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(dropReason || 'sample_rate', hasSpanStreamingEnabled(client) ? 'span' : 'transaction');
const outcomeReason = dropReason || 'sample_rate';
// A standalone span is sent on its own and never becomes a transaction.
// TODO(standalone): drop the `isStandalone` check once the static trace lifecycle is gone.
if (!hasSpanStreamingEnabled(client) && !spanArguments.isStandalone) {
client.recordDroppedEvent(outcomeReason, 'transaction');
}
// Child spans of this root record their own `span` outcome in `_startChildSpan`.
client.recordDroppedEvent(outcomeReason, 'span');
}

setCapturedScopesOnSpan(rootSpan, scope, isolationScope);
Expand Down Expand Up @@ -568,7 +575,7 @@ function _startChildSpan(
return childSpan;
}

if (hasSpanStreamingEnabled(client) && spanIsNonRecordingSpan(childSpan)) {
if (spanIsNonRecordingSpan(childSpan)) {
if (spanIsNonRecordingSpan(parentSpan) && parentSpan.dropReason) {
// We land here if the parent span was a segment span that was ignored (`ignoreSpans`).
// In this case, the child was also ignored (see `sampled` above) but we need to
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/utils/prepareEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export function prepareEvent(
// Skip event processors for internal exceptions to prevent recursion
// oxlint-disable-next-line typescript/prefer-optional-chain
const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true;
const isTransaction = event.type === 'transaction';
// Snapshot the count rather than reading `event.spans` later: processors get a shallow copy of the
// event, so one that mutates `spans` in place also mutates the original array.
const spanCountBeforeProcessing = event.spans?.length || 0;
const result: PromiseLike<Event | null> = isInternalException
? resolvedSyncPromise(prepared)
: notifyEventProcessors(eventProcessors, prepared, hint, 0, reason => {
Expand All @@ -118,8 +122,8 @@ export function prepareEvent(
}

client.recordDroppedEvent(reason, getDataCategoryByType(event.type));
if (event.type === 'transaction') {
client.recordDroppedEvent(reason, 'span', 1 + (event.spans || []).length);
if (isTransaction) {
client.recordDroppedEvent(reason, 'span', 1 + spanCountBeforeProcessing);
}
});

Expand All @@ -128,6 +132,13 @@ export function prepareEvent(
return null;
}

if (isTransaction && client) {
const droppedSpanCount = spanCountBeforeProcessing - (evt.spans?.length || 0);
if (droppedSpanCount > 0) {
client.recordDroppedEvent('event_processor', 'span', droppedSpanCount);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

// We apply the debug_meta field only after all event processors have ran, so that if any event processors modified
// file names (e.g.the RewriteFrames integration) the filename -> debug ID relationship isn't destroyed.
// This should not cause any PII issues, since we're only moving data that is already on the event and not adding
Expand Down
Loading
Loading