Skip to content

Commit 249c1ac

Browse files
committed
feat(browser): Start standalone (segment) spans via start*Span APIs
separate standalone and segment tmp add test helper (no need after rebase) add tests
1 parent 1981cec commit 249c1ac

File tree

13 files changed

+335
-14
lines changed

13 files changed

+335
-14
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://[email protected]/1337',
7+
tracesSampleRate: 1.0,
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.startSpan({ name: 'standalone_segment_span', experimental: { standalone: true, segment: false } }, () => {});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { expect } from '@playwright/test';
2+
import type { SpanEnvelope } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../../../utils/helpers';
10+
11+
sentryTest('sends a span envelope with is_segment: false', async ({ getLocalTestPath, page }) => {
12+
if (shouldSkipTracingTest()) {
13+
sentryTest.skip();
14+
}
15+
16+
const url = await getLocalTestPath({ testDir: __dirname });
17+
const spanEnvelope = await getFirstSentryEnvelopeRequest<SpanEnvelope>(page, url, properFullEnvelopeRequestParser);
18+
19+
const headers = spanEnvelope[0];
20+
const item = spanEnvelope[1][0];
21+
22+
const itemHeader = item[0];
23+
const spanJson = item[1];
24+
25+
expect(headers).toMatchObject({
26+
sent_at: expect.any(String),
27+
});
28+
29+
expect(itemHeader).toEqual({
30+
type: 'span',
31+
});
32+
33+
expect(spanJson).toEqual({
34+
data: {
35+
'sentry.origin': 'manual',
36+
'sentry.sample_rate': 1,
37+
'sentry.source': 'custom',
38+
},
39+
description: 'standalone_segment_span',
40+
origin: 'manual',
41+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
42+
start_timestamp: expect.any(Number),
43+
timestamp: expect.any(Number),
44+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
45+
is_segment: false,
46+
});
47+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.startSpan({ name: 'standalone_segment_span', experimental: { standalone: true, segment: true } }, () => {});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect } from '@playwright/test';
2+
import type { SpanEnvelope } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../../../utils/helpers';
10+
11+
sentryTest('sends a segment span envelope', async ({ getLocalTestPath, page }) => {
12+
if (shouldSkipTracingTest()) {
13+
sentryTest.skip();
14+
}
15+
16+
const url = await getLocalTestPath({ testDir: __dirname });
17+
const spanEnvelope = await getFirstSentryEnvelopeRequest<SpanEnvelope>(page, url, properFullEnvelopeRequestParser);
18+
19+
const headers = spanEnvelope[0];
20+
const item = spanEnvelope[1][0];
21+
22+
const itemHeader = item[0];
23+
const spanJson = item[1];
24+
25+
expect(headers).toMatchObject({
26+
sent_at: expect.any(String),
27+
});
28+
29+
expect(itemHeader).toEqual({
30+
type: 'span',
31+
});
32+
33+
expect(spanJson).toEqual({
34+
data: {
35+
'sentry.origin': 'manual',
36+
'sentry.sample_rate': 1,
37+
'sentry.source': 'custom',
38+
},
39+
description: 'standalone_segment_span',
40+
origin: 'manual',
41+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
42+
start_timestamp: expect.any(Number),
43+
timestamp: expect.any(Number),
44+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
45+
is_segment: true,
46+
segment_id: spanJson.span_id,
47+
});
48+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.startSpan({ name: 'standalone_segment_span', experimental: { standalone: true } }, () => {});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect } from '@playwright/test';
2+
import type { SpanEnvelope } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../../../utils/helpers';
10+
11+
sentryTest('sends a segment-less span envelope', async ({ getLocalTestPath, page }) => {
12+
if (shouldSkipTracingTest()) {
13+
sentryTest.skip();
14+
}
15+
16+
const url = await getLocalTestPath({ testDir: __dirname });
17+
const spanEnvelope = await getFirstSentryEnvelopeRequest<SpanEnvelope>(page, url, properFullEnvelopeRequestParser);
18+
19+
const headers = spanEnvelope[0];
20+
const item = spanEnvelope[1][0];
21+
22+
const itemHeader = item[0];
23+
const spanJson = item[1];
24+
25+
expect(headers).toMatchObject({
26+
sent_at: expect.any(String),
27+
});
28+
29+
expect(itemHeader).toEqual({
30+
type: 'span',
31+
});
32+
33+
expect(spanJson).toEqual({
34+
data: {
35+
'sentry.origin': 'manual',
36+
'sentry.sample_rate': 1,
37+
'sentry.source': 'custom',
38+
},
39+
description: 'standalone_segment_span',
40+
origin: 'manual',
41+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
42+
start_timestamp: expect.any(Number),
43+
timestamp: expect.any(Number),
44+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
45+
});
46+
});

dev-packages/browser-integration-tests/utils/helpers.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Page, Request } from '@playwright/test';
2-
import type { EnvelopeItem, EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types';
2+
import type { Envelope, EnvelopeItem, EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types';
33
import { parseEnvelope } from '@sentry/utils';
44

55
export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
@@ -43,6 +43,13 @@ export const properEnvelopeRequestParser = <T = Event>(request: Request | null,
4343
return properEnvelopeParser(request)[0][envelopeIndex] as T;
4444
};
4545

46+
export const properFullEnvelopeRequestParser = <T extends Envelope>(request: Request | null): T => {
47+
// https://develop.sentry.dev/sdk/envelopes/
48+
const envelope = request?.postData() || '';
49+
50+
return parseEnvelope(envelope) as T;
51+
};
52+
4653
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
4754
// https://develop.sentry.dev/sdk/envelopes/
4855
const envelope = request?.postData() || '';

packages/core/src/tracing/sentrySpan.ts

+41-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
SpanAttributeValue,
55
SpanAttributes,
66
SpanContextData,
7+
SpanEnvelope,
78
SpanJSON,
89
SpanOrigin,
910
SpanStatus,
@@ -16,6 +17,7 @@ import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/ut
1617
import { getClient, getCurrentScope } from '../currentScopes';
1718
import { DEBUG_BUILD } from '../debug-build';
1819

20+
import { createSpanEnvelope } from '../envelope';
1921
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
2022
import {
2123
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
@@ -58,6 +60,12 @@ export class SentrySpan implements Span {
5860
/** The timed events added to this span. */
5961
protected _events: TimedEvent[];
6062

63+
/** if true, treat span as a standalone span (not part of a transaction) */
64+
private _isStandaloneSpan?: boolean;
65+
66+
/** send standalone span as segment span */
67+
private _isSegmentSpan?: boolean;
68+
6169
/**
6270
* You should never call the constructor manually, always use `Sentry.startSpan()`
6371
* or other span methods.
@@ -96,6 +104,9 @@ export class SentrySpan implements Span {
96104
if (this._endTime) {
97105
this._onSpanEnded();
98106
}
107+
108+
this._isStandaloneSpan = spanContext.isStandalone;
109+
this._isSegmentSpan = spanContext.isStandalone && spanContext.isSegment;
99110
}
100111

101112
/** @inheritdoc */
@@ -188,6 +199,8 @@ export class SentrySpan implements Span {
188199
profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined,
189200
exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined,
190201
measurements: timedEventsToMeasurements(this._events),
202+
is_segment: !this._isStandaloneSpan ? undefined : this._isSegmentSpan,
203+
segment_id: this._isStandaloneSpan && this._isSegmentSpan ? this._spanId : undefined,
191204
});
192205
}
193206

@@ -227,13 +240,20 @@ export class SentrySpan implements Span {
227240
client.emit('spanEnd', this);
228241
}
229242

230-
// If this is a root span, send it when it is endedf
231-
if (this === getRootSpan(this)) {
232-
const transactionEvent = this._convertSpanToTransaction();
233-
if (transactionEvent) {
234-
const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope();
235-
scope.captureEvent(transactionEvent);
236-
}
243+
// If this is not a root span, we're done, otherwise, we send it when it is ended
244+
if (this !== getRootSpan(this)) {
245+
return;
246+
}
247+
248+
if (this._isStandaloneSpan) {
249+
sendSpanEnvelope(createSpanEnvelope([this]));
250+
return;
251+
}
252+
253+
const transactionEvent = this._convertSpanToTransaction();
254+
if (transactionEvent) {
255+
const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope();
256+
scope.captureEvent(transactionEvent);
237257
}
238258
}
239259

@@ -318,3 +338,17 @@ function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): val
318338
function isFullFinishedSpan(input: Partial<SpanJSON>): input is SpanJSON {
319339
return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id;
320340
}
341+
342+
function sendSpanEnvelope(envelope: SpanEnvelope): void {
343+
const client = getClient();
344+
if (!client) {
345+
return;
346+
}
347+
348+
const transport = client.getTransport();
349+
if (transport) {
350+
transport.send(envelope).then(null, reason => {
351+
DEBUG_BUILD && logger.error('Error while sending span:', reason);
352+
});
353+
}
354+
}

packages/core/src/tracing/trace.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span) =
4545
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
4646
const activeSpan = shouldSkipSpan
4747
? new SentryNonRecordingSpan()
48-
: createChildSpanOrTransaction({
48+
: createChildOrRootSpan({
4949
parentSpan,
5050
spanContext,
5151
forceTransaction: context.forceTransaction,
@@ -92,7 +92,7 @@ export function startSpanManual<T>(context: StartSpanOptions, callback: (span: S
9292
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
9393
const activeSpan = shouldSkipSpan
9494
? new SentryNonRecordingSpan()
95-
: createChildSpanOrTransaction({
95+
: createChildOrRootSpan({
9696
parentSpan,
9797
spanContext,
9898
forceTransaction: context.forceTransaction,
@@ -144,7 +144,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span {
144144
return new SentryNonRecordingSpan();
145145
}
146146

147-
return createChildSpanOrTransaction({
147+
return createChildOrRootSpan({
148148
parentSpan,
149149
spanContext,
150150
forceTransaction: context.forceTransaction,
@@ -212,7 +212,7 @@ export function suppressTracing<T>(callback: () => T): T {
212212
});
213213
}
214214

215-
function createChildSpanOrTransaction({
215+
function createChildOrRootSpan({
216216
parentSpan,
217217
spanContext,
218218
forceTransaction,
@@ -291,14 +291,21 @@ function createChildSpanOrTransaction({
291291
* Eventually the StartSpanOptions will be more aligned with OpenTelemetry.
292292
*/
293293
function normalizeContext(context: StartSpanOptions): SentrySpanArguments {
294+
const exp = context.experimental || {};
295+
const initialCtx: SentrySpanArguments = {
296+
isStandalone: exp.standalone,
297+
isSegment: exp.standalone && exp.segment,
298+
...context,
299+
};
300+
294301
if (context.startTime) {
295-
const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...context };
302+
const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...initialCtx };
296303
ctx.startTimestamp = spanTimeInputToSeconds(context.startTime);
297304
delete ctx.startTime;
298305
return ctx;
299306
}
300307

301-
return context;
308+
return initialCtx;
302309
}
303310

304311
function getAcs(): AsyncContextStrategy {

0 commit comments

Comments
 (0)