Skip to content

Commit 46182e9

Browse files
committed
ensure standalone spans are filtered out when transaction is active
1 parent 922ccf3 commit 46182e9

File tree

4 files changed

+188
-7
lines changed

4 files changed

+188
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
tracesSampleRate: 1.0,
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Sentry.startSpan({ name: 'outer' }, () => {
2+
Sentry.startSpan({ name: 'inner' }, () => {});
3+
Sentry.startSpan({ name: 'standalone', experimental: { standalone: true } }, () => {});
4+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { expect } from '@playwright/test';
2+
import type { Envelope, Event, EventEnvelope, EventItem, SpanEnvelope, TransactionEvent } from '@sentry/types';
3+
4+
import exp from 'constants';
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import {
7+
getMultipleSentryEnvelopeRequests,
8+
properFullEnvelopeRequestParser,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
12+
sentryTest(
13+
'sends a transaction and a span envelope if a standalone span is created as a child of an ongoing span tree',
14+
async ({ getLocalTestPath, page }) => {
15+
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
16+
17+
if (shouldSkipTracingTest()) {
18+
sentryTest.skip();
19+
}
20+
21+
const url = await getLocalTestPath({ testDir: __dirname });
22+
const envelopes = await getMultipleSentryEnvelopeRequests<Envelope>(
23+
page,
24+
2,
25+
{ url, envelopeType: ['transaction', 'span'] },
26+
properFullEnvelopeRequestParser,
27+
);
28+
29+
const spanEnvelope = envelopes.find(envelope => envelope[1][0][0].type === 'span') as SpanEnvelope;
30+
const transactionEnvelope = envelopes.find(envelope => envelope[1][0][0].type === 'transaction') as EventEnvelope;
31+
32+
const spanEnvelopeHeader = spanEnvelope[0];
33+
const spanEnvelopeItem = spanEnvelope[1][0][1];
34+
35+
const transactionEnvelopeHeader = transactionEnvelope[0];
36+
const transactionEnvelopeItem = transactionEnvelope[1][0][1] as TransactionEvent;
37+
38+
const traceId = transactionEnvelopeHeader.trace!.trace_id!;
39+
const parentSpanId = transactionEnvelopeItem.contexts?.trace?.span_id;
40+
41+
expect(traceId).toMatch(/[a-f0-9]{32}/);
42+
expect(parentSpanId).toMatch(/[a-f0-9]{16}/);
43+
44+
// TODO: the span envelope also needs to contain the `trace` header (follow-up PR)
45+
expect(spanEnvelopeHeader).toEqual({
46+
sent_at: expect.any(String),
47+
});
48+
49+
expect(transactionEnvelopeHeader).toEqual({
50+
event_id: expect.any(String),
51+
sdk: {
52+
name: 'sentry.javascript.browser',
53+
version: expect.any(String),
54+
},
55+
sent_at: expect.any(String),
56+
trace: {
57+
environment: 'production',
58+
public_key: 'public',
59+
sample_rate: '1',
60+
sampled: 'true',
61+
trace_id: traceId,
62+
transaction: 'outer',
63+
},
64+
});
65+
66+
expect(spanEnvelopeItem).toEqual({
67+
data: {
68+
'sentry.origin': 'manual',
69+
},
70+
description: 'standalone',
71+
segment_id: spanEnvelopeItem.span_id,
72+
is_segment: true,
73+
parent_span_id: parentSpanId,
74+
origin: 'manual',
75+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
76+
start_timestamp: expect.any(Number),
77+
timestamp: expect.any(Number),
78+
trace_id: traceId,
79+
});
80+
81+
expect(transactionEnvelopeItem).toEqual({
82+
contexts: {
83+
trace: {
84+
data: {
85+
'sentry.origin': 'manual',
86+
'sentry.sample_rate': 1,
87+
'sentry.source': 'custom',
88+
},
89+
origin: 'manual',
90+
span_id: parentSpanId,
91+
trace_id: traceId,
92+
},
93+
},
94+
environment: 'production',
95+
event_id: expect.any(String),
96+
platform: 'javascript',
97+
request: {
98+
headers: expect.any(Object),
99+
url: expect.any(String),
100+
},
101+
sdk: expect.any(Object),
102+
spans: [
103+
{
104+
data: {
105+
'sentry.origin': 'manual',
106+
},
107+
description: 'inner',
108+
origin: 'manual',
109+
parent_span_id: parentSpanId,
110+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
111+
start_timestamp: expect.any(Number),
112+
timestamp: expect.any(Number),
113+
trace_id: traceId,
114+
},
115+
],
116+
start_timestamp: expect.any(Number),
117+
timestamp: expect.any(Number),
118+
transaction: 'outer',
119+
transaction_info: {
120+
source: 'custom',
121+
},
122+
type: 'transaction',
123+
});
124+
},
125+
);

packages/core/src/tracing/sentrySpan.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
TransactionEvent,
1414
TransactionSource,
1515
} from '@sentry/types';
16-
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
16+
import { consoleSandbox, dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1717
import { getClient, getCurrentScope } from '../currentScopes';
1818
import { DEBUG_BUILD } from '../debug-build';
1919

@@ -229,24 +229,55 @@ export class SentrySpan implements Span {
229229
return this;
230230
}
231231

232+
/**
233+
* This method should generally not be used,
234+
* but for now we need a way to publicly check if the `_isStandaloneSpan` flag is set.
235+
* USE THIS WITH CAUTION!
236+
* @internal
237+
* @hidden
238+
* @experimental
239+
*/
240+
public isStandaloneSpan(): boolean {
241+
return !!this._isStandaloneSpan;
242+
}
243+
232244
/** Emit `spanEnd` when the span is ended. */
233245
private _onSpanEnded(): void {
234246
const client = getClient();
235247
if (client) {
236248
client.emit('spanEnd', this);
237249
}
238250

239-
// If this is not a root span (== segment span) and we don't want to send it standalone, we're done.
240-
// otherwise, we send it
241-
if (this !== getRootSpan(this)) {
251+
// A segment span is basically the root span of a local span tree.
252+
// So for now, this is either what we previously refer to as the root span,
253+
// or a standalone span.
254+
const isSegmentSpan = this._isStandaloneSpan || this === getRootSpan(this);
255+
consoleSandbox(() => {
256+
console.log('isSegmentSpan', isSegmentSpan);
257+
});
258+
259+
// If this is not a root span (== segment span) and we don't want to send it
260+
if (!isSegmentSpan) {
261+
consoleSandbox(() => {
262+
console.log('early ret');
263+
});
242264
return;
243265
}
244266

267+
// if this is a standalone span, we send it immediately
245268
if (this._isStandaloneSpan) {
269+
consoleSandbox(() => {
270+
console.log('calling sendSpanEnvelope');
271+
});
272+
246273
sendSpanEnvelope(createSpanEnvelope([this]));
247274
return;
248275
}
249276

277+
consoleSandbox(() => {
278+
console.log('converting to transaction');
279+
});
280+
250281
const transactionEvent = this._convertSpanToTransaction();
251282
if (transactionEvent) {
252283
const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope();
@@ -283,10 +314,9 @@ export class SentrySpan implements Span {
283314
return undefined;
284315
}
285316

286-
// The transaction span itself should be filtered out
287-
const finishedSpans = getSpanDescendants(this).filter(span => span !== this);
317+
// The transaction span itself as well as any potential standalone spans should be filtered out
318+
const finishedSpans = getSpanDescendants(this).filter(span => span !== this && !isStandaloneSpan(span));
288319

289-
// TODO: filter out standalone spans!
290320
const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan);
291321

292322
const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined;
@@ -337,14 +367,28 @@ function isFullFinishedSpan(input: Partial<SpanJSON>): input is SpanJSON {
337367
return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id;
338368
}
339369

370+
/** `SentrySpan`s can be sent as a standalone span rather than belonging to a transaction */
371+
function isStandaloneSpan(span: Span): boolean {
372+
return span instanceof SentrySpan && span.isStandaloneSpan();
373+
}
374+
340375
function sendSpanEnvelope(envelope: SpanEnvelope): void {
376+
consoleSandbox(() => {
377+
console.log('sendSpanEnvelope', envelope);
378+
});
341379
const client = getClient();
342380
if (!client) {
381+
consoleSandbox(() => {
382+
console.log('no client');
383+
});
343384
return;
344385
}
345386

346387
const transport = client.getTransport();
347388
if (transport) {
389+
consoleSandbox(() => {
390+
console.log('transport send');
391+
});
348392
transport.send(envelope).then(null, reason => {
349393
DEBUG_BUILD && logger.error('Error while sending span:', reason);
350394
});

0 commit comments

Comments
 (0)