Skip to content

Commit d4e94fe

Browse files
authored
feat(core)!: Pass root spans to beforeSendSpan and disallow returning null (#14831)
- [x] Disallows returning null from `beforeSendSpan` - [x] Passes root spans to `beforeSendSpan` - [x] Adds entry to migration guide and changelog closes #14336
1 parent b23fcd1 commit d4e94fe

File tree

9 files changed

+365
-57
lines changed

9 files changed

+365
-57
lines changed

docs/migration/v8-to-v9.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ Sentry.init({
6868
});
6969
```
7070

71+
- Dropping spans in the `beforeSendSpan` hook is no longer possible.
72+
- The `beforeSendSpan` hook now receives the root span as well as the child spans.
7173
- In previous versions, we determined if tracing is enabled (for Tracing Without Performance) by checking if either `tracesSampleRate` or `traceSampler` are _defined_ at all, in `Sentry.init()`. This means that e.g. the following config would lead to tracing without performance (=tracing being enabled, even if no spans would be started):
7274

7375
```js
@@ -243,6 +245,10 @@ The following outlines deprecations that were introduced in version 8 of the SDK
243245
## General
244246

245247
- **Returning `null` from `beforeSendSpan` span is deprecated.**
248+
249+
Returning `null` from `beforeSendSpan` will now result in a warning being logged.
250+
In v9, dropping spans is not possible anymore within this hook.
251+
246252
- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9**
247253

248254
In v8, a setup like the following:

packages/core/src/client.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ import { logger } from './utils-hoist/logger';
5252
import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc';
5353
import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise';
5454
import { getPossibleEventMessages } from './utils/eventUtils';
55+
import { merge } from './utils/merge';
5556
import { parseSampleRate } from './utils/parseSampleRate';
5657
import { prepareEvent } from './utils/prepareEvent';
5758
import { showSpanDropWarning } from './utils/spanUtils';
59+
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
5860

5961
const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
6062
const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release';
@@ -1004,41 +1006,54 @@ function processBeforeSend(
10041006
hint: EventHint,
10051007
): PromiseLike<Event | null> | Event | null {
10061008
const { beforeSend, beforeSendTransaction, beforeSendSpan } = options;
1009+
let processedEvent = event;
10071010

1008-
if (isErrorEvent(event) && beforeSend) {
1009-
return beforeSend(event, hint);
1011+
if (isErrorEvent(processedEvent) && beforeSend) {
1012+
return beforeSend(processedEvent, hint);
10101013
}
10111014

1012-
if (isTransactionEvent(event)) {
1013-
if (event.spans && beforeSendSpan) {
1014-
const processedSpans: SpanJSON[] = [];
1015-
for (const span of event.spans) {
1016-
const processedSpan = beforeSendSpan(span);
1017-
if (processedSpan) {
1018-
processedSpans.push(processedSpan);
1019-
} else {
1020-
showSpanDropWarning();
1021-
client.recordDroppedEvent('before_send', 'span');
1015+
if (isTransactionEvent(processedEvent)) {
1016+
if (beforeSendSpan) {
1017+
// process root span
1018+
const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent));
1019+
if (!processedRootSpanJson) {
1020+
showSpanDropWarning();
1021+
} else {
1022+
// update event with processed root span values
1023+
processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
1024+
}
1025+
1026+
// process child spans
1027+
if (processedEvent.spans) {
1028+
const processedSpans: SpanJSON[] = [];
1029+
for (const span of processedEvent.spans) {
1030+
const processedSpan = beforeSendSpan(span);
1031+
if (!processedSpan) {
1032+
showSpanDropWarning();
1033+
processedSpans.push(span);
1034+
} else {
1035+
processedSpans.push(processedSpan);
1036+
}
10221037
}
1038+
processedEvent.spans = processedSpans;
10231039
}
1024-
event.spans = processedSpans;
10251040
}
10261041

10271042
if (beforeSendTransaction) {
1028-
if (event.spans) {
1043+
if (processedEvent.spans) {
10291044
// We store the # of spans before processing in SDK metadata,
10301045
// so we can compare it afterwards to determine how many spans were dropped
1031-
const spanCountBefore = event.spans.length;
1032-
event.sdkProcessingMetadata = {
1046+
const spanCountBefore = processedEvent.spans.length;
1047+
processedEvent.sdkProcessingMetadata = {
10331048
...event.sdkProcessingMetadata,
10341049
spanCountBeforeProcessing: spanCountBefore,
10351050
};
10361051
}
1037-
return beforeSendTransaction(event, hint);
1052+
return beforeSendTransaction(processedEvent as TransactionEvent, hint);
10381053
}
10391054
}
10401055

1041-
return event;
1056+
return processedEvent;
10421057
}
10431058

10441059
function isErrorEvent(event: Event): event is ErrorEvent {

packages/core/src/envelope.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
SessionItem,
1919
SpanEnvelope,
2020
SpanItem,
21-
SpanJSON,
2221
} from './types-hoist';
2322
import { dsnToString } from './utils-hoist/dsn';
2423
import {
@@ -127,13 +126,17 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
127126
const beforeSendSpan = client && client.getOptions().beforeSendSpan;
128127
const convertToSpanJSON = beforeSendSpan
129128
? (span: SentrySpan) => {
130-
const spanJson = beforeSendSpan(spanToJSON(span) as SpanJSON);
131-
if (!spanJson) {
129+
const spanJson = spanToJSON(span);
130+
const processedSpan = beforeSendSpan(spanJson);
131+
132+
if (!processedSpan) {
132133
showSpanDropWarning();
134+
return spanJson;
133135
}
134-
return spanJson;
136+
137+
return processedSpan;
135138
}
136-
: (span: SentrySpan) => spanToJSON(span);
139+
: spanToJSON;
137140

138141
const items: SpanItem[] = [];
139142
for (const span of spans) {

packages/core/src/types-hoist/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
290290
*
291291
* @returns A new span that will be sent or null if the span should not be sent.
292292
*/
293-
beforeSendSpan?: (span: SpanJSON) => SpanJSON | null;
293+
beforeSendSpan?: (span: SpanJSON) => SpanJSON;
294294

295295
/**
296296
* An event-processing callback for transaction events, guaranteed to be invoked after all other event

packages/core/src/utils/spanUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export function showSpanDropWarning(): void {
286286
consoleSandbox(() => {
287287
// eslint-disable-next-line no-console
288288
console.warn(
289-
'[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.',
289+
'[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly.',
290290
);
291291
});
292292
hasShownSpanDropWarning = true;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_PROFILE_ID } from '../semanticAttributes';
2+
import type { SpanJSON, TransactionEvent } from '../types-hoist';
3+
import { dropUndefinedKeys } from '../utils-hoist';
4+
5+
/**
6+
* Converts a transaction event to a span JSON object.
7+
*/
8+
export function convertTransactionEventToSpanJson(event: TransactionEvent): SpanJSON {
9+
const { trace_id, parent_span_id, span_id, status, origin, data, op } = event.contexts?.trace ?? {};
10+
11+
return dropUndefinedKeys({
12+
data: data ?? {},
13+
description: event.transaction,
14+
op,
15+
parent_span_id,
16+
span_id: span_id ?? '',
17+
start_timestamp: event.start_timestamp ?? 0,
18+
status,
19+
timestamp: event.timestamp,
20+
trace_id: trace_id ?? '',
21+
origin,
22+
profile_id: data?.[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined,
23+
exclusive_time: data?.[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined,
24+
measurements: event.measurements,
25+
is_segment: true,
26+
});
27+
}
28+
29+
/**
30+
* Converts a span JSON object to a transaction event.
31+
*/
32+
export function convertSpanJsonToTransactionEvent(span: SpanJSON): TransactionEvent {
33+
const event: TransactionEvent = {
34+
type: 'transaction',
35+
timestamp: span.timestamp,
36+
start_timestamp: span.start_timestamp,
37+
transaction: span.description,
38+
contexts: {
39+
trace: {
40+
trace_id: span.trace_id,
41+
span_id: span.span_id,
42+
parent_span_id: span.parent_span_id,
43+
op: span.op,
44+
status: span.status,
45+
origin: span.origin,
46+
data: {
47+
...span.data,
48+
...(span.profile_id && { [SEMANTIC_ATTRIBUTE_PROFILE_ID]: span.profile_id }),
49+
...(span.exclusive_time && { [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: span.exclusive_time }),
50+
},
51+
},
52+
},
53+
measurements: span.measurements,
54+
};
55+
56+
return dropUndefinedKeys(event);
57+
}

0 commit comments

Comments
 (0)