Skip to content

Commit e27da11

Browse files
committed
ref(v8): Remove Transaction concept
1 parent b1a2f8b commit e27da11

File tree

23 files changed

+224
-350
lines changed

23 files changed

+224
-350
lines changed

packages/browser/src/exports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export type {
1010
StackFrame,
1111
Stacktrace,
1212
Thread,
13-
Transaction,
1413
User,
1514
Session,
1615
} from '@sentry/types';

packages/bun/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export type {
1313
StackFrame,
1414
Stacktrace,
1515
Thread,
16-
Transaction,
1716
User,
1817
} from '@sentry/types';
1918
export type { AddRequestDataToEventOptions } from '@sentry/utils';

packages/bun/src/integrations/bunserver.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {
22
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
33
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
4-
Transaction,
54
captureException,
65
continueTrace,
76
defineIntegration,
8-
getCurrentScope,
97
setHttpStatus,
108
startSpan,
119
withIsolationScope,
@@ -100,13 +98,10 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
10098
>);
10199
if (response && response.status) {
102100
setHttpStatus(span, response.status);
103-
if (span instanceof Transaction) {
104-
const scope = getCurrentScope();
105-
scope.setContext('response', {
106-
headers: response.headers.toJSON(),
107-
status_code: response.status,
108-
});
109-
}
101+
isolationScope.setContext('response', {
102+
headers: response.headers.toJSON(),
103+
status_code: response.status,
104+
});
110105
}
111106
return response;
112107
} catch (e) {

packages/core/src/integrations/requestdata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Client, IntegrationFn, Transaction } from '@sentry/types';
1+
import type { Client, IntegrationFn, Span } from '@sentry/types';
22
import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils';
33
import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils';
44
import { defineIntegration } from '../integration';
@@ -92,7 +92,7 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) =
9292

9393
// In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction`
9494
// value with a high-quality one
95-
const reqWithTransaction = req as { _sentryTransaction?: Transaction };
95+
const reqWithTransaction = req as { _sentryTransaction?: Span };
9696
const transaction = reqWithTransaction._sentryTransaction;
9797
if (transaction) {
9898
const name = spanToJSON(transaction).description || '';

packages/core/src/tracing/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export { addTracingExtensions } from './hubextensions';
22
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
33
export { SentrySpan } from './sentrySpan';
44
export { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
5-
export { Transaction } from './transaction';
65
export {
76
setHttpStatus,
87
getSpanStatusFromHttpCode,

packages/core/src/tracing/sentrySpan.ts

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,33 @@ import type {
99
SpanStatus,
1010
SpanTimeInput,
1111
TimedEvent,
12+
TransactionEvent,
13+
TransactionSource,
1214
} from '@sentry/types';
1315
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
14-
import { getClient } from '../currentScopes';
16+
import { getClient, getCurrentScope } from '../currentScopes';
1517
import { DEBUG_BUILD } from '../debug-build';
1618

1719
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
18-
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
19-
import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, getStatusMessage, spanTimeInputToSeconds } from '../utils/spanUtils';
20+
import {
21+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
22+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
23+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
24+
} from '../semanticAttributes';
25+
import {
26+
TRACE_FLAG_NONE,
27+
TRACE_FLAG_SAMPLED,
28+
getRootSpan,
29+
getSpanDescendants,
30+
getStatusMessage,
31+
spanTimeInputToSeconds,
32+
spanToJSON,
33+
spanToTraceContext,
34+
} from '../utils/spanUtils';
35+
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2036
import { logSpanEnd } from './logSpans';
37+
import { timedEventsToMeasurements } from './measurement';
38+
import { getCapturedScopesOnSpan } from './utils';
2139

2240
/**
2341
* Span contains all data about a span
@@ -71,6 +89,11 @@ export class SentrySpan implements Span {
7189
}
7290

7391
this._events = [];
92+
93+
// If the span is already ended, ensure we finalize the span immediately
94+
if (this._endTime) {
95+
this._onSpanEnded();
96+
}
7497
}
7598

7699
/** @inheritdoc */
@@ -194,13 +217,100 @@ export class SentrySpan implements Span {
194217

195218
/** Emit `spanEnd` when the span is ended. */
196219
private _onSpanEnded(): void {
220+
console.log('SPAN END!', this === getRootSpan(this));
197221
const client = getClient();
198222
if (client) {
199223
client.emit('spanEnd', this);
200224
}
225+
226+
// If this is a root span, send it when it is endedf
227+
if (this === getRootSpan(this)) {
228+
const transactionEvent = this._convertSpanToTransaction();
229+
if (transactionEvent) {
230+
const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope();
231+
scope.captureEvent(transactionEvent);
232+
}
233+
}
234+
}
235+
236+
/**
237+
* Finish the transaction & prepare the event to send to Sentry.
238+
*/
239+
private _convertSpanToTransaction(): TransactionEvent | undefined {
240+
// We can only convert finished spans
241+
if (!isFullFinishedSpan(spanToJSON(this))) {
242+
return undefined;
243+
}
244+
245+
if (!this._name) {
246+
DEBUG_BUILD && logger.warn('Transaction has no name, falling back to `<unlabeled transaction>`.');
247+
this._name = '<unlabeled transaction>';
248+
}
249+
250+
const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this);
251+
const scope = capturedSpanScope || getCurrentScope();
252+
const client = scope.getClient() || getClient();
253+
254+
if (this._sampled !== true) {
255+
// At this point if `sampled !== true` we want to discard the transaction.
256+
DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.');
257+
258+
if (client) {
259+
client.recordDroppedEvent('sample_rate', 'transaction');
260+
}
261+
262+
return undefined;
263+
}
264+
265+
// The transaction span itself should be filtered out
266+
const finishedSpans = getSpanDescendants(this).filter(span => span !== this);
267+
268+
const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan);
269+
270+
const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined;
271+
272+
const transaction: TransactionEvent = {
273+
contexts: {
274+
trace: spanToTraceContext(this),
275+
},
276+
spans,
277+
start_timestamp: this._startTime,
278+
timestamp: this._endTime,
279+
transaction: this._name,
280+
type: 'transaction',
281+
sdkProcessingMetadata: {
282+
capturedSpanScope,
283+
capturedSpanIsolationScope,
284+
...dropUndefinedKeys({
285+
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
286+
}),
287+
},
288+
_metrics_summary: getMetricSummaryJsonForSpan(this),
289+
...(source && {
290+
transaction_info: {
291+
source,
292+
},
293+
}),
294+
};
295+
296+
const measurements = timedEventsToMeasurements(this._events);
297+
const hasMeasurements = Object.keys(measurements).length;
298+
299+
if (hasMeasurements) {
300+
DEBUG_BUILD &&
301+
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2));
302+
transaction.measurements = measurements;
303+
}
304+
305+
return transaction;
201306
}
202307
}
203308

204309
function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): value is SpanTimeInput {
205310
return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value);
206311
}
312+
313+
// We want to filter out any incomplete SpanJSON objects
314+
function isFullFinishedSpan(input: Partial<SpanJSON>): input is SpanJSON {
315+
return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id;
316+
}

packages/core/src/tracing/trace.ts

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import type {
2-
ClientOptions,
3-
Scope,
4-
SentrySpanArguments,
5-
Span,
6-
SpanTimeInput,
7-
StartSpanOptions,
8-
TransactionArguments,
9-
} from '@sentry/types';
1+
import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types';
102

113
import { propagationContextFromHeaders } from '@sentry/utils';
124
import type { AsyncContextStrategy } from '../asyncContext';
135
import { getMainCarrier } from '../asyncContext';
146
import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes';
157

16-
import { getAsyncContextStrategy, getCurrentHub } from '../hub';
17-
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes';
8+
import { getAsyncContextStrategy } from '../hub';
9+
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
1810
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
1911
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
2012
import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope';
@@ -31,7 +23,6 @@ import { sampleSpan } from './sampling';
3123
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
3224
import { SentrySpan } from './sentrySpan';
3325
import { SPAN_STATUS_ERROR } from './spanstatus';
34-
import { Transaction } from './transaction';
3526
import { setCapturedScopesOnSpan } from './utils';
3627

3728
/**
@@ -220,7 +211,7 @@ function createChildSpanOrTransaction({
220211
scope,
221212
}: {
222213
parentSpan: SentrySpan | undefined;
223-
spanContext: TransactionArguments;
214+
spanContext: SentrySpanArguments;
224215
forceTransaction?: boolean;
225216
scope: Scope;
226217
}): Span {
@@ -232,34 +223,43 @@ function createChildSpanOrTransaction({
232223

233224
let span: Span;
234225
if (parentSpan && !forceTransaction) {
235-
span = _startChild(parentSpan, spanContext);
226+
span = _startChildSpan(parentSpan, spanContext);
236227
addChildSpanToSpan(parentSpan, span);
237228
} else if (parentSpan) {
238229
// If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope
239230
const dsc = getDynamicSamplingContextFromSpan(parentSpan);
240231
const { traceId, spanId: parentSpanId } = parentSpan.spanContext();
241-
const sampled = spanIsSampled(parentSpan);
232+
const parentSampled = spanIsSampled(parentSpan);
242233

243-
span = _startTransaction({
244-
traceId,
245-
parentSpanId,
246-
parentSampled: sampled,
247-
...spanContext,
248-
});
234+
span = _startRootSpan(
235+
{
236+
traceId,
237+
parentSpanId,
238+
...spanContext,
239+
},
240+
parentSampled,
241+
);
249242

250243
freezeDscOnSpan(span, dsc);
251244
} else {
252-
const { traceId, dsc, parentSpanId, sampled } = {
245+
const {
246+
traceId,
247+
dsc,
248+
parentSpanId,
249+
sampled: parentSampled,
250+
} = {
253251
...isolationScope.getPropagationContext(),
254252
...scope.getPropagationContext(),
255253
};
256254

257-
span = _startTransaction({
258-
traceId,
259-
parentSpanId,
260-
parentSampled: sampled,
261-
...spanContext,
262-
});
255+
span = _startRootSpan(
256+
{
257+
traceId,
258+
parentSpanId,
259+
...spanContext,
260+
},
261+
parentSampled,
262+
);
263263

264264
if (dsc) {
265265
freezeDscOnSpan(span, dsc);
@@ -274,15 +274,15 @@ function createChildSpanOrTransaction({
274274
}
275275

276276
/**
277-
* This converts StartSpanOptions to TransactionArguments.
277+
* This converts StartSpanOptions to SentrySpanArguments.
278278
* For the most part (for now) we accept the same options,
279279
* but some of them need to be transformed.
280280
*
281281
* Eventually the StartSpanOptions will be more aligned with OpenTelemetry.
282282
*/
283-
function normalizeContext(context: StartSpanOptions): TransactionArguments {
283+
function normalizeContext(context: StartSpanOptions): SentrySpanArguments {
284284
if (context.startTime) {
285-
const ctx: TransactionArguments & { startTime?: SpanTimeInput } = { ...context };
285+
const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...context };
286286
ctx.startTimestamp = spanTimeInputToSeconds(context.startTime);
287287
delete ctx.startTime;
288288
return ctx;
@@ -296,20 +296,29 @@ function getAcs(): AsyncContextStrategy {
296296
return getAsyncContextStrategy(carrier);
297297
}
298298

299-
function _startTransaction(transactionContext: TransactionArguments): Transaction {
299+
function _startRootSpan(spanArguments: SentrySpanArguments, parentSampled?: boolean): SentrySpan {
300300
const client = getClient();
301301
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
302302

303-
const { name, parentSampled, attributes } = transactionContext;
303+
const { name = '', attributes } = spanArguments;
304304
const [sampled, sampleRate] = sampleSpan(options, {
305305
name,
306306
parentSampled,
307307
attributes,
308-
transactionContext,
308+
transactionContext: {
309+
name,
310+
parentSampled,
311+
},
309312
});
310313

311-
// eslint-disable-next-line deprecation/deprecation
312-
const transaction = new Transaction({ ...transactionContext, sampled }, getCurrentHub());
314+
const transaction = new SentrySpan({
315+
...spanArguments,
316+
attributes: {
317+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
318+
...spanArguments.attributes,
319+
},
320+
sampled,
321+
});
313322
if (sampleRate !== undefined) {
314323
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate);
315324
}
@@ -325,12 +334,12 @@ function _startTransaction(transactionContext: TransactionArguments): Transactio
325334
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
326335
* This inherits the sampling decision from the parent span.
327336
*/
328-
function _startChild(parentSpan: Span, spanContext: SentrySpanArguments): SentrySpan {
337+
function _startChildSpan(parentSpan: Span, spanArguments: SentrySpanArguments): SentrySpan {
329338
const { spanId, traceId } = parentSpan.spanContext();
330339
const sampled = spanIsSampled(parentSpan);
331340

332341
const childSpan = new SentrySpan({
333-
...spanContext,
342+
...spanArguments,
334343
parentSpanId: spanId,
335344
traceId,
336345
sampled,
@@ -342,7 +351,7 @@ function _startChild(parentSpan: Span, spanContext: SentrySpanArguments): Sentry
342351
if (client) {
343352
client.emit('spanStart', childSpan);
344353
// If it has an endTimestamp, it's already ended
345-
if (spanContext.endTimestamp) {
354+
if (spanArguments.endTimestamp) {
346355
client.emit('spanEnd', childSpan);
347356
}
348357
}

0 commit comments

Comments
 (0)