Skip to content

ref(v8): Remove Transaction concept #11422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export type {
StackFrame,
Stacktrace,
Thread,
Transaction,
User,
Session,
} from '@sentry/types';
Expand Down
1 change: 0 additions & 1 deletion packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type {
StackFrame,
Stacktrace,
Thread,
Transaction,
User,
} from '@sentry/types';
export type { AddRequestDataToEventOptions } from '@sentry/utils';
Expand Down
13 changes: 4 additions & 9 deletions packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
Transaction,
captureException,
continueTrace,
defineIntegration,
getCurrentScope,
setHttpStatus,
startSpan,
withIsolationScope,
Expand Down Expand Up @@ -100,13 +98,10 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
>);
if (response && response.status) {
setHttpStatus(span, response.status);
if (span instanceof Transaction) {
const scope = getCurrentScope();
scope.setContext('response', {
headers: response.headers.toJSON(),
status_code: response.status,
});
}
isolationScope.setContext('response', {
headers: response.headers.toJSON(),
status_code: response.status,
});
}
return response;
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/integrations/requestdata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Client, IntegrationFn, Transaction } from '@sentry/types';
import type { Client, IntegrationFn, Span } from '@sentry/types';
import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils';
import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils';
import { defineIntegration } from '../integration';
Expand Down Expand Up @@ -92,7 +92,7 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) =

// In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction`
// value with a high-quality one
const reqWithTransaction = req as { _sentryTransaction?: Transaction };
const reqWithTransaction = req as { _sentryTransaction?: Span };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing seems to set _sentryTransaction 🤔

Can we remove all of this functionality here?

const transaction = reqWithTransaction._sentryTransaction;
if (transaction) {
const name = spanToJSON(transaction).description || '';
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export { addTracingExtensions } from './hubextensions';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
export { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
export { Transaction } from './transaction';
export {
setHttpStatus,
getSpanStatusFromHttpCode,
Expand Down
115 changes: 112 additions & 3 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,33 @@ import type {
SpanStatus,
SpanTimeInput,
TimedEvent,
TransactionEvent,
TransactionSource,
} from '@sentry/types';
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
import { getClient } from '../currentScopes';
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';

import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, getStatusMessage, spanTimeInputToSeconds } from '../utils/spanUtils';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '../semanticAttributes';
import {
TRACE_FLAG_NONE,
TRACE_FLAG_SAMPLED,
getRootSpan,
getSpanDescendants,
getStatusMessage,
spanTimeInputToSeconds,
spanToJSON,
spanToTraceContext,
} from '../utils/spanUtils';
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { getCapturedScopesOnSpan } from './utils';

/**
* Span contains all data about a span
Expand Down Expand Up @@ -71,6 +89,11 @@ export class SentrySpan implements Span {
}

this._events = [];

// If the span is already ended, ensure we finalize the span immediately
if (this._endTime) {
this._onSpanEnded();
}
}

/** @inheritdoc */
Expand Down Expand Up @@ -198,9 +221,95 @@ export class SentrySpan implements Span {
if (client) {
client.emit('spanEnd', this);
}

// If this is a root span, send it when it is endedf
if (this === getRootSpan(this)) {
const transactionEvent = this._convertSpanToTransaction();
if (transactionEvent) {
const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope();
scope.captureEvent(transactionEvent);
}
}
}

/**
* Finish the transaction & prepare the event to send to Sentry.
*/
private _convertSpanToTransaction(): TransactionEvent | undefined {
// We can only convert finished spans
if (!isFullFinishedSpan(spanToJSON(this))) {
return undefined;
}

if (!this._name) {
DEBUG_BUILD && logger.warn('Transaction has no name, falling back to `<unlabeled transaction>`.');
this._name = '<unlabeled transaction>';
}

const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this);
const scope = capturedSpanScope || getCurrentScope();
const client = scope.getClient() || getClient();

if (this._sampled !== true) {
// At this point if `sampled !== true` we want to discard the transaction.
DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.');

if (client) {
client.recordDroppedEvent('sample_rate', 'transaction');
}

return undefined;
}

// The transaction span itself should be filtered out
const finishedSpans = getSpanDescendants(this).filter(span => span !== this);

const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan);

const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined;

const transaction: TransactionEvent = {
contexts: {
trace: spanToTraceContext(this),
},
spans,
start_timestamp: this._startTime,
timestamp: this._endTime,
transaction: this._name,
type: 'transaction',
sdkProcessingMetadata: {
capturedSpanScope,
capturedSpanIsolationScope,
...dropUndefinedKeys({
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
}),
},
_metrics_summary: getMetricSummaryJsonForSpan(this),
...(source && {
transaction_info: {
source,
},
}),
};

const measurements = timedEventsToMeasurements(this._events);
const hasMeasurements = Object.keys(measurements).length;

if (hasMeasurements) {
DEBUG_BUILD &&
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2));
transaction.measurements = measurements;
}

return transaction;
}
}

function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): value is SpanTimeInput {
return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value);
}

// We want to filter out any incomplete SpanJSON objects
function isFullFinishedSpan(input: Partial<SpanJSON>): input is SpanJSON {
return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id;
}
87 changes: 48 additions & 39 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import type {
ClientOptions,
Scope,
SentrySpanArguments,
Span,
SpanTimeInput,
StartSpanOptions,
TransactionArguments,
} from '@sentry/types';
import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types';

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

import { getAsyncContextStrategy, getCurrentHub } from '../hub';
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes';
import { getAsyncContextStrategy } from '../hub';
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope';
Expand All @@ -31,7 +23,6 @@ import { sampleSpan } from './sampling';
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
import { SentrySpan } from './sentrySpan';
import { SPAN_STATUS_ERROR } from './spanstatus';
import { Transaction } from './transaction';
import { setCapturedScopesOnSpan } from './utils';

/**
Expand Down Expand Up @@ -220,7 +211,7 @@ function createChildSpanOrTransaction({
scope,
}: {
parentSpan: SentrySpan | undefined;
spanContext: TransactionArguments;
spanContext: SentrySpanArguments;
forceTransaction?: boolean;
scope: Scope;
}): Span {
Expand All @@ -232,34 +223,43 @@ function createChildSpanOrTransaction({

let span: Span;
if (parentSpan && !forceTransaction) {
span = _startChild(parentSpan, spanContext);
span = _startChildSpan(parentSpan, spanContext);
addChildSpanToSpan(parentSpan, span);
} else if (parentSpan) {
// If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope
const dsc = getDynamicSamplingContextFromSpan(parentSpan);
const { traceId, spanId: parentSpanId } = parentSpan.spanContext();
const sampled = spanIsSampled(parentSpan);
const parentSampled = spanIsSampled(parentSpan);

span = _startTransaction({
traceId,
parentSpanId,
parentSampled: sampled,
...spanContext,
});
span = _startRootSpan(
{
traceId,
parentSpanId,
...spanContext,
},
parentSampled,
);

freezeDscOnSpan(span, dsc);
} else {
const { traceId, dsc, parentSpanId, sampled } = {
const {
traceId,
dsc,
parentSpanId,
sampled: parentSampled,
} = {
...isolationScope.getPropagationContext(),
...scope.getPropagationContext(),
};

span = _startTransaction({
traceId,
parentSpanId,
parentSampled: sampled,
...spanContext,
});
span = _startRootSpan(
{
traceId,
parentSpanId,
...spanContext,
},
parentSampled,
);

if (dsc) {
freezeDscOnSpan(span, dsc);
Expand All @@ -274,15 +274,15 @@ function createChildSpanOrTransaction({
}

/**
* This converts StartSpanOptions to TransactionArguments.
* This converts StartSpanOptions to SentrySpanArguments.
* For the most part (for now) we accept the same options,
* but some of them need to be transformed.
*
* Eventually the StartSpanOptions will be more aligned with OpenTelemetry.
*/
function normalizeContext(context: StartSpanOptions): TransactionArguments {
function normalizeContext(context: StartSpanOptions): SentrySpanArguments {
if (context.startTime) {
const ctx: TransactionArguments & { startTime?: SpanTimeInput } = { ...context };
const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...context };
ctx.startTimestamp = spanTimeInputToSeconds(context.startTime);
delete ctx.startTime;
return ctx;
Expand All @@ -296,20 +296,29 @@ function getAcs(): AsyncContextStrategy {
return getAsyncContextStrategy(carrier);
}

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

const { name, parentSampled, attributes } = transactionContext;
const { name = '', attributes } = spanArguments;
const [sampled, sampleRate] = sampleSpan(options, {
name,
parentSampled,
attributes,
transactionContext,
transactionContext: {
name,
parentSampled,
},
});

// eslint-disable-next-line deprecation/deprecation
const transaction = new Transaction({ ...transactionContext, sampled }, getCurrentHub());
const transaction = new SentrySpan({
...spanArguments,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
...spanArguments.attributes,
},
sampled,
});
if (sampleRate !== undefined) {
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate);
}
Expand All @@ -325,12 +334,12 @@ function _startTransaction(transactionContext: TransactionArguments): Transactio
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
* This inherits the sampling decision from the parent span.
*/
function _startChild(parentSpan: Span, spanContext: SentrySpanArguments): SentrySpan {
function _startChildSpan(parentSpan: Span, spanArguments: SentrySpanArguments): SentrySpan {
const { spanId, traceId } = parentSpan.spanContext();
const sampled = spanIsSampled(parentSpan);

const childSpan = new SentrySpan({
...spanContext,
...spanArguments,
parentSpanId: spanId,
traceId,
sampled,
Expand All @@ -342,7 +351,7 @@ function _startChild(parentSpan: Span, spanContext: SentrySpanArguments): Sentry
if (client) {
client.emit('spanStart', childSpan);
// If it has an endTimestamp, it's already ended
if (spanContext.endTimestamp) {
if (spanArguments.endTimestamp) {
client.emit('spanEnd', childSpan);
}
}
Expand Down
Loading