Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '31 KB',
limit: '32 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
Expand Down
27 changes: 25 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ import type { RequestEventData } from './types-hoist/request';
import type { SdkMetadata } from './types-hoist/sdkmetadata';
import type { Session, SessionAggregates } from './types-hoist/session';
import type { SeverityLevel } from './types-hoist/severity';
import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span';
import type { Span, SpanAttributes, SpanContextData, SpanJSON, StreamedSpanJSON } from './types-hoist/span';
import type { StartSpanOptions } from './types-hoist/startSpanOptions';
import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport';
import { isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan';
import { createClientReportEnvelope } from './utils/clientreport';
import { debug } from './utils/debug-logger';
import { dsnToString, makeDsn } from './utils/dsn';
Expand Down Expand Up @@ -612,6 +613,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'spanEnd', callback: (span: Span) => void): () => void;

/**
* Register a callback for when a span JSON is processed, to add some data to the span JSON.
*/
public on(hook: 'processSpan', callback: (streamedSpanJSON: StreamedSpanJSON) => void): () => void;

/**
* Register a callback for when a segment span JSON is processed, to add some data to the segment span JSON.
*/
public on(hook: 'processSegmentSpan', callback: (streamedSpanJSON: StreamedSpanJSON) => void): () => void;

/**
* Register a callback for when an idle span is allowed to auto-finish.
* @returns {() => void} A function that, when executed, removes the registered callback.
Expand Down Expand Up @@ -884,6 +895,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
/** Fire a hook whenever a span ends. */
public emit(hook: 'spanEnd', span: Span): void;

/**
* Register a callback for when a span JSON is processed, to add some data to the span JSON.
*/
public emit(hook: 'processSpan', streamedSpanJSON: StreamedSpanJSON): void;

/**
* Register a callback for when a segment span JSON is processed, to add some data to the segment span JSON.
*/
public emit(hook: 'processSegmentSpan', streamedSpanJSON: StreamedSpanJSON): void;

/**
* Fire a hook indicating that an idle span is allowed to auto finish.
*/
Expand Down Expand Up @@ -1502,7 +1523,9 @@ function processBeforeSend(
event: Event,
hint: EventHint,
): PromiseLike<Event | null> | Event | null {
const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
const { beforeSend, beforeSendTransaction, ignoreSpans } = options;
const beforeSendSpan = !isStreamedBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan;

let processedEvent = event;

if (isErrorEvent(processedEvent) && beforeSend) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { Event } from './types-hoist/event';
import type { SdkInfo } from './types-hoist/sdkinfo';
import type { SdkMetadata } from './types-hoist/sdkmetadata';
import type { Session, SessionAggregates } from './types-hoist/session';
import { isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan';
import { dsnToString } from './utils/dsn';
import {
createEnvelope,
Expand Down Expand Up @@ -152,7 +153,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
const convertToSpanJSON = beforeSendSpan
? (span: SentrySpan) => {
const spanJson = spanToJSON(span);
const processedSpan = beforeSendSpan(spanJson);
const processedSpan = !isStreamedBeforeSendSpanCallback(beforeSendSpan) ? beforeSendSpan(spanJson) : spanJson;

if (!processedSpan) {
showSpanDropWarning();
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
export { hasSpansEnabled } from './utils/hasSpansEnabled';
export { withStreamedSpan } from './utils/beforeSendSpan';
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
export { handleCallbackErrors } from './utils/handleCallbackErrors';
export { parameterize, fmt } from './utils/parameterize';
Expand All @@ -79,11 +80,13 @@ export {
convertSpanLinksForEnvelope,
spanToTraceHeader,
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
getRootSpan,
INTERNAL_getSegmentSpan,
getActiveSpan,
addChildSpanToSpan,
spanTimeInputToSeconds,
Expand Down Expand Up @@ -173,6 +176,9 @@ export type {
GoogleGenAIOptions,
GoogleGenAIIstrumentedMethod,
} from './tracing/google-genai/types';

export { SpanBuffer } from './tracing/spans/spanBuffer';

export type { FeatureFlag } from './utils/featureFlags';

export {
Expand Down Expand Up @@ -386,6 +392,7 @@ export type {
ProfileChunkEnvelope,
ProfileChunkItem,
SpanEnvelope,
StreamedSpanEnvelope,
SpanItem,
LogEnvelope,
MetricEnvelope,
Expand Down Expand Up @@ -453,6 +460,7 @@ export type {
SpanJSON,
SpanContextData,
TraceFlag,
StreamedSpanJSON,
} from './types-hoist/span';
export type { SpanStatus } from './types-hoist/spanStatus';
export type { Log, LogSeverityLevel } from './types-hoist/log';
Expand Down
28 changes: 25 additions & 3 deletions packages/core/src/semanticAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Use this attribute to represent the source of a span.
* Should be one of: custom, url, route, view, component, task, unknown
*
* Use this attribute to represent the source of a span name.
* Must be one of: custom, url, route, view, component, task
* TODO(v11): rename this to sentry.span.source'
*/
export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source';

Expand Down Expand Up @@ -40,6 +40,28 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_un
/** The value of a measurement, which may be stored as a TimedEvent. */
export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value';

/** The release version of the application */
export const SEMANTIC_ATTRIBUTE_SENTRY_RELEASE = 'sentry.release';
/** The environment name (e.g., "production", "staging", "development") */
export const SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT = 'sentry.environment';
/** The segment name (e.g., "GET /users") */
export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME = 'sentry.segment.name';
/** The id of the segment that this span belongs to. */
export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID = 'sentry.segment.id';
/** The name of the Sentry SDK (e.g., "sentry.php", "sentry.javascript") */
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME = 'sentry.sdk.name';
/** The version of the Sentry SDK */
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION = 'sentry.sdk.version';

/** The user ID (gated by sendDefaultPii) */
export const SEMANTIC_ATTRIBUTE_USER_ID = 'user.id';
/** The user email (gated by sendDefaultPii) */
export const SEMANTIC_ATTRIBUTE_USER_EMAIL = 'user.email';
/** The user IP address (gated by sendDefaultPii) */
export const SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS = 'user.ip_address';
/** The user username (gated by sendDefaultPii) */
export const SEMANTIC_ATTRIBUTE_USER_USERNAME = 'user.name';

/**
* A custom span name set by users guaranteed to be taken over any automatically
* inferred name. This attribute is removed before the span is sent.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tracing/dynamicSamplingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<
const dsc = getDynamicSamplingContextFromClient(span.spanContext().traceId, client);

// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
const source = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
// TODO(v11): Only read `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` again, once we renamed it to `sentry.span.source`
const source = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] ?? rootSpanAttributes['sentry.span.source'];

// after JSON conversion, txn.name becomes jsonSpan.description
const name = rootSpanJson.description;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ export {
export { setMeasurement, timedEventsToMeasurements } from './measurement';
export { sampleSpan } from './sampling';
export { logSpanEnd, logSpanStart } from './logSpans';

// Span Streaming
export { captureSpan } from './spans/captureSpan';
28 changes: 28 additions & 0 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import { createSpanEnvelope } from '../envelope';
Expand All @@ -21,6 +22,7 @@ import type {
SpanJSON,
SpanOrigin,
SpanTimeInput,
StreamedSpanJSON,
} from '../types-hoist/span';
import type { SpanStatus } from '../types-hoist/spanStatus';
import type { TimedEvent } from '../types-hoist/timedEvent';
Expand All @@ -29,8 +31,10 @@ import { generateSpanId, generateTraceId } from '../utils/propagationContext';
import {
convertSpanLinksForEnvelope,
getRootSpan,
getSimpleStatusMessage,
getSpanDescendants,
getStatusMessage,
getStreamedSpanLinks,
spanTimeInputToSeconds,
spanToJSON,
spanToTransactionTraceContext,
Expand Down Expand Up @@ -241,6 +245,30 @@ export class SentrySpan implements Span {
};
}

/**
* Get {@link StreamedSpanJSON} representation of this span.
*
* @hidden
* @internal This method is purely for internal purposes and should not be used outside
* of SDK code. If you need to get a JSON representation of a span,
* use `spanToStreamedSpanJSON(span)` instead.
*/
public getStreamedSpanJSON(): StreamedSpanJSON {
return {
name: this._name ?? '',
span_id: this._spanId,
trace_id: this._traceId,
parent_span_id: this._parentSpanId,
start_timestamp: this._startTime,
// just in case _endTime is not set, we use the start time (i.e. duration 0)
end_timestamp: this._endTime ?? this._startTime,
is_segment: this._isStandaloneSpan || this === getRootSpan(this),
status: getSimpleStatusMessage(this._status),
attributes: this._attributes,
links: getStreamedSpanLinks(this._links),
};
}

/** @inheritdoc */
public isRecording(): boolean {
return !this._endTime && !!this._sampled;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/tracing/spans/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
For now, all span streaming related tracing code is in this sub directory.
Once we get rid of transaction-based tracing, we can clean up and flatten the entire tracing directory.
150 changes: 150 additions & 0 deletions packages/core/src/tracing/spans/captureSpan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { RawAttributes } from '../../attributes';
import type { Client } from '../../client';
import type { ScopeData } from '../../scope';
import {
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SEMANTIC_ATTRIBUTE_USER_EMAIL,
SEMANTIC_ATTRIBUTE_USER_ID,
SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS,
SEMANTIC_ATTRIBUTE_USER_USERNAME,
} from '../../semanticAttributes';
import type { SerializedStreamedSpan, Span, StreamedSpanJSON } from '../../types-hoist/span';
import { isStreamedBeforeSendSpanCallback } from '../../utils/beforeSendSpan';
import { getCombinedScopeData } from '../../utils/scopeData';
import {
INTERNAL_getSegmentSpan,
showSpanDropWarning,
spanToStreamedSpanJSON,
streamedSpanJsonToSerializedSpan,
} from '../../utils/spanUtils';
import { getCapturedScopesOnSpan } from '../utils';

export type SerializedStreamedSpanWithSegmentSpan = SerializedStreamedSpan & {
_segmentSpan: Span;
};

/**
* Captures a span and returns a JSON representation to be enqueued for sending.
*
* IMPORTANT: This function converts the span to JSON immediately to avoid writing
* to an already-ended OTel span instance (which is blocked by the OTel Span class).
*
* @returns the final serialized span with a reference to its segment span. This reference
* is needed later on to compute the DSC for the span envelope.
*/
export function captureSpan(span: Span, client: Client): SerializedStreamedSpanWithSegmentSpan {
// Convert to JSON FIRST - we cannot write to an already-ended span
const spanJSON = spanToStreamedSpanJSON(span);

const segmentSpan = INTERNAL_getSegmentSpan(span);
const serializedSegmentSpan = spanToStreamedSpanJSON(segmentSpan);

const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span);

const finalScopeData = getCombinedScopeData(spanIsolationScope, spanScope);

applyCommonSpanAttributes(spanJSON, serializedSegmentSpan, client, finalScopeData);

if (span === segmentSpan) {
applyScopeToSegmentSpan(spanJSON, finalScopeData);
// Allow hook subscribers to mutate the segment span JSON
client.emit('processSegmentSpan', spanJSON);
}

// Allow hook subscribers to mutate the span JSON
client.emit('processSpan', spanJSON);

const { beforeSendSpan } = client.getOptions();
const processedSpan =
beforeSendSpan && isStreamedBeforeSendSpanCallback(beforeSendSpan)
? applyBeforeSendSpanCallback(spanJSON, beforeSendSpan)
: spanJSON;

// Backfill sentry.span.source from sentry.source. Only `sentry.span.source` is respected by Sentry.
// TODO(v11): Remove this backfill once we renamed SEMANTIC_ATTRIBUTE_SENTRY_SOURCE to sentry.span.source
const spanNameSource = processedSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
if (spanNameSource) {
safeSetSpanJSONAttributes(processedSpan, {
// Purposefully not using a constant defined here like in other attributes:
// This will be the name for SEMANTIC_ATTRIBUTE_SENTRY_SOURCE in v11
'sentry.span.source': spanNameSource,
});
}

return {
...streamedSpanJsonToSerializedSpan(processedSpan),
_segmentSpan: segmentSpan,
};
}

function applyScopeToSegmentSpan(_segmentSpanJSON: StreamedSpanJSON, _scopeData: ScopeData): void {
// TODO: Apply all scope and request data from auto instrumentation (contexts, request) to segment span
// This will follow in a separate PR
}

function applyCommonSpanAttributes(
spanJSON: StreamedSpanJSON,
serializedSegmentSpan: StreamedSpanJSON,
client: Client,
scopeData: ScopeData,
): void {
const sdk = client.getSdkMetadata();
const { release, environment, sendDefaultPii } = client.getOptions();

// avoid overwriting any previously set attributes (from users or potentially our SDK instrumentation)
safeSetSpanJSONAttributes(spanJSON, {
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: release,
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: environment,
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: serializedSegmentSpan.name,
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: serializedSegmentSpan.span_id,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: sdk?.sdk?.name,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version,
...(sendDefaultPii
? {
[SEMANTIC_ATTRIBUTE_USER_ID]: scopeData.user?.id,
[SEMANTIC_ATTRIBUTE_USER_EMAIL]: scopeData.user?.email,
[SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: scopeData.user?.ip_address,
[SEMANTIC_ATTRIBUTE_USER_USERNAME]: scopeData.user?.username,
}
: {}),
...scopeData.attributes,
});
}

/**
* Apply a user-provided beforeSendSpan callback to a span JSON.
*/
export function applyBeforeSendSpanCallback(
span: StreamedSpanJSON,
beforeSendSpan: (span: StreamedSpanJSON) => StreamedSpanJSON,
): StreamedSpanJSON {
const modifedSpan = beforeSendSpan(span);
if (!modifedSpan) {
showSpanDropWarning();
return span;
}
return modifedSpan;
}

/**
* Safely set attributes on a span JSON.
* If an attribute already exists, it will not be overwritten.
*/
export function safeSetSpanJSONAttributes(
spanJSON: StreamedSpanJSON,
newAttributes: RawAttributes<Record<string, unknown>>,
): void {
const originalAttributes = spanJSON.attributes ?? (spanJSON.attributes = {});

Object.entries(newAttributes).forEach(([key, value]) => {
if (value != null && !(key in originalAttributes)) {
originalAttributes[key] = value;
}
});
}
Loading
Loading