Skip to content

Commit

Permalink
feat(core): Capture # of dropped spans through beforeSendTransaction (
Browse files Browse the repository at this point in the history
#13022)

Fixes #12849.

This is a bit tricky because `beforeSendTransaction` can return a
promise, so in order to avoid dealing with this I put the # of spans on
the sdk metadata, and then compare that afterwards.
  • Loading branch information
mydea authored and krystofwoldrich committed Aug 13, 2024
1 parent eeae3cf commit 305f4f4
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 7 deletions.
32 changes: 25 additions & 7 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,21 +419,21 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
/**
* @inheritDoc
*/
public recordDroppedEvent(reason: EventDropReason, category: DataCategory, _event?: Event): void {
// Note: we use `event` in replay, where we overwrite this hook.

public recordDroppedEvent(reason: EventDropReason, category: DataCategory, eventOrCount?: Event | number): void {
if (this._options.sendClientReports) {
// TODO v9: We do not need the `event` passed as third argument anymore, and can possibly remove this overload
// If event is passed as third argument, we assume this is a count of 1
const count = typeof eventOrCount === 'number' ? eventOrCount : 1;

// We want to track each category (error, transaction, session, replay_event) separately
// but still keep the distinction between different type of outcomes.
// We could use nested maps, but it's much easier to read and type this way.
// A correct type for map-based implementation if we want to go that route
// would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
// With typescript 4.1 we could even use template literal types
const key = `${reason}:${category}`;
DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`);

// The following works because undefined + 1 === NaN and NaN is falsy
this._outcomes[key] = this._outcomes[key] + 1 || 1;
DEBUG_BUILD && logger.log(`Recording outcome: "${key}"${count > 1 ? ` (${count} times)` : ''}`);
this._outcomes[key] = (this._outcomes[key] || 0) + count;
}
}

Expand Down Expand Up @@ -778,6 +778,12 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
.then(processedEvent => {
if (processedEvent === null) {
this.recordDroppedEvent('before_send', dataCategory, event);
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);
}
throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
}

Expand All @@ -786,6 +792,18 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
this._updateSessionFromEvent(session, processedEvent);
}

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

const droppedSpanCount = spanCountBefore - spanCountAfter;
if (droppedSpanCount > 0) {
this.recordDroppedEvent('before_send', 'span', droppedSpanCount);
}
}

// None of the Sentry built event processor will update transaction name,
// so if the transaction name has been changed by an event processor, we know
// it has to come from custom event processor added by a user
Expand Down
63 changes: 63 additions & 0 deletions packages/core/test/lib/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,69 @@ describe('BaseClient', () => {
expect(TestClient.instance!.event!.transaction).toBe('/adopt/dont/shop');
});

test('calls `beforeSendTransaction` and drops spans', () => {
const beforeSendTransaction = jest.fn(event => {
event.spans = [{ span_id: 'span5', trace_id: 'trace1', start_timestamp: 1234 }];
return event;
});
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction });
const client = new TestClient(options);

client.captureEvent({
transaction: '/dogs/are/great',
type: 'transaction',
spans: [
{ span_id: 'span1', trace_id: 'trace1', start_timestamp: 1234 },
{ span_id: 'span2', trace_id: 'trace1', start_timestamp: 1234 },
{ span_id: 'span3', trace_id: 'trace1', start_timestamp: 1234 },
],
});

expect(beforeSendTransaction).toHaveBeenCalled();
expect(TestClient.instance!.event!.spans?.length).toBe(1);

expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
});

test('calls `beforeSendSpan` and uses the modified spans', () => {
expect.assertions(3);

const beforeSendSpan = jest.fn(span => {
span.data = { version: 'bravo' };
return span;
});

const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan });
const client = new TestClient(options);
const transaction: Event = {
transaction: '/cats/are/great',
type: 'transaction',
spans: [
{
description: 'first span',
span_id: '9e15bf99fbe4bc80',
start_timestamp: 1591603196.637835,
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
},
{
description: 'second span',
span_id: 'aa554c1f506b0783',
start_timestamp: 1591603196.637835,
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
},
],
};

client.captureEvent(transaction);

expect(beforeSendSpan).toHaveBeenCalledTimes(2);
const capturedEvent = TestClient.instance!.event!;
for (const [idx, span] of capturedEvent.spans!.entries()) {
const originalSpan = transaction.spans![idx];
expect(span).toEqual({ ...originalSpan, data: { version: 'bravo' } });
}
});

test('calls `beforeSend` and discards the event', () => {
expect.assertions(4);

Expand Down

0 comments on commit 305f4f4

Please sign in to comment.