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 committed Jul 29, 2024
1 parent a880712 commit df45d65
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '76 KB',
limit: '78 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
limit: '89 KB',
limit: '90 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)',
Expand Down
38 changes: 30 additions & 8 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,20 +371,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}"`);

this._outcomes[key] = (this._outcomes[key] || 0) + 1;
DEBUG_BUILD && logger.log(`Recording outcome: "${key}"${count > 1 ? ` (${count} times)` : ''}`);
this._outcomes[key] = (this._outcomes[key] || 0) + count;
}
}

Expand Down Expand Up @@ -794,11 +795,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
.then(processedEvent => {
if (processedEvent === null) {
this.recordDroppedEvent('before_send', dataCategory, event);
if (isTransactionEvent(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._outcomes['span'] = (this._outcomes['span'] || 0) + spanCount;
this.recordDroppedEvent('before_send', 'span', spanCount);
}
throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
}
Expand All @@ -808,6 +809,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 Expand Up @@ -973,6 +986,15 @@ function processBeforeSend(
}

if (beforeSendTransaction) {
if (event.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 = event.spans.length;
event.sdkProcessingMetadata = {
...event.sdkProcessingMetadata,
spanCountBeforeProcessing: spanCountBefore,
};
}
return beforeSendTransaction(event, hint);
}
}
Expand Down
28 changes: 26 additions & 2 deletions packages/core/test/lib/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,30 @@ 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);

Expand Down Expand Up @@ -1120,8 +1144,6 @@ describe('BaseClient', () => {
});

test('calls `beforeSendSpan` and discards the span', () => {
expect.assertions(2);

const beforeSendSpan = jest.fn(() => null);
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan });
const client = new TestClient(options);
Expand Down Expand Up @@ -1149,6 +1171,7 @@ describe('BaseClient', () => {
expect(beforeSendSpan).toHaveBeenCalledTimes(2);
const capturedEvent = TestClient.instance!.event!;
expect(capturedEvent.spans).toHaveLength(0);
expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
});

test('calls `beforeSend` and logs info about invalid return value', () => {
Expand Down Expand Up @@ -2017,6 +2040,7 @@ describe('BaseClient', () => {

describe('hook removal with `on`', () => {
it('should return a cleanup function that, when executed, unregisters a hook', async () => {
jest.useFakeTimers();
expect.assertions(8);

const client = new TestClient(
Expand Down

0 comments on commit df45d65

Please sign in to comment.