Skip to content

Commit 11e4e4d

Browse files
committed
feat(core): Add span.spanContext()
1 parent 3acb7e4 commit 11e4e4d

File tree

23 files changed

+216
-42
lines changed

23 files changed

+216
-42
lines changed

MIGRATION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ In v8, the Span class is heavily reworked. The following properties & methods ar
1717
* `span.setName(newName)`: Use `span.updateName(newName)` instead.
1818
* `span.toTraceparent()`: use `spanToTraceHeader(span)` util instead.
1919
* `span.getTraceContext()`: Use `spanToTraceContext(span)` utility function instead.
20+
* `span.spanId`: Use `span.spanContext().spanId` instead.
21+
* `span.traceId`: Use `span.spanContext().traceId` instead.
22+
* `span.sampled`: Use `spanIsSampled(span)` instead.
2023

2124
## Deprecate `pushScope` & `popScope` in favor of `withScope`
2225

dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ sentryTest('should report finished spans as children of the root transaction', a
3939

4040
const span_5 = transaction.spans?.[2];
4141
expect(span_5?.op).toBe('span_5');
42+
// eslint-disable-next-line deprecation/deprecation
4243
expect(span_5?.parentSpanId).toEqual(span_3?.spanId);
4344
});

dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ app.use(cors());
2828
app.get('/test/express', (_req, res) => {
2929
const transaction = Sentry.getCurrentHub().getScope().getTransaction();
3030
if (transaction) {
31+
// eslint-disable-next-line deprecation/deprecation
3132
transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd';
3233
}
3334
const headers = http.get('http://somewhere.not.sentry/').getHeaders();

dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ app.use(cors());
3131
app.get('/test/express', (_req, res) => {
3232
const transaction = Sentry.getCurrentHub().getScope().getTransaction();
3333
if (transaction) {
34+
// eslint-disable-next-line deprecation/deprecation
3435
transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd';
3536
transaction.setMetadata({ source: 'route' });
3637
}

packages/astro/test/server/meta.test.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ import { vi } from 'vitest';
44
import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta';
55

66
const mockedSpan = {
7-
sampled: true,
8-
traceId: '12345678901234567890123456789012',
9-
spanId: '1234567890123456',
7+
spanContext: () => {
8+
return {
9+
traceId: '12345678901234567890123456789012',
10+
spanId: '1234567890123456',
11+
traceFlags: TraceFlagSampled,
12+
};
13+
},
1014
transaction: {
1115
getDynamicSamplingContext: () => ({
1216
environment: 'production',
1317
}),
1418
},
1519
} as any;
1620

21+
// eslint-disable-next-line no-bitwise
22+
const TraceFlagSampled = 0x1 << 0;
23+
1724
const mockedClient = {} as any;
1825

1926
const mockedScope = {
@@ -70,9 +77,13 @@ describe('getTracingMetaTags', () => {
7077
const tags = getTracingMetaTags(
7178
// @ts-expect-error - only passing a partial span object
7279
{
73-
sampled: true,
74-
traceId: '12345678901234567890123456789012',
75-
spanId: '1234567890123456',
80+
spanContext: () => {
81+
return {
82+
traceId: '12345678901234567890123456789012',
83+
spanId: '1234567890123456',
84+
traceFlags: TraceFlagSampled,
85+
};
86+
},
7687
transaction: undefined,
7788
},
7889
mockedScope,
@@ -84,7 +95,7 @@ describe('getTracingMetaTags', () => {
8495
});
8596
});
8697

87-
it('returns only the `sentry-trace` tag if no DSC is available', () => {
98+
it('returns only the `sentry-trace` tag if no DSC is available without a client', () => {
8899
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
89100
trace_id: '',
90101
public_key: undefined,
@@ -93,9 +104,13 @@ describe('getTracingMetaTags', () => {
93104
const tags = getTracingMetaTags(
94105
// @ts-expect-error - only passing a partial span object
95106
{
96-
sampled: true,
97-
traceId: '12345678901234567890123456789012',
98-
spanId: '1234567890123456',
107+
spanContext: () => {
108+
return {
109+
traceId: '12345678901234567890123456789012',
110+
spanId: '1234567890123456',
111+
traceFlags: TraceFlagSampled,
112+
};
113+
},
99114
transaction: undefined,
100115
},
101116
mockedScope,

packages/browser/src/profiling/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable max-lines */
22

3-
import { DEFAULT_ENVIRONMENT, getClient } from '@sentry/core';
3+
import { DEFAULT_ENVIRONMENT, getClient, spanIsSampled } from '@sentry/core';
44
import type { DebugImage, Envelope, Event, EventEnvelope, StackFrame, StackParser, Transaction } from '@sentry/types';
55
import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling';
66
import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils';
@@ -515,7 +515,7 @@ export function shouldProfileTransaction(transaction: Transaction): boolean {
515515
return false;
516516
}
517517

518-
if (!transaction.sampled) {
518+
if (!spanIsSampled(transaction)) {
519519
if (DEBUG_BUILD) {
520520
logger.log('[Profiling] Discarding profile because transaction was not sampled.');
521521
}

packages/browser/test/unit/profiling/hubextensions.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { JSDOM } from 'jsdom';
1010

1111
import { onProfilingStartRouteTransaction } from '../../../src';
1212

13+
// eslint-disable-next-line no-bitwise
14+
const TraceFlagSampled = 0x1 << 0;
15+
1316
// @ts-expect-error store a reference so we can reset it later
1417
const globalDocument = global.document;
1518
// @ts-expect-error store a reference so we can reset it later
@@ -67,9 +70,16 @@ describe('BrowserProfilingIntegration', () => {
6770
// @ts-expect-error force api to be undefined
6871
global.window.Profiler = undefined;
6972
// set sampled to true so that profiling does not early return
70-
const mockTransaction = { sampled: true } as Transaction;
73+
const mockTransaction = {
74+
spanContext: () => ({
75+
traceId: '12345678901234567890123456789012',
76+
spanId: '1234567890123456',
77+
traceFlags: TraceFlagSampled,
78+
}),
79+
} as Transaction;
7180
expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow();
7281
});
82+
7383
it('does not throw if constructor throws', () => {
7484
const spy = jest.fn();
7585

@@ -81,7 +91,13 @@ describe('BrowserProfilingIntegration', () => {
8191
}
8292

8393
// set sampled to true so that profiling does not early return
84-
const mockTransaction = { sampled: true } as Transaction;
94+
const mockTransaction = {
95+
spanContext: () => ({
96+
traceId: '12345678901234567890123456789012',
97+
spanId: '1234567890123456',
98+
traceFlags: TraceFlagSampled,
99+
}),
100+
} as Transaction;
85101

86102
// @ts-expect-error override with our own constructor
87103
global.window.Profiler = Profiler;

packages/bun/test/integrations/bunserver.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeAll, beforeEach, describe, expect, test } from 'bun:test';
2-
import { Hub, makeMain } from '@sentry/core';
2+
import { Hub, makeMain, spanIsSampled } from '@sentry/core';
33

44
import { BunClient } from '../../src/client';
55
import { instrumentBunServe } from '../../src/integrations/bunserver';
@@ -78,9 +78,9 @@ describe('Bun Serve Integration', () => {
7878
const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production';
7979

8080
client.on('finishTransaction', transaction => {
81-
expect(transaction.traceId).toBe(TRACE_ID);
81+
expect(transaction.spanContext().traceId).toBe(TRACE_ID);
8282
expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID);
83-
expect(transaction.sampled).toBe(true);
83+
expect(spanIsSampled(transaction)).toBe(true);
8484

8585
expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' });
8686
});

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export { prepareEvent } from './utils/prepareEvent';
6969
export { createCheckInEnvelope } from './checkin';
7070
export { hasTracingEnabled } from './utils/hasTracingEnabled';
7171
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
72-
export { spanToTraceHeader } from './utils/spanUtils';
72+
export { spanToTraceHeader, spanIsSampled } from './utils/spanUtils';
7373
export { DEFAULT_ENVIRONMENT } from './constants';
7474
export { ModuleMetadata } from './integrations/metadata';
7575
export { RequestData } from './integrations/requestdata';

packages/core/src/tracing/idletransaction.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ export class IdleTransactionSpanRecorder extends SpanRecorder {
4545
public add(span: Span): void {
4646
// We should make sure we do not push and pop activities for
4747
// the transaction that this span recorder belongs to.
48-
if (span.spanId !== this.transactionSpanId) {
48+
if (span.spanContext().spanId !== this.transactionSpanId) {
4949
// We patch span.end() to pop an activity after setting an endTimestamp.
5050
// eslint-disable-next-line @typescript-eslint/unbound-method
5151
const originalEnd = span.end;
5252
span.end = (...rest: unknown[]) => {
53-
this._popActivity(span.spanId);
53+
this._popActivity(span.spanContext().spanId);
5454
return originalEnd.apply(span, rest);
5555
};
5656

5757
// We should only push new activities if the span does not have an end timestamp.
5858
if (span.endTimestamp === undefined) {
59-
this._pushActivity(span.spanId);
59+
this._pushActivity(span.spanContext().spanId);
6060
}
6161
}
6262

@@ -123,7 +123,7 @@ export class IdleTransaction extends Transaction {
123123
if (_onScope) {
124124
// We set the transaction here on the scope so error events pick up the trace
125125
// context and attach it to the error.
126-
DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`);
126+
DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`);
127127
_idleHub.getScope().setSpan(this);
128128
}
129129

@@ -158,7 +158,7 @@ export class IdleTransaction extends Transaction {
158158

159159
this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => {
160160
// If we are dealing with the transaction itself, we just return it
161-
if (span.spanId === this.spanId) {
161+
if (span.spanContext().spanId === this.spanContext().spanId) {
162162
return true;
163163
}
164164

@@ -233,7 +233,7 @@ export class IdleTransaction extends Transaction {
233233
this._popActivity(id);
234234
};
235235

236-
this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanId, maxlen);
236+
this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanContext().spanId, maxlen);
237237

238238
// Start heartbeat so that transactions do not run forever.
239239
DEBUG_BUILD && logger.log('Starting heartbeat');

packages/core/src/tracing/span.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
SpanAttributeValue,
77
SpanAttributes,
88
SpanContext,
9+
SpanContextData,
910
SpanOrigin,
1011
TraceContext,
1112
Transaction,
@@ -16,6 +17,10 @@ import { DEBUG_BUILD } from '../debug-build';
1617
import { spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils';
1718
import { ensureTimestampInSeconds } from './utils';
1819

20+
export const TraceFlagNone = 0x0;
21+
// eslint-disable-next-line no-bitwise
22+
export const TraceFlagSampled = 0x1 << 0;
23+
1924
/**
2025
* Keeps track of finished spans for a given transaction
2126
* @internal
@@ -184,6 +189,16 @@ export class Span implements SpanInterface {
184189
this.updateName(name);
185190
}
186191

192+
/** @inheritdoc */
193+
public spanContext(): SpanContextData {
194+
const { spanId, traceId, sampled } = this;
195+
return {
196+
spanId,
197+
traceId,
198+
traceFlags: sampled ? TraceFlagSampled : TraceFlagNone,
199+
};
200+
}
201+
187202
/**
188203
* @inheritDoc
189204
*/
@@ -207,7 +222,7 @@ export class Span implements SpanInterface {
207222
if (DEBUG_BUILD && childSpan.transaction) {
208223
const opStr = (spanContext && spanContext.op) || '< unknown op >';
209224
const nameStr = childSpan.transaction.name || '< unknown name >';
210-
const idStr = childSpan.transaction.spanId;
225+
const idStr = childSpan.transaction.spanContext().spanId;
211226

212227
const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`;
213228
childSpan.transaction.metadata.spanMetadata[childSpan.spanId] = { logMessage };
@@ -305,7 +320,7 @@ export class Span implements SpanInterface {
305320
DEBUG_BUILD &&
306321
// Don't call this for transactions
307322
this.transaction &&
308-
this.transaction.spanId !== this.spanId
323+
this.transaction.spanContext().spanId !== this.spanId
309324
) {
310325
const { logMessage } = this.transaction.metadata.spanMetadata[this.spanId];
311326
if (logMessage) {

packages/core/src/utils/spanUtils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Span, TraceContext } from '@sentry/types';
22
import { dropUndefinedKeys, generateSentryTraceHeader } from '@sentry/utils';
3+
import { TraceFlagSampled } from '../tracing/span';
34

45
/**
56
* Convert a span to a trace context, which can be sent as the `trace` context in an event.
@@ -24,5 +25,14 @@ export function spanToTraceContext(span: Span): TraceContext {
2425
* Convert a Span to a Sentry trace header.
2526
*/
2627
export function spanToTraceHeader(span: Span): string {
27-
return generateSentryTraceHeader(span.traceId, span.spanId, span.sampled);
28+
const { traceId, spanId } = span.spanContext();
29+
const sampled = spanIsSampled(span);
30+
return generateSentryTraceHeader(traceId, spanId, sampled);
31+
}
32+
33+
/** Returns true if a span is sampled. */
34+
export function spanIsSampled(span: Span): boolean {
35+
const { traceFlags } = span.spanContext();
36+
// eslint-disable-next-line no-bitwise
37+
return Boolean(traceFlags & TraceFlagSampled);
2838
}

packages/core/test/lib/tracing/span.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Span } from '../../../src';
2+
import { TraceFlagNone, TraceFlagSampled } from '../../../src/tracing/span';
23

34
describe('span', () => {
45
it('works with name', () => {
@@ -174,6 +175,35 @@ describe('span', () => {
174175
});
175176
});
176177

178+
describe('spanContext', () => {
179+
it('works with default span', () => {
180+
const span = new Span();
181+
expect(span.spanContext()).toEqual({
182+
spanId: span.spanId,
183+
traceId: span.traceId,
184+
traceFlags: TraceFlagNone,
185+
});
186+
});
187+
188+
it('works sampled span', () => {
189+
const span = new Span({ sampled: true });
190+
expect(span.spanContext()).toEqual({
191+
spanId: span.spanId,
192+
traceId: span.traceId,
193+
traceFlags: TraceFlagSampled,
194+
});
195+
});
196+
197+
it('works unsampled span', () => {
198+
const span = new Span({ sampled: false });
199+
expect(span.spanContext()).toEqual({
200+
spanId: span.spanId,
201+
traceId: span.traceId,
202+
traceFlags: TraceFlagNone,
203+
});
204+
});
205+
});
206+
177207
// Ensure that attributes & data are merged together
178208
describe('_getData', () => {
179209
it('works without data & attributes', () => {

packages/core/test/lib/utils/spanUtils.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TRACEPARENT_REGEXP } from '@sentry/utils';
22
import { Span, spanToTraceHeader } from '../../../src';
3+
import { spanIsSampled } from '../../../src/utils/spanUtils';
34

45
describe('spanToTraceHeader', () => {
56
test('simple', () => {
@@ -11,3 +12,15 @@ describe('spanToTraceHeader', () => {
1112
expect(spanToTraceHeader(span)).toMatch(TRACEPARENT_REGEXP);
1213
});
1314
});
15+
16+
describe('spanIsSampled', () => {
17+
test('sampled', () => {
18+
const span = new Span({ sampled: true });
19+
expect(spanIsSampled(span)).toBe(true);
20+
});
21+
22+
test('not sampled', () => {
23+
const span = new Span({ sampled: false });
24+
expect(spanIsSampled(span)).toBe(false);
25+
});
26+
});

packages/node/test/handlers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ describe('tracingHandler', () => {
448448
expect(finishTransaction).toHaveBeenCalled();
449449
expect(span.endTimestamp).toBeLessThanOrEqual(transaction.endTimestamp!);
450450
expect(sentEvent.spans?.length).toEqual(1);
451-
expect(sentEvent.spans?.[0].spanId).toEqual(span.spanId);
451+
expect(sentEvent.spans?.[0].spanContext().spanId).toEqual(span.spanId);
452452
done();
453453
});
454454
});

packages/opentelemetry-node/src/utils/spanMap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function getSentrySpan(spanId: string): SentrySpan | undefined {
3131
export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void {
3232
let ref: SpanRefType = SPAN_REF_ROOT;
3333

34-
const rootSpanId = sentrySpan.transaction?.spanId;
34+
const rootSpanId = sentrySpan.transaction?.spanContext().spanId;
3535

3636
if (rootSpanId && rootSpanId !== spanId) {
3737
const root = SPAN_MAP.get(rootSpanId);

0 commit comments

Comments
 (0)