diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index f59474b776e6..57caadbc322c 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -62,7 +62,7 @@ export function _updateSpanAttributesForParametrizedUrl(route: string, url: stri return; } - const { data: attributes, op } = spanToJSON(span); + const attributes = spanToJSON(span).attributes; if (!attributes || attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { span.updateName(route); @@ -70,7 +70,7 @@ export function _updateSpanAttributesForParametrizedUrl(route: string, url: stri const absoluteUrl = getAbsoluteUrl(url); span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${op}.angular`, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${attributes[SENTRY_OP]}.angular`, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_FULL]: absoluteUrl, [URL_PATH]: parseStringToURLObject(absoluteUrl)?.pathname, @@ -252,7 +252,7 @@ export class TraceService implements OnDestroy { const rootSpan = getRootSpan(activeSpan); - this._pageloadOngoing = spanToJSON(rootSpan).op === 'pageload'; + this._pageloadOngoing = spanToJSON(rootSpan).attributes[SENTRY_OP] === 'pageload'; return this._pageloadOngoing; } } diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 2b846fe68788..49798683ec4e 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { HTTP_ROUTE, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY } from '@sentry/conventions/attributes'; +import { HTTP_ROUTE, SENTRY_OP, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; import { addNonEnumerableProperty, @@ -96,7 +96,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareHandler = const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; // if there is an active span, we just want to enhance it with routing data etc. - if (rootSpan && spanToJSON(rootSpan).op === 'http.server') { + if (rootSpan && spanToJSON(rootSpan).attributes[SENTRY_OP] === 'http.server') { return enhanceHttpServerSpan(ctx, next, rootSpan); } diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index f1e28a7d50b5..64f6bdf9b498 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -26,7 +26,7 @@ import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; import { DEBUG_BUILD } from '../debug-build'; -import { URL_FULL } from '@sentry/conventions/attributes'; +import { SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes'; interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; } @@ -114,13 +114,13 @@ export function startTrackingLongTasks(): void { return; } - const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(parent); + const { attributes: parentAttributes, start_timestamp: parentStartTimestamp } = spanToJSON(parent); for (const entry of entries) { const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); const duration = msToSec(entry.duration); - if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) { + if (parentAttributes[SENTRY_OP] === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) { // Skip adding a span if the long task started before the navigation started. // `startAndEndSpan` will otherwise adjust the parent's start time to the span's start // time, potentially skewing the duration of the actual navigation as reported via our @@ -158,7 +158,10 @@ export function startTrackingLongAnimationFrames(): void { const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime); - const { start_timestamp: parentStartTimestamp, op: parentOp } = spanToJSON(parent); + const { + start_timestamp: parentStartTimestamp, + attributes: { [SENTRY_OP]: parentOp }, + } = spanToJSON(parent); if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) { // Skip adding the span if the long animation frame started before the navigation started. @@ -344,7 +347,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries const performanceEntries = performance.getEntries(); - const { op, start_timestamp: transactionStartTime } = spanToJSON(span); + const { attributes, start_timestamp: transactionStartTime } = spanToJSON(span); performanceEntries.slice(_performanceCursor).forEach(entry => { const startTime = msToSec(entry.startTime); @@ -356,7 +359,11 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries Math.max(0, entry.duration), ); - if (op === 'navigation' && transactionStartTime && timeOrigin + startTime < transactionStartTime) { + if ( + attributes?.[SENTRY_OP] === 'navigation' && + transactionStartTime && + timeOrigin + startTime < transactionStartTime + ) { return; } @@ -410,7 +417,7 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption const timeOrigin = msToSec(origin); // Measurements are only available for pageload transactions - if (spanToJSON(span).op === 'pageload') { + if (spanToJSON(span).attributes?.[SENTRY_OP] === 'pageload') { _addTtfbRequestTimeToMeasurements(_measurements); if (spanStreamingEnabled) { @@ -682,7 +689,7 @@ function _trackNavigator(span: Span, spanStreamingEnabled: boolean | undefined): if (isMeasurementValue(connection.rtt)) { if (spanStreamingEnabled) { span.setAttribute('network.connection.rtt', connection.rtt); - } else if (spanToJSON(span).op === 'pageload') { + } else if (spanToJSON(span).attributes?.[SENTRY_OP] === 'pageload') { // Measurements are only recorded on the pageload span, matching the historical // behavior where `connection.rtt` was only flushed for pageload transactions. setMeasurement('connection.rtt', connection.rtt, 'millisecond'); diff --git a/packages/browser-utils/src/metrics/userTiming.ts b/packages/browser-utils/src/metrics/userTiming.ts index 822a47c13991..8910b78f532b 100644 --- a/packages/browser-utils/src/metrics/userTiming.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -1,4 +1,4 @@ -import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { IntegrationFn, Span, SpanAttributes, SpanAttributeValue } from '@sentry/core'; import { browserPerformanceTimeOrigin, @@ -34,7 +34,9 @@ const _userTimingIntegration = ((options: UserTimingOptions = {}) => { let performanceCursor = 0; client.on('beforeIdleSpanEnd', idleSpan => { - const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(idleSpan); + const { attributes, start_timestamp: parentStartTimestamp } = spanToJSON(idleSpan); + const parentOp = attributes[SENTRY_OP]; + if (parentOp !== 'pageload' && parentOp !== 'navigation') { return; } diff --git a/packages/browser-utils/src/metrics/webVitalSpans.ts b/packages/browser-utils/src/metrics/webVitalSpans.ts index 9c4f82249874..84b28d242445 100644 --- a/packages/browser-utils/src/metrics/webVitalSpans.ts +++ b/packages/browser-utils/src/metrics/webVitalSpans.ts @@ -10,7 +10,7 @@ import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToStreamedSpanJSON, + spanToJSON, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; @@ -100,7 +100,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { ...passedAttributes, }; - if (parentSpan && spanToStreamedSpanJSON(parentSpan).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') { + if (parentSpan && spanToJSON(parentSpan).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') { // for LCP and CLS, we collect the pageload span id as an attribute attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId; } @@ -338,9 +338,7 @@ export function _sendInpSpan(inpValue: number, entry: PerformanceEventTiming, st const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; const spanToUse = cachedContext?.span || rootSpan; - const routeName = spanToUse - ? spanToStreamedSpanJSON(spanToUse).name - : getCurrentScope().getScopeData().transactionName; + const routeName = spanToUse ? spanToJSON(spanToUse).name : getCurrentScope().getScopeData().transactionName; const name = cachedContext?.elementName || htmlTreeAsString(entry.target); _emitWebVitalSpan({ diff --git a/packages/browser/src/integrations/graphqlClient.ts b/packages/browser/src/integrations/graphqlClient.ts index eafead683d22..610479cddf76 100644 --- a/packages/browser/src/integrations/graphqlClient.ts +++ b/packages/browser/src/integrations/graphqlClient.ts @@ -10,7 +10,7 @@ import { } from '@sentry/core/browser'; import type { FetchHint, XhrHint } from '@sentry/browser-utils'; import { getBodyString, getFetchRequestArgBody, SENTRY_XHR_DATA_KEY } from '@sentry/browser-utils'; -import { GRAPHQL_DOCUMENT, URL_FULL } from '@sentry/conventions/attributes'; +import { GRAPHQL_DOCUMENT, HTTP_METHOD, SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes'; interface GraphQLClientOptions { endpoints: Array; @@ -59,8 +59,8 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption client.on('beforeOutgoingRequestSpan', (span, hint) => { const spanJSON = spanToJSON(span); - const spanAttributes = spanJSON.data || {}; - const spanOp = spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const spanAttributes = spanJSON.attributes || {}; + const spanOp = spanAttributes[SENTRY_OP]; const isHttpClientSpan = spanOp === 'http.client'; @@ -69,7 +69,8 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption } const httpUrl = spanAttributes[URL_FULL]; - const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method']; + // oxlint-disable-next-line typescript/no-deprecated + const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes[HTTP_METHOD]; if (!isString(httpUrl) || !isString(httpMethod)) { return; diff --git a/packages/browser/src/profiling/startProfileForSpan.ts b/packages/browser/src/profiling/startProfileForSpan.ts index 974cf9e8e9b2..529bab1db988 100644 --- a/packages/browser/src/profiling/startProfileForSpan.ts +++ b/packages/browser/src/profiling/startProfileForSpan.ts @@ -24,6 +24,7 @@ export function startProfileForSpan(span: Span): void { startTimestamp = timestampInSeconds() * 1000; } + const spanName = spanToJSON(span).name; const profiler = startJSSelfProfile(); // We failed to construct the profiler, so we skip. @@ -33,7 +34,7 @@ export function startProfileForSpan(span: Span): void { } if (DEBUG_BUILD) { - debug.log(`[Profiling] started profiling span: ${spanToJSON(span).description}`); + debug.log(`[Profiling] started profiling span: ${spanName}`); } // We create "unique" span names to avoid concurrent spans with same names @@ -72,7 +73,7 @@ export function startProfileForSpan(span: Span): void { } if (processedProfile) { if (DEBUG_BUILD) { - debug.log('[Profiling] profile for:', spanToJSON(span).description, 'already exists, returning early'); + debug.log('[Profiling] profile for:', spanName, 'already exists, returning early'); } return; } @@ -86,14 +87,14 @@ export function startProfileForSpan(span: Span): void { } if (DEBUG_BUILD) { - debug.log(`[Profiling] stopped profiling of span: ${spanToJSON(span).description}`); + debug.log(`[Profiling] stopped profiling of span: ${spanName}`); } // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { debug.log( - `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`, + `[Profiling] profiler returned null profile for: ${spanName}`, 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started', ); } @@ -113,7 +114,7 @@ export function startProfileForSpan(span: Span): void { // Enqueue a timeout to prevent profiles from running over max duration. let maxDurationTimeoutID: number | undefined = WINDOW.setTimeout(() => { if (DEBUG_BUILD) { - debug.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); + debug.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanName); } // If the timeout exceeds, we want to stop profiling, but not finish the span // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 938b7eadb784..ebe7e212f867 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -27,6 +27,7 @@ import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor, JSSelfProfileStack } from './jsSelfProfiling'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; const MS_TO_NS = 1e6; @@ -380,7 +381,7 @@ export function isProfiledTransactionEvent(event: Event): event is ProfiledEvent * */ export function isAutomatedPageLoadSpan(span: Span): boolean { - return spanToJSON(span).op === 'pageload'; + return spanToJSON(span).attributes[SENTRY_OP] === 'pageload'; } /** diff --git a/packages/browser/src/tracing/backgroundtab.ts b/packages/browser/src/tracing/backgroundtab.ts index 29d728c75001..a0e518c977e7 100644 --- a/packages/browser/src/tracing/backgroundtab.ts +++ b/packages/browser/src/tracing/backgroundtab.ts @@ -1,6 +1,7 @@ import { debug, getActiveSpan, getRootSpan, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; /** * Add a listener that cancels and finishes a transaction when the global @@ -19,7 +20,10 @@ export function registerBackgroundTabDetection(): void { if (WINDOW.document.hidden && rootSpan) { const cancelledStatus = 'cancelled'; - const { op, status } = spanToJSON(rootSpan); + const { + attributes: { [SENTRY_OP]: op }, + status, + } = spanToJSON(rootSpan); if (DEBUG_BUILD) { debug.log(`[Tracing] Transaction: ${cancelledStatus} -> since tab moved to the background, op: ${op}`); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index b0c67a006185..50414a41070b 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -51,7 +51,7 @@ import { WEB_VITALS_INTEGRATION_NAME, webVitalsIntegration } from '../integratio import { registerBackgroundTabDetection } from './backgroundtab'; import { linkTraces } from './linkedTraces'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; -import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; @@ -485,10 +485,12 @@ export const browserTracingIntegration = ((options: Partial { const conversationId = scopeData.conversationId || isolationScopeData.conversationId; if (conversationId) { - const { op, data: attributes, description: name } = spanToJSON(span); + const { op, data: attributes, description: name } = spanToStaticSpanJSON(span); // Only apply conversation ID to gen_ai spans. // We also check for Vercel AI spans (ai.operationId attribute or ai.* span name) diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 03dc9635ff11..134cab7171be 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -10,6 +10,7 @@ export type { OfflineStore, OfflineTransportOptions } from './transports/offline export type { IntegrationIndex } from './integration'; export * from './tracing'; export * from './semanticAttributes'; +export type { RawAttributes } from './attributes'; export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { captureException, @@ -99,8 +100,8 @@ export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, spanToTraceHeader, + spanToStaticSpanJSON, spanToJSON, - spanToStreamedSpanJSON, spanIsSampled, spanIsSentrySpan, spanToTraceContext, diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 8d428ddaa1d4..b56818dcdec7 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -13,7 +13,7 @@ import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBa import { extractOrgIdFromClient } from '../utils/dsn'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; import { addNonEnumerableProperty } from '../utils/object'; -import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; +import { getRootSpan, spanIsSampled, spanToStaticSpanJSON } from '../utils/spanUtils'; import { spanIsNonRecordingSpan } from './sentryNonRecordingSpan'; import { getCapturedScopesOnSpan } from './utils'; @@ -82,7 +82,7 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly child !== span); - const spanJson = spanToJSON(span); + const spanJson = spanToStaticSpanJSON(span); // If we have no spans, we just end, nothing else to do here // Likewise, if users explicitly ended the span, we simply end the span without timestamp adjustment @@ -164,7 +164,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti const ignoreSpans = client.getOptions().ignoreSpans; const latestSpanEndTimestamp = spans?.reduce((acc: number | undefined, current) => { - const currentSpanJson = spanToJSON(current); + const currentSpanJson = spanToStaticSpanJSON(current); if (!currentSpanJson.timestamp) { return acc; } @@ -287,7 +287,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti _setSpanForScope(scope, previousActiveSpan); - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); const { start_timestamp: startTimestamp } = spanJSON; // This should never happen, but to make TS happy... @@ -320,7 +320,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti debug.log('[Tracing] Cancelling span since span ended early', JSON.stringify(childSpan, undefined, 2)); } - const childSpanJSON = spanToJSON(childSpan); + const childSpanJSON = spanToStaticSpanJSON(childSpan); const { timestamp: childEndTimestamp = 0, start_timestamp: childStartTimestamp = 0 } = childSpanJSON; const spanStartedBeforeIdleSpanEnd = childStartTimestamp <= endTimestamp; @@ -359,7 +359,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti if ( _finished || startedSpan === span || - !!spanToJSON(startedSpan).timestamp || + !!spanToStaticSpanJSON(startedSpan).timestamp || (startedSpan instanceof SentrySpan && startedSpan.isStandaloneSpan()) ) { return; diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts index 6c880c8a03d9..f5120e519bc9 100644 --- a/packages/core/src/tracing/logSpans.ts +++ b/packages/core/src/tracing/logSpans.ts @@ -1,7 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Span } from '../types/span'; import { debug } from '../utils/debug-logger'; -import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; +import { getRootSpan, spanIsSampled, spanToStaticSpanJSON } from '../utils/spanUtils'; /** * Print a log message for a started span. @@ -9,7 +9,11 @@ import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; export function logSpanStart(span: Span): void { if (!DEBUG_BUILD) return; - const { description = '< unknown name >', op = '< unknown op >', parent_span_id: parentSpanId } = spanToJSON(span); + const { + description = '< unknown name >', + op = '< unknown op >', + parent_span_id: parentSpanId, + } = spanToStaticSpanJSON(span); const { spanId } = span.spanContext(); const sampled = spanIsSampled(span); @@ -25,7 +29,7 @@ export function logSpanStart(span: Span): void { } if (!isRootSpan) { - const { op, description } = spanToJSON(rootSpan); + const { op, description } = spanToStaticSpanJSON(rootSpan); infoParts.push(`root ID: ${rootSpan.spanContext().spanId}`); if (op) { infoParts.push(`root op: ${op}`); @@ -45,7 +49,7 @@ export function logSpanStart(span: Span): void { export function logSpanEnd(span: Span): void { if (!DEBUG_BUILD) return; - const { description = '< unknown name >', op = '< unknown op >' } = spanToJSON(span); + const { description = '< unknown name >', op = '< unknown op >' } = spanToStaticSpanJSON(span); const { spanId } = span.spanContext(); const rootSpan = getRootSpan(span); const isRootSpan = rootSpan === span; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 06004521e417..649e4b25eea7 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -36,7 +36,7 @@ import { getStatusMessage, getStreamedSpanLinks, spanTimeInputToSeconds, - spanToJSON, + spanToStaticSpanJSON, spanToTransactionTraceContext, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, @@ -436,7 +436,7 @@ export class SentrySpan implements Span { */ private _convertSpanToTransaction(options: SegmentSpanCaptureConvertOptions = {}): TransactionEvent | undefined { // We can only convert finished spans - if (!isFullFinishedSpan(spanToJSON(this))) { + if (!isFullFinishedSpan(spanToStaticSpanJSON(this))) { return undefined; } @@ -463,7 +463,7 @@ export class SentrySpan implements Span { if (descendant === this || isStandaloneSpan(descendant) || options.isSpanAlreadyCaptured?.(descendant)) { continue; } - const spanJSON = spanToJSON(descendant); + const spanJSON = spanToStaticSpanJSON(descendant); if (!isFullFinishedSpan(spanJSON)) { continue; } diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts index 39ad44585c5f..76e15ce56cd1 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -16,8 +16,8 @@ import { getCombinedScopeData } from '../../utils/scopeData'; import { INTERNAL_getSegmentSpan, showSpanDropWarning, + spanToStaticSpanJSON, spanToJSON, - spanToStreamedSpanJSON, streamedSpanJsonToSerializedSpan, } from '../../utils/spanUtils'; import { getCapturedScopesOnSpan } from '../utils'; @@ -48,10 +48,10 @@ export type SerializedStreamedSpanWithSegmentSpan = SerializedStreamedSpan & { */ 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 spanJSON = spanToJSON(span); const segmentSpan = INTERNAL_getSegmentSpan(span); - const serializedSegmentSpan = spanToStreamedSpanJSON(segmentSpan); + const serializedSegmentSpan = spanToJSON(segmentSpan); const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span); @@ -179,10 +179,10 @@ export function captureStandaloneSpanWithStaticCallback( client: Client, beforeSendSpan: (span: SpanJSON) => SpanJSON, ): SerializedStreamedSpan { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); const segmentSpan = INTERNAL_getSegmentSpan(span); - const serializedSegmentSpan = spanToStreamedSpanJSON(segmentSpan); + const serializedSegmentSpan = spanToJSON(segmentSpan); const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span); const finalScopeData = getCombinedScopeData(spanIsolationScope, spanScope); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index cfcdea9d4ecb..6fa47e92fc6d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -24,7 +24,13 @@ import { parseSampleRate } from '../utils/parseSampleRate'; import { generateTraceId } from '../utils/propagationContext'; import { safeMathRandom } from '../utils/randomSafeContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; -import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; +import { + addChildSpanToSpan, + getRootSpan, + spanIsSampled, + spanTimeInputToSeconds, + spanToStaticSpanJSON, +} from '../utils/spanUtils'; import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; @@ -89,7 +95,7 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) = () => callback(activeSpan), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished - const { status } = spanToJSON(activeSpan); + const { status } = spanToStaticSpanJSON(activeSpan); if (activeSpan.isRecording() && status === 'ok') { activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } @@ -155,7 +161,7 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S () => callback(activeSpan, () => activeSpan.end()), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished - const { status } = spanToJSON(activeSpan); + const { status } = spanToStaticSpanJSON(activeSpan); if (activeSpan.isRecording() && status === 'ok') { activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index 458077e40ccd..886e92426d94 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -7,7 +7,7 @@ import { shouldEnableTruncation } from '../ai/utils'; import type { Event } from '../../types/event'; import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, StreamedSpanJSON } from '../../types/span'; import { _INTERNAL_skipAiProviderWrapping } from '../../utils/ai/providerSkip'; -import { spanToJSON } from '../../utils/spanUtils'; +import { spanToStaticSpanJSON } from '../../utils/spanUtils'; import { WORKERS_AI_INTEGRATION_NAME } from '../workers-ai/constants'; import { GEN_AI_CONVERSATION_ID, @@ -70,7 +70,7 @@ import { * This is supposed to be used in `client.on('spanStart', ...) */ function onVercelAiSpanStart(span: Span): void { - const { data: attributes, description: name } = spanToJSON(span); + const { data: attributes, description: name } = spanToStaticSpanJSON(span); if (!name) { return; @@ -504,7 +504,7 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute if (descriptions.size > 0) { // Tool call spans are siblings of doGenerate (both children of invoke_agent), // so we key by the parent span ID (the invoke_agent span). - const parentSpanId = spanToJSON(span).parent_span_id; + const parentSpanId = spanToStaticSpanJSON(span).parent_span_id; if (parentSpanId) { toolDescriptionMap.set(parentSpanId, descriptions); } diff --git a/packages/core/src/types/samplingcontext.ts b/packages/core/src/types/samplingcontext.ts index 19e2e3e2435f..67c945c025bd 100644 --- a/packages/core/src/types/samplingcontext.ts +++ b/packages/core/src/types/samplingcontext.ts @@ -1,6 +1,6 @@ +import type { RawAttributes } from '../attributes'; import type { RequestEventData } from '../types/request'; import type { WorkerLocation } from './misc'; -import type { SpanAttributes } from './span'; /** * Context data passed by the user when starting a transaction, to be used by the tracesSampler method. @@ -38,7 +38,7 @@ export interface SamplingContext extends CustomSamplingContext { name: string; /** Initial attributes that have been passed to the span being sampled. */ - attributes?: SpanAttributes; + attributes?: RawAttributes>; } /** diff --git a/packages/core/src/types/span.ts b/packages/core/src/types/span.ts index 020953009832..bd1548593d74 100644 --- a/packages/core/src/types/span.ts +++ b/packages/core/src/types/span.ts @@ -52,7 +52,7 @@ export interface StreamedSpanJSON { end_timestamp: number; status: 'ok' | 'error'; is_segment: boolean; - attributes?: RawAttributes>; + attributes: RawAttributes>; links?: SpanLinkJSON>>[]; } diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index caafe439473c..fed1d30975ce 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -2,7 +2,7 @@ import { getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { type Event } from '../types/event'; import { debug } from './debug-logger'; -import { getActiveSpan, spanToJSON } from './spanUtils'; +import { getActiveSpan, spanToStaticSpanJSON } from './spanUtils'; /** * Ordered LRU cache for storing feature flags in the scope context. The name @@ -143,7 +143,7 @@ export function _INTERNAL_addFeatureFlagToActiveSpan( return; } - const attributes = spanToJSON(span).data; + const attributes = spanToStaticSpanJSON(span).data; // If the flag already exists, always update it if (`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}` in attributes) { diff --git a/packages/core/src/utils/scopeData.ts b/packages/core/src/utils/scopeData.ts index afc7182f554d..8ad95bbd51c4 100644 --- a/packages/core/src/utils/scopeData.ts +++ b/packages/core/src/utils/scopeData.ts @@ -5,7 +5,7 @@ import type { Breadcrumb } from '../types/breadcrumb'; import type { Event } from '../types/event'; import type { Span } from '../types/span'; import { merge } from './merge'; -import { getRootSpan, spanToJSON, spanToTraceContext } from './spanUtils'; +import { getRootSpan, spanToStaticSpanJSON, spanToTraceContext } from './spanUtils'; /** * Applies data from the scope to the event and runs all event processors on it. @@ -181,7 +181,7 @@ function applySpanToEvent(event: Event, span: Span): void { }; const rootSpan = getRootSpan(span); - const transactionName = spanToJSON(rootSpan).description; + const transactionName = spanToStaticSpanJSON(rootSpan).description; if (transactionName && !event.transaction && event.type === 'transaction') { event.transaction = transactionName; } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 84dc4b57039a..ce6cbeeaeab3 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -46,7 +46,7 @@ let hasShownSpanDropWarning = false; */ export function spanToTransactionTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); - const { data, op, parent_span_id, status, origin, links } = spanToJSON(span); + const { data, op, parent_span_id, status, origin, links } = spanToStaticSpanJSON(span); return { parent_span_id, @@ -68,7 +68,7 @@ export function spanToTraceContext(span: Span): TraceContext { // If the span is remote, we use a random/virtual span as span_id to the trace context, // and the remote span as parent_span_id - const parent_span_id = isRemote ? spanId : spanToJSON(span).parent_span_id; + const parent_span_id = isRemote ? spanId : spanToStaticSpanJSON(span).parent_span_id; const scope = getCapturedScopesOnSpan(span).scope; const span_id = isRemote ? scope?.getPropagationContext().propagationSpanId || generateSpanId() : spanId; @@ -171,7 +171,7 @@ function ensureTimestampInSeconds(timestamp: number): number { // Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json). // This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility. // And `spanToJSON` needs the Span class from `span.ts` to check here. -export function spanToJSON(span: Span): SpanJSON { +export function spanToStaticSpanJSON(span: Span): SpanJSON { if (spanIsSentrySpan(span)) { return span.getSpanJSON(); } @@ -212,7 +212,7 @@ export function spanToJSON(span: Span): SpanJSON { /** * Convert a span to the intermediate {@link StreamedSpanJSON} representation. */ -export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { +export function spanToJSON(span: Span): StreamedSpanJSON { if (spanIsSentrySpan(span)) { return span.getStreamedSpanJSON(); } @@ -247,6 +247,7 @@ export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { end_timestamp: 0, status: 'ok', is_segment: span === INTERNAL_getSegmentSpan(span), + attributes: {}, }; } diff --git a/packages/core/test/lib/integrations/conversationId.test.ts b/packages/core/test/lib/integrations/conversationId.test.ts index be69a1476e83..3cd97a2d4b42 100644 --- a/packages/core/test/lib/integrations/conversationId.test.ts +++ b/packages/core/test/lib/integrations/conversationId.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getCurrentScope, getIsolationScope, setCurrentClient, startSpan } from '../../../src'; import { conversationIdIntegration } from '../../../src/integrations/conversationId'; import { GEN_AI_CONVERSATION_ID_ATTRIBUTE } from '../../../src/semanticAttributes'; -import { spanToJSON } from '../../../src/utils/spanUtils'; +import { spanToStaticSpanJSON } from '../../../src/utils/spanUtils'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; describe('ConversationId', () => { @@ -27,7 +27,7 @@ describe('ConversationId', () => { getCurrentScope().setConversationId('conv_test_123'); startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_test_123'); }); }); @@ -36,7 +36,7 @@ describe('ConversationId', () => { getIsolationScope().setConversationId('conv_isolation_456'); startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_isolation_456'); }); }); @@ -46,14 +46,14 @@ describe('ConversationId', () => { getIsolationScope().setConversationId('conv_isolation_999'); startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_current_789'); }); }); it('does not apply conversation ID when not set in scope', () => { startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); }); }); @@ -63,7 +63,7 @@ describe('ConversationId', () => { getCurrentScope().setConversationId(null); startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); }); }); @@ -73,7 +73,7 @@ describe('ConversationId', () => { startSpan({ name: 'parent-span', op: 'gen_ai.invoke_agent' }, () => { startSpan({ name: 'child-span', op: 'gen_ai.chat' }, childSpan => { - const childJSON = spanToJSON(childSpan); + const childJSON = spanToStaticSpanJSON(childSpan); expect(childJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_nested_abc'); }); }); @@ -91,7 +91,7 @@ describe('ConversationId', () => { }, }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_from_scope'); }, ); @@ -101,7 +101,7 @@ describe('ConversationId', () => { getCurrentScope().setConversationId('conv_test_123'); startSpan({ name: 'db-query', op: 'db.query' }, span => { - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); }); }); diff --git a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts index e328f66b72e5..c0326ce472fe 100644 --- a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts +++ b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { SPAN_STATUS_ERROR } from '../../../src/tracing'; import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; import type { Span } from '../../../src/types/span'; -import { spanIsSampled, spanToJSON, TRACE_FLAG_NONE } from '../../../src/utils/spanUtils'; +import { spanIsSampled, spanToStaticSpanJSON, TRACE_FLAG_NONE } from '../../../src/utils/spanUtils'; describe('SentryNonRecordingSpan', () => { it('satisfies the Span interface', () => { @@ -16,7 +16,7 @@ describe('SentryNonRecordingSpan', () => { expect(spanIsSampled(span)).toBe(false); expect(span.isRecording()).toBe(false); - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: {}, @@ -33,7 +33,7 @@ describe('SentryNonRecordingSpan', () => { span.setStatus({ code: SPAN_STATUS_ERROR }); // but nothing is actually set/readable - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: {}, diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 7f509cc23c0e..c7be45044184 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -16,7 +16,7 @@ import { } from '../../../src/tracing/utils'; import type { Envelope } from '../../../src/types/envelope'; import type { SpanJSON } from '../../../src/types/span'; -import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; +import { spanToStaticSpanJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; import { timestampInSeconds } from '../../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; @@ -24,16 +24,16 @@ describe('SentrySpan', () => { describe('name', () => { it('works with name', () => { const span = new SentrySpan({ name: 'span name' }); - expect(spanToJSON(span).description).toEqual('span name'); + expect(spanToStaticSpanJSON(span).description).toEqual('span name'); }); it('allows to update the name via updateName', () => { const span = new SentrySpan({ name: 'span name' }); - expect(spanToJSON(span).description).toEqual('span name'); + expect(spanToStaticSpanJSON(span).description).toEqual('span name'); span.updateName('new name'); - expect(spanToJSON(span).description).toEqual('new name'); + expect(spanToStaticSpanJSON(span).description).toEqual('new name'); }); it('sets the source to custom when calling updateName', () => { @@ -44,7 +44,7 @@ describe('SentrySpan', () => { span.updateName('new name'); - const spanJson = spanToJSON(span); + const spanJson = spanToStaticSpanJSON(span); expect(spanJson.description).toEqual('new name'); expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); }); @@ -54,7 +54,7 @@ describe('SentrySpan', () => { span.updateName('new name'); - const spanJson = spanToJSON(span); + const spanJson = spanToStaticSpanJSON(span); expect(spanJson.description).toEqual('new name'); expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); }); @@ -65,7 +65,7 @@ describe('SentrySpan', () => { span.updateName('new name'); - const spanJson = spanToJSON(span); + const spanJson = spanToStaticSpanJSON(span); expect(spanJson.description).toEqual('new name'); expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBeUndefined(); }); @@ -104,9 +104,9 @@ describe('SentrySpan', () => { describe('setters', () => { test('setName', () => { const span = new SentrySpan({}); - expect(spanToJSON(span).description).toBeUndefined(); + expect(spanToStaticSpanJSON(span).description).toBeUndefined(); span.updateName('foo'); - expect(spanToJSON(span).description).toBe('foo'); + expect(spanToStaticSpanJSON(span).description).toBe('foo'); }); }); @@ -114,13 +114,13 @@ describe('SentrySpan', () => { test('setStatus', () => { const span = new SentrySpan({}); span.setStatus({ code: SPAN_STATUS_ERROR, message: 'permission_denied' }); - expect(spanToJSON(span).status).toBe('permission_denied'); + expect(spanToStaticSpanJSON(span).status).toBe('permission_denied'); }); }); describe('toJSON', () => { test('simple', () => { - const span = spanToJSON( + const span = spanToStaticSpanJSON( new SentrySpan({ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', spanId: 'bbbbbbbbbbbbbbbb' }), ); expect(span).toHaveProperty('span_id', 'bbbbbbbbbbbbbbbb'); @@ -135,7 +135,7 @@ describe('SentrySpan', () => { sampled: false, parentSpanId: spanA.spanContext().spanId, }); - const serialized = spanToJSON(spanB); + const serialized = spanToStaticSpanJSON(spanB); expect(serialized).toHaveProperty('parent_span_id', 'b'); expect(serialized).toHaveProperty('span_id', 'd'); expect(serialized).toHaveProperty('trace_id', 'c'); @@ -168,7 +168,7 @@ describe('SentrySpan', () => { [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', }); - const json = spanToJSON(span); + const json = spanToStaticSpanJSON(span); expect(json.data?.['key']).toBe('before'); expect(json.data?.['key2']).toBeUndefined(); expect(json.status).toBe('permission_denied'); @@ -189,7 +189,7 @@ describe('SentrySpan', () => { span.updateStartTime(999); span.addLink({ context: linked.spanContext() }); - const json = spanToJSON(span); + const json = spanToStaticSpanJSON(span); expect(json.data?.['key']).toBe('after'); expect(json.description).toBe('after'); expect(json.start_timestamp).toBe(999); @@ -210,32 +210,32 @@ describe('SentrySpan', () => { span.end(); span.setAttribute('key', 'after'); - expect(spanToJSON(span).data?.['key']).toBe('before'); + expect(spanToStaticSpanJSON(span).data?.['key']).toBe('before'); }); }); describe('end', () => { test('simple', () => { const span = new SentrySpan({}); - expect(spanToJSON(span).timestamp).toBeUndefined(); + expect(spanToStaticSpanJSON(span).timestamp).toBeUndefined(); span.end(); - expect(spanToJSON(span).timestamp).toBeGreaterThan(1); + expect(spanToStaticSpanJSON(span).timestamp).toBeGreaterThan(1); }); test('with endTime in seconds', () => { const span = new SentrySpan({}); - expect(spanToJSON(span).timestamp).toBeUndefined(); + expect(spanToStaticSpanJSON(span).timestamp).toBeUndefined(); const endTime = Date.now() / 1000; span.end(endTime); - expect(spanToJSON(span).timestamp).toBe(endTime); + expect(spanToStaticSpanJSON(span).timestamp).toBe(endTime); }); test('with endTime in milliseconds', () => { const span = new SentrySpan({}); - expect(spanToJSON(span).timestamp).toBeUndefined(); + expect(spanToStaticSpanJSON(span).timestamp).toBeUndefined(); const endTime = Date.now(); span.end(endTime); - expect(spanToJSON(span).timestamp).toBe(endTime / 1000); + expect(spanToStaticSpanJSON(span).timestamp).toBe(endTime / 1000); }); test('uses sampled config for standalone span', () => { @@ -588,7 +588,7 @@ describe('SentrySpan', () => { const now = timestampInSeconds(); span.end(); - expect(spanToJSON(span).timestamp).toBeGreaterThanOrEqual(now); + expect(spanToStaticSpanJSON(span).timestamp).toBeGreaterThanOrEqual(now); }); it('works with endTimestamp in seconds', () => { @@ -596,7 +596,7 @@ describe('SentrySpan', () => { const timestamp = timestampInSeconds() - 1; span.end(timestamp); - expect(spanToJSON(span).timestamp).toEqual(timestamp); + expect(spanToStaticSpanJSON(span).timestamp).toEqual(timestamp); }); it('works with endTimestamp in milliseconds', () => { @@ -604,7 +604,7 @@ describe('SentrySpan', () => { const timestamp = Date.now() - 1000; span.end(timestamp); - expect(spanToJSON(span).timestamp).toEqual(timestamp / 1000); + expect(spanToStaticSpanJSON(span).timestamp).toEqual(timestamp / 1000); }); it('works with endTimestamp in array form', () => { @@ -612,7 +612,7 @@ describe('SentrySpan', () => { const seconds = Math.floor(timestampInSeconds() - 1); span.end([seconds, 0]); - expect(spanToJSON(span).timestamp).toEqual(seconds); + expect(spanToStaticSpanJSON(span).timestamp).toEqual(seconds); }); it('skips if span is already ended', () => { @@ -622,7 +622,7 @@ describe('SentrySpan', () => { span.end(); - expect(spanToJSON(span).timestamp).toBe(endTimestamp); + expect(spanToStaticSpanJSON(span).timestamp).toBe(endTimestamp); }); }); diff --git a/packages/core/test/lib/tracing/workers-ai.test.ts b/packages/core/test/lib/tracing/workers-ai.test.ts index e0fefb2ac2b4..3ac1717f1ce7 100644 --- a/packages/core/test/lib/tracing/workers-ai.test.ts +++ b/packages/core/test/lib/tracing/workers-ai.test.ts @@ -25,7 +25,7 @@ import { AI_OPERATION_ID_ATTRIBUTE } from '../../../src/tracing/vercel-ai/vercel import { instrumentWorkersAiClient } from '../../../src/tracing/workers-ai'; import type { DataCollection } from '../../../src/types/datacollection'; import { _INTERNAL_clearAiProviderSkips } from '../../../src/utils/ai/providerSkip'; -import { spanToJSON } from '../../../src/utils/spanUtils'; +import { spanToStaticSpanJSON } from '../../../src/utils/spanUtils'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const MODEL = '@cf/meta/llama-3.1-8b-instruct'; @@ -167,7 +167,7 @@ describe('instrumentWorkersAiClient', () => { }); expect(endedSpans).toHaveLength(1); - expect(spanToJSON(endedSpans[0]!).data).toEqual(expected); + expect(spanToStaticSpanJSON(endedSpans[0]!).data).toEqual(expected); }); }); @@ -183,7 +183,7 @@ describe('instrumentWorkersAiClient', () => { spans = []; const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); client.on('spanEnd', span => { - spans.push(spanToJSON(span).description ?? ''); + spans.push(spanToStaticSpanJSON(span).description ?? ''); }); setCurrentClient(client); addVercelAiProcessors(client); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index c6e1542716f2..27e7a650d9d9 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -25,7 +25,7 @@ import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, - spanToJSON, + spanToStaticSpanJSON, spanToStreamedSpanJSON, spanToTraceContext, streamedSpanJsonToSerializedSpan, @@ -316,7 +316,7 @@ describe('spanToJSON', () => { describe('SentrySpan', () => { it('works with a simple span', () => { const span = new SentrySpan(); - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ span_id: span.spanContext().spanId, trace_id: span.spanContext().traceId, origin: 'manual', @@ -343,7 +343,7 @@ describe('spanToJSON', () => { }); span.setStatus({ code: SPAN_STATUS_OK }); - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ description: 'test name', op: 'test op', parent_span_id: '1234', @@ -373,7 +373,7 @@ describe('spanToJSON', () => { status: { code: SPAN_STATUS_UNSET }, }); - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ span_id: 'SPAN-1', trace_id: 'TRACE-1', start_timestamp: 123, @@ -399,7 +399,7 @@ describe('spanToJSON', () => { status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, }); - expect(spanToJSON(span)).toEqual({ + expect(spanToStaticSpanJSON(span)).toEqual({ span_id: 'SPAN-1', trace_id: 'TRACE-1', start_timestamp: 123, @@ -731,7 +731,7 @@ describe('spanToJSON', () => { }), }; - expect(spanToJSON(span as unknown as Span)).toEqual({ + expect(spanToStaticSpanJSON(span as unknown as Span)).toEqual({ status: 'ok', span_id: 'SPAN-1', trace_id: 'TRACE-1', @@ -784,7 +784,7 @@ describe('updateSpanName', () => { it('updates the span name and source', () => { const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }); updateSpanName(span, 'new-name'); - const spanJSON = spanToJSON(span); + const spanJSON = spanToStaticSpanJSON(span); expect(spanJSON.description).toBe('new-name'); expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); }); diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index 323b55137403..8affa37c9aad 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -1,12 +1,6 @@ import { HTTP_TARGET, URL_FULL } from '@sentry/conventions/attributes'; -import { - getClient, - GLOBAL_OBJ, - isSentryRequestUrl, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - type Span, - type SpanAttributes, -} from '@sentry/core'; +import type { RawAttributes } from '@sentry/core'; +import { getClient, GLOBAL_OBJ, isSentryRequestUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span } from '@sentry/core'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; import { isPathnameUnderSentryTunnelRoute } from './tunnelPathnameMatch'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; @@ -21,7 +15,10 @@ const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { * 1. Requests to the local tunnel route (before rewrite) via middleware or BaseServer.handleRequest * 2. Requests to Sentry ingest (after rewrite) via fetch spans */ -export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | undefined): void { +export function dropMiddlewareTunnelRequests( + span: Span, + attrs: RawAttributes> | undefined, +): void { // When the user brings their own OTel setup (skipOpenTelemetrySetup: true), we should not // mutate their spans with Sentry-internal attributes as it pollutes their tracing backends. if ((getClient()?.getOptions() as { skipOpenTelemetrySetup?: boolean } | undefined)?.skipOpenTelemetrySetup) { @@ -50,7 +47,7 @@ export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | } } -function isSentryRequestSpan(attrs: SpanAttributes): boolean { +function isSentryRequestSpan(attrs: RawAttributes>): boolean { const httpUrl = attrs[URL_FULL]; if (!httpUrl) { diff --git a/packages/nextjs/src/common/utils/forkIsolationScopeForRootSpan.ts b/packages/nextjs/src/common/utils/forkIsolationScopeForRootSpan.ts index 53330ea9328f..1ff8c45ae7a0 100644 --- a/packages/nextjs/src/common/utils/forkIsolationScopeForRootSpan.ts +++ b/packages/nextjs/src/common/utils/forkIsolationScopeForRootSpan.ts @@ -1,5 +1,5 @@ import { context } from '@opentelemetry/api'; -import type { Span, SpanAttributes } from '@sentry/core'; +import type { RawAttributes, Span } from '@sentry/core'; import { getCapturedScopesOnSpan, getCurrentScope, @@ -14,7 +14,10 @@ import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; * Forks the isolation scope for `BaseServer.handleRequest` / `Middleware.execute` root spans so that request-scoped * data (e.g. `normalizedRequest`) stays isolated per request. */ -export function maybeForkIsolationScopeForRootSpan(span: Span, spanAttributes: SpanAttributes | undefined): void { +export function maybeForkIsolationScopeForRootSpan( + span: Span, + spanAttributes: RawAttributes> | undefined, +): void { const spanType = spanAttributes?.[ATTR_NEXT_SPAN_TYPE]; if (spanType !== 'BaseServer.handleRequest' && spanType !== 'Middleware.execute') { return; diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts index 916bea190470..aa2a484d0186 100644 --- a/packages/nextjs/src/common/utils/tracingUtils.ts +++ b/packages/nextjs/src/common/utils/tracingUtils.ts @@ -1,5 +1,5 @@ import { HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; -import type { PropagationContext, Span, SpanAttributes } from '@sentry/core'; +import type { PropagationContext, RawAttributes, Span, SpanAttributes } from '@sentry/core'; import { isObjectLike, debug, @@ -123,8 +123,8 @@ export function dropNextjsRootContext(): void { const nextJsOwnedSpan = getActiveSpan(); if (nextJsOwnedSpan) { const rootSpan = getRootSpan(nextJsOwnedSpan); - const rootSpanAttributes = spanToJSON(rootSpan).data; - if (rootSpanAttributes?.['next.span_type']) { + const rootSpanAttributes = spanToJSON(rootSpan).attributes; + if (rootSpanAttributes['next.span_type']) { getRootSpan(nextJsOwnedSpan)?.setAttribute(TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, true); } } @@ -135,7 +135,7 @@ export function dropNextjsRootContext(): void { * @param spanAttributes The attributes of the span to check. * @returns True if the span is a resolve segment span, false otherwise. */ -export function isResolveSegmentSpan(spanAttributes: SpanAttributes): boolean { +export function isResolveSegmentSpan(spanAttributes: RawAttributes>): boolean { return ( spanAttributes[ATTR_NEXT_SPAN_TYPE] === 'NextNodeServer.getLayoutOrPageModule' && spanAttributes[ATTR_NEXT_SPAN_NAME] === 'resolve segment modules' && @@ -170,8 +170,8 @@ export function getEnhancedResolveSegmentSpanName({ segment, route }: { segment: */ export function maybeEnhanceServerComponentSpanName( activeSpan: Span, - spanAttributes: SpanAttributes, - rootSpanAttributes: SpanAttributes, + spanAttributes: RawAttributes>, + rootSpanAttributes: RawAttributes>, ): void { if (!isResolveSegmentSpan(spanAttributes)) { return; @@ -183,7 +183,7 @@ export function maybeEnhanceServerComponentSpanName( activeSpan.updateName(enhancedName); activeSpan.setAttributes({ 'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout', - 'sentry.nextjs.ssr.function.route': route, + 'sentry.nextjs.ssr.function.route': route as string | undefined, [SENTRY_OP]: 'function.nextjs', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index bdb9b111f29f..1b88fa2a1aec 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -121,7 +121,7 @@ export function init(options: VercelEdgeOptions = {}): void { }); client.on('spanStart', span => { - const spanAttributes = spanToJSON(span).data; + const spanAttributes = spanToJSON(span).attributes; const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index ddf3f19cbe65..bd87b5f37196 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -66,15 +66,15 @@ export function wrapApiHandlerWithSentry( const rootSpan = getRootSpan(activeSpan); if (rootSpan) { - const rootSpanAttributes = spanToJSON(rootSpan).data; + const rootSpanAttributes = spanToJSON(rootSpan).attributes; rootSpan.updateName( req instanceof Request ? `${req.method} ${parameterizedRoute}` : `handler ${parameterizedRoute}`, ); rootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [URL_FULL]: rootSpanAttributes[URL_FULL] ?? urlAttributes[URL_FULL], - [URL_PATH]: rootSpanAttributes[URL_PATH] ?? urlAttributes[URL_PATH], + [URL_FULL]: (rootSpanAttributes[URL_FULL] ?? urlAttributes[URL_FULL]) as string | undefined, + [URL_PATH]: (rootSpanAttributes[URL_PATH] ?? urlAttributes[URL_PATH]) as string | undefined, [HTTP_ROUTE]: parameterizedRoute, ...headerAttributes, }); diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts index d4889cb90603..12d105f7d228 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -22,9 +22,9 @@ import { maybeEnrichQueueConsumerSpan, maybeEnrichQueueProducerSpan } from './ve * @param span The span that is starting. */ export function handleOnSpanStart(span: Span): void { - const spanAttributes = spanToJSON(span).data; + const spanAttributes = spanToJSON(span).attributes; const rootSpan = getRootSpan(span); - const rootSpanAttributes = spanToJSON(rootSpan).data; + const rootSpanAttributes = spanToJSON(rootSpan).attributes; const isRootSpan = span === rootSpan; dropMiddlewareTunnelRequests(span, spanAttributes); diff --git a/packages/nextjs/src/server/vercelCronsMonitoring.ts b/packages/nextjs/src/server/vercelCronsMonitoring.ts index a514f86a7a51..dc0e493b52b6 100644 --- a/packages/nextjs/src/server/vercelCronsMonitoring.ts +++ b/packages/nextjs/src/server/vercelCronsMonitoring.ts @@ -86,7 +86,7 @@ export function maybeStartCronCheckIn(span: Span, route: string | undefined): vo * Should be called from the spanEnd event handler. */ export function maybeCompleteCronCheckIn(span: Span): void { - const spanData = spanToJSON(span).data; + const spanData = spanToJSON(span).attributes; const checkInId = spanData?.[ATTR_SENTRY_CRON_CHECK_IN_ID]; const monitorSlug = spanData?.[ATTR_SENTRY_CRON_MONITOR_SLUG]; const startTime = spanData?.[ATTR_SENTRY_CRON_START_TIME]; diff --git a/packages/nextjs/src/server/vercelQueuesMonitoring.ts b/packages/nextjs/src/server/vercelQueuesMonitoring.ts index 2f759d0af3d8..e4216afd6c88 100644 --- a/packages/nextjs/src/server/vercelQueuesMonitoring.ts +++ b/packages/nextjs/src/server/vercelQueuesMonitoring.ts @@ -74,7 +74,7 @@ export function maybeEnrichQueueConsumerSpan(span: Span): void { * We use domain-based detection to avoid false positives from user routes. */ export function maybeEnrichQueueProducerSpan(span: Span): void { - const spanData = spanToJSON(span).data; + const spanData = spanToJSON(span).attributes; // http.client spans have url.full attribute const urlFull = spanData?.[URL_FULL] as string | undefined; @@ -113,7 +113,7 @@ export function maybeEnrichQueueProducerSpan(span: Span): void { * Cleans up the internal marker attribute from enriched queue spans on end. */ export function maybeCleanupQueueSpan(span: Span): void { - const spanData = spanToJSON(span).data; + const spanData = spanToJSON(span).attributes; if (spanData?.[ATTR_SENTRY_QUEUE_ENRICHED]) { span.setAttribute(ATTR_SENTRY_QUEUE_ENRICHED, undefined); } diff --git a/packages/node/src/integrations/tracing/redis/cache.ts b/packages/node/src/integrations/tracing/redis/cache.ts index 826f1e4e9465..a6a11ba4fa57 100644 --- a/packages/node/src/integrations/tracing/redis/cache.ts +++ b/packages/node/src/integrations/tracing/redis/cache.ts @@ -19,6 +19,14 @@ import { shouldConsiderForCache, } from '../../../utils/redisCache'; import type { IORedisResponseCustomAttributeFunction } from './vendored/types'; +import { + NET_PEER_NAME, + NET_PEER_PORT, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; // This module deliberately does NOT import the vendored OTel `IORedisInstrumentation`/ // `RedisInstrumentation`, so the orchestrion opt-in can pull `cacheResponseHook` @@ -74,11 +82,14 @@ export const cacheResponseHook: IORedisResponseCustomAttributeFunction = ( // Fall back to stable semconv attributes (server.address/server.port) when // old-semconv ones are absent, eg OTEL_SEMCONV_STABILITY_OPT_IN=database // set for node-redis v4/v5. - const spanData = spanToJSON(span).data; - const networkPeerAddress = spanData['net.peer.name'] ?? spanData['server.address']; - const networkPeerPort = spanData['net.peer.port'] ?? spanData['server.port']; + const attributes = spanToJSON(span).attributes; + // oxlint-disable-next-line typescript/no-deprecated + const networkPeerAddress = (attributes[NET_PEER_NAME] ?? attributes[SERVER_ADDRESS]) as string | undefined; + // oxlint-disable-next-line typescript/no-deprecated + const networkPeerPort = (attributes[NET_PEER_PORT] ?? attributes[SERVER_PORT]) as number | undefined; + if (networkPeerPort && networkPeerAddress) { - span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort }); + span.setAttributes({ [NETWORK_PEER_ADDRESS]: networkPeerAddress, [NETWORK_PEER_PORT]: networkPeerPort }); } // A remove response is a delete-count, not a cached value, so its size is meaningless. diff --git a/packages/opentelemetry/src/applyOtelSpanData.ts b/packages/opentelemetry/src/applyOtelSpanData.ts index 104a2c53a82a..9a4742a489e7 100644 --- a/packages/opentelemetry/src/applyOtelSpanData.ts +++ b/packages/opentelemetry/src/applyOtelSpanData.ts @@ -12,7 +12,7 @@ import { SPAN_STATUS_OK, isStatusErrorMessageValid, } from '@sentry/core'; -import type { Span, SpanAttributes } from '@sentry/core'; +import type { RawAttributes, Span, SpanAttributeValue } from '@sentry/core'; import { inferStatusFromAttributes } from './utils/mapStatus'; import { inferSpanData } from './utils/parseSpanDescription'; @@ -28,7 +28,7 @@ import { inferSpanData } from './utils/parseSpanDescription'; */ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolean } = {}): void { const spanJSON = spanToJSON(span); - const attributes = spanJSON.data; + const attributes = spanJSON.attributes; const mayInferSource = spanShouldInferOtelSource(span); const hasCustomSpanName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME] !== undefined; // We may only infer the source/name when the span is OTel-branded and user code hasn't already @@ -64,7 +64,7 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea function applyOtelSpanStatus( span: Span, - attributes: SpanAttributes, + attributes: RawAttributes>, status: string, spanStreamingEnabled: boolean, ): void { @@ -83,13 +83,13 @@ function applyOtelSpanStatus( } } -function applyOtelCompatibilityAttributes(span: Span, attributes: SpanAttributes): void { +function applyOtelCompatibilityAttributes(span: Span, attributes: RawAttributes>): void { // `http.status_code` is the deprecated legacy attribute, read for backward compatibility. // eslint-disable-next-line typescript/no-deprecated const legacyHttpStatusCode = attributes[HTTP_STATUS_CODE]; if (attributes[HTTP_RESPONSE_STATUS_CODE] === undefined && legacyHttpStatusCode !== undefined) { - span.setAttribute(HTTP_RESPONSE_STATUS_CODE, legacyHttpStatusCode); + span.setAttribute(HTTP_RESPONSE_STATUS_CODE, legacyHttpStatusCode as SpanAttributeValue); attributes[HTTP_RESPONSE_STATUS_CODE] = legacyHttpStatusCode; } } diff --git a/packages/opentelemetry/src/utils/mapStatus.ts b/packages/opentelemetry/src/utils/mapStatus.ts index bafdbb09d6db..20394022d910 100644 --- a/packages/opentelemetry/src/utils/mapStatus.ts +++ b/packages/opentelemetry/src/utils/mapStatus.ts @@ -1,5 +1,5 @@ import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE, RPC_GRPC_STATUS_CODE } from '@sentry/conventions/attributes'; -import type { SpanAttributes, SpanStatus } from '@sentry/core'; +import type { RawAttributes, SpanStatus } from '@sentry/core'; import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '@sentry/core'; // canonicalCodesGrpcMap maps some GRPC codes to Sentry's span statuses. See description in grpc documentation. @@ -22,7 +22,7 @@ const canonicalGrpcErrorCodesMap: Record = { '16': 'unauthenticated', } as const; -export function inferStatusFromAttributes(attributes: SpanAttributes): SpanStatus | undefined { +export function inferStatusFromAttributes(attributes: RawAttributes>): SpanStatus | undefined { // If the span status is UNSET, we try to infer it from HTTP or GRPC status codes. // eslint-disable-next-line typescript/no-deprecated diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index e50364fb1f3d..e1baef866f44 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -1,4 +1,3 @@ -import type { Attributes } from '@opentelemetry/api'; import { DB_SYSTEM, DB_SYSTEM_NAME, @@ -14,7 +13,7 @@ import { URL_FULL, URL_QUERY, } from '@sentry/conventions/attributes'; -import type { Span, SpanAttributes } from '@sentry/core'; +import type { Span } from '@sentry/core'; import { getSanitizedUrlString, getUrlFragment, @@ -26,6 +25,7 @@ import { spanToJSON, stripUrlQueryAndFragment, } from '@sentry/core'; +import type { RawAttributes } from '@sentry/core'; interface SpanDescription { op: string | undefined; @@ -35,7 +35,7 @@ interface SpanDescription { /** * Infer the op & description for a set of name, attributes and kind of a span. */ -export function inferSpanData(attributes: SpanAttributes): SpanDescription { +export function inferSpanData(attributes: RawAttributes>): SpanDescription { // if http.method exists, this is an http request span // eslint-disable-next-line typescript/no-deprecated const httpMethod = attributes[HTTP_REQUEST_METHOD] || attributes[HTTP_METHOD]; @@ -96,12 +96,12 @@ export function inferSpanData(attributes: SpanAttributes): SpanDescription { */ export function parseSpanDescription(span: Span): SpanDescription { const json = spanToJSON(span); - const attributes = json.data; + const attributes = json.attributes; return inferSpanData(attributes); } -function descriptionForDbSystem(attributes: Attributes): SpanDescription { +function descriptionForDbSystem(attributes: RawAttributes>): SpanDescription { // if we already have a custom name, we don't overwrite it but only set the op const userDefinedName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; if (typeof userDefinedName === 'string') { @@ -119,7 +119,7 @@ function descriptionForDbSystem(attributes: Attributes): SpanDescription { } /** Only exported for tests. */ -export function descriptionForHttpMethod(attributes: Attributes): SpanDescription { +export function descriptionForHttpMethod(attributes: RawAttributes>): SpanDescription { const opParts = ['http']; const kind = attributes[SENTRY_KIND]; @@ -164,7 +164,7 @@ export function descriptionForHttpMethod(attributes: Attributes): SpanDescriptio } /** Exported for tests only */ -export function getSanitizedUrl(attributes: Attributes): { +export function getSanitizedUrl(attributes: RawAttributes>): { url: string | undefined; urlPath: string | undefined; query: string | undefined; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 0662bc4a2b3a..fb66fca5e7c4 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -322,10 +322,7 @@ class ContinuousProfiler { // Enqueue a timeout to prevent profiles from running over max duration. const timeout = global.setTimeout(() => { DEBUG_BUILD && - debug.log( - '[Profiling] max profile duration elapsed, stopping profiling for:', - spanToJSON(span).description, - ); + debug.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).name); const profile = stopSpanProfile(span, profile_id); if (profile) { diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts index 7a593815e446..435ceb739330 100644 --- a/packages/profiling-node/src/spanProfileUtils.ts +++ b/packages/profiling-node/src/spanProfileUtils.ts @@ -49,13 +49,13 @@ export function maybeProfileSpan( // Prefer sampler to sample rate if both are provided. if (typeof profilesSampler === 'function') { - const { description: spanName = '', data } = spanToJSON(span); + const { name = '', attributes } = spanToJSON(span); // We bail out early if that is not the case const parentSampled = true; profilesSampleRate = profilesSampler({ - name: spanName, - attributes: data, + name, + attributes, parentSampled, ...customSamplingContext, }); @@ -97,7 +97,7 @@ export function maybeProfileSpan( const profile_id = uuid4(); CpuProfilerBindings.startProfiling(profile_id); - DEBUG_BUILD && debug.log(`[Profiling] started profiling transaction: ${spanToJSON(span).description}`); + DEBUG_BUILD && debug.log(`[Profiling] started profiling transaction: ${spanToJSON(span).name}`); // set transaction context - do this regardless if profiling fails down the line // so that we can still see the profile_id in the transaction context @@ -116,14 +116,15 @@ export function stopSpanProfile(span: Span, profile_id: string | undefined): Raw return null; } + const spanName = spanToJSON(span).name; const profile = CpuProfilerBindings.stopProfiling(profile_id, 0); - DEBUG_BUILD && debug.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(span).description}`); + DEBUG_BUILD && debug.log(`[Profiling] stopped profiling of transaction: ${spanName}`); // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { DEBUG_BUILD && debug.log( - `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`, + `[Profiling] profiler returned null profile for: ${spanName}`, 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started', ); return null; diff --git a/packages/react-router/src/client/createClientInstrumentation.ts b/packages/react-router/src/client/createClientInstrumentation.ts index 68244445eb35..8d0b273dc2f0 100644 --- a/packages/react-router/src/client/createClientInstrumentation.ts +++ b/packages/react-router/src/client/createClientInstrumentation.ts @@ -24,7 +24,7 @@ import { finalizeNavigationSpanFromHydratedRouter, updateNavigationSpanUrlFromLocation, } from './utils'; -import { URL_FULL, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, URL_FULL, URL_TEMPLATE } from '@sentry/conventions/attributes'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; @@ -371,7 +371,8 @@ function updateRootSpanRoute(routeName: string, hasPattern: boolean): void { return; } - const { op } = spanToJSON(rootSpan); + const { attributes } = spanToJSON(rootSpan); + const op = attributes[SENTRY_OP]; if (op !== 'navigation' && op !== 'pageload') { return; } diff --git a/packages/react-router/src/client/hydratedRouter.ts b/packages/react-router/src/client/hydratedRouter.ts index bac743956db9..ec759454d3a7 100644 --- a/packages/react-router/src/client/hydratedRouter.ts +++ b/packages/react-router/src/client/hydratedRouter.ts @@ -22,7 +22,7 @@ import { resolveNavigateAbsoluteUrl, resolveNavigateArg, } from './utils'; -import { URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { __reactRouterDataRouter?: DataRouter; @@ -49,7 +49,7 @@ export function instrumentHydratedRouter(): void { const pageloadSpan = getActiveRootSpan(); if (pageloadSpan) { - const pageloadName = spanToJSON(pageloadSpan).description; + const pageloadName = spanToJSON(pageloadSpan).name; const parameterizePageloadRoute = getParameterizedRoute(router.state); if ( pageloadName && @@ -126,20 +126,21 @@ export function instrumentHydratedRouter(): void { } const rootSpanJson = spanToJSON(rootSpan); + const rootSpanAttributes = rootSpanJson.attributes; // When the instrumentation API is active, navigation roots are parameterized // by the native route hooks if ( - rootSpanJson.op === 'navigation' && + rootSpanAttributes[SENTRY_OP] === 'navigation' && isClientInstrumentationApiUsed() && - rootSpanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'route' + rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'route' ) { return; } - const rootSpanName = rootSpanJson.description; + const rootSpanName = rootSpanJson.name; const parameterizedRoute = getParameterizedRoute(newState); - const spanPathname = rootSpanJson.data?.[URL_PATH] as string | undefined; + const spanPathname = rootSpanAttributes[URL_PATH] as string | undefined; const destinationPathname = normalizePathname(newState.location.pathname); if ( @@ -207,7 +208,7 @@ function getActiveRootSpan(): Span | undefined { const rootSpan = getRootSpan(activeSpan); - const op = spanToJSON(rootSpan).op; + const op = spanToJSON(rootSpan).attributes[SENTRY_OP]; // Only use this root span if it is a pageload or navigation span return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index e7a85b0e86dd..76f6bddf700d 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -107,7 +107,7 @@ class Profiler extends React.Component { const { name, includeRender = true } = this.props; if (this._mountSpan && includeRender) { - const startTime = spanToJSON(this._mountSpan).timestamp; + const startTime = spanToJSON(this._mountSpan).start_timestamp; withActiveSpan(this._mountSpan, () => { const renderSpan = startInactiveSpan({ onlyIfParent: true, @@ -209,7 +209,7 @@ function useProfiler( return (): void => { if (mountSpan && options.hasRenderSpan) { - const startTime = spanToJSON(mountSpan).timestamp; + const startTime = spanToJSON(mountSpan).start_timestamp; const endTimestamp = timestampInSeconds(); const renderSpan = startInactiveSpan({ diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index 62bf4521c2b4..eaf71c390bc0 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -47,7 +47,7 @@ import { setNavigationContext, transactionNameHasWildcard, } from './utils'; -import { URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; let _useEffect: UseEffect; let _useLocation: UseLocation; @@ -320,15 +320,15 @@ export function processResolvedRoutes( // Use captured span if provided, otherwise fall back to current active span const targetSpan = capturedSpan ?? getActiveRootSpan(); if (targetSpan) { - const spanJson = spanToJSON(targetSpan); + const { end_timestamp, attributes } = spanToJSON(targetSpan); // Skip update if span has already ended (timestamp is set when span.end() is called) - if (spanJson.timestamp) { + if (end_timestamp) { DEBUG_BUILD && debug.warn('[React Router] Lazy handler resolved after span ended - skipping update'); return; } - const spanOp = spanJson.op; + const spanOp = attributes[SENTRY_OP]; // Use captured location for route matching (ensures we match against the correct route) // Fall back to window.location only if no captured location and no captured span @@ -370,14 +370,13 @@ export function updateNavigationSpan( forceUpdate = false, matchRoutes: MatchRoutes, ): void { - const spanJson = spanToJSON(activeRootSpan); - const currentName = spanJson.description; + const { name: currentName, end_timestamp, attributes } = spanToJSON(activeRootSpan); const hasBeenNamed = (activeRootSpan as { __sentry_navigation_name_set__?: boolean })?.__sentry_navigation_name_set__; const currentNameHasWildcard = currentName && transactionNameHasWildcard(currentName); const shouldUpdate = !hasBeenNamed || forceUpdate || currentNameHasWildcard; - if (shouldUpdate && !spanJson.timestamp) { + if (shouldUpdate && end_timestamp) { const currentBranches = matchRoutes(allRoutes, location); const [name, source] = resolveRouteNameAndSource( location, @@ -389,7 +388,7 @@ export function updateNavigationSpan( _enableAsyncRouteHandlers, ); - const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const currentSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; const isImprovement = name && (!currentName || // No current name - always set @@ -419,7 +418,7 @@ function setupRouterSubscription( activeRootSpan: Span | undefined, ): void { let isInitialPageloadComplete = false; - let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload'; + let hasSeenPageloadSpan = !!activeRootSpan && spanToJSON(activeRootSpan).attributes[SENTRY_OP] === 'pageload'; let hasSeenPopAfterPageload = false; let scheduledNavigationHandler: number | null = null; let lastHandledPathname: string | null = null; @@ -427,7 +426,7 @@ function setupRouterSubscription( router.subscribe((state: RouterState) => { if (!isInitialPageloadComplete) { const currentRootSpan = getActiveRootSpan(); - const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).op === 'pageload'; + const isCurrentlyInPageload = currentRootSpan && spanToJSON(currentRootSpan).attributes[SENTRY_OP] === 'pageload'; if (isCurrentlyInPageload) { hasSeenPageloadSpan = true; @@ -872,8 +871,8 @@ function wrapPatchRoutesOnNavigation( targetPath && activeRootSpan && spanJson && - !spanJson.timestamp && // Span hasn't ended yet - spanJson.op === 'navigation' + !spanJson.end_timestamp && // Span hasn't ended yet + spanJson.attributes[SENTRY_OP] === 'navigation' ) { updateNavigationSpan( activeRootSpan, @@ -909,8 +908,8 @@ function wrapPatchRoutesOnNavigation( if ( activeRootSpan && spanJson && - !spanJson.timestamp && // Span hasn't ended yet - spanJson.op === 'navigation' + !spanJson.end_timestamp && // Span hasn't ended yet + spanJson.attributes[SENTRY_OP] === 'navigation' ) { // Use targetPath consistently - don't fall back to WINDOW.location which may have changed // if the user navigated away during async loading @@ -958,7 +957,7 @@ export function handleNavigation(opts: { } const activeRootSpan = getActiveRootSpan(); - if (activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload' && navigationType === 'POP') { + if (activeRootSpan && spanToJSON(activeRootSpan).attributes[SENTRY_OP] === 'pageload' && navigationType === 'POP') { return; } @@ -978,7 +977,7 @@ export function handleNavigation(opts: { // Determine if this navigation should be skipped as a duplicate const trackedSpanHasEnded = - trackedNav && !trackedNav.isPlaceholder ? !!spanToJSON(trackedNav.span).timestamp : false; + trackedNav && !trackedNav.isPlaceholder ? !!spanToJSON(trackedNav.span).end_timestamp : false; const { skip, shouldUpdate } = shouldSkipNavigation(trackedNav, locationKey, name, trackedSpanHasEnded); if (skip) { @@ -1203,7 +1202,7 @@ function tryUpdateSpanNameBeforeEnd( allRoutes: Set, ): void { try { - const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const currentSource = spanJson.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as string | undefined; if (currentSource === 'route' && currentName && !transactionNameHasWildcard(currentName)) { return; @@ -1228,7 +1227,7 @@ function tryUpdateSpanNameBeforeEnd( ); const isImprovement = shouldUpdateWildcardSpanName(currentName, currentSource, name, source, true); - const spanNotEnded = spanType === 'pageload' || !spanJson.timestamp; + const spanNotEnded = spanType === 'pageload' || !spanJson.end_timestamp; if (isImprovement && spanNotEnded) { span.updateName(name); @@ -1276,8 +1275,8 @@ function patchSpanEnd( const endTimestamp = args.length > 0 ? args[0] : Date.now() / 1000; const spanJson = spanToJSON(span); - const currentName = spanJson.description; - const currentSource = spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const currentName = spanJson.name; + const currentSource = spanJson.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // Helper to clean up activeNavigationSpans after span ends const cleanupNavigationSpan = (): void => { @@ -1332,7 +1331,7 @@ function patchSpanEnd( tryUpdateSpanNameBeforeEnd( span, updatedSpanJson, - updatedSpanJson.description, + updatedSpanJson.name, location, routes, basename, diff --git a/packages/react/src/reactrouter-compat-utils/utils.ts b/packages/react/src/reactrouter-compat-utils/utils.ts index 3f44319d9ba2..c3d47f3cc6e0 100644 --- a/packages/react/src/reactrouter-compat-utils/utils.ts +++ b/packages/react/src/reactrouter-compat-utils/utils.ts @@ -3,6 +3,7 @@ import { debug, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core/brow import { DEBUG_BUILD } from '../debug-build'; import type { Location, MatchRoutes, RouteMatch, RouteObject } from '../types'; import { matchRouteManifest, stripBasenameFromPathname } from './route-manifest'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; // Global variables that these utilities depend on let _matchRoutes: MatchRoutes; @@ -375,7 +376,7 @@ export function getActiveRootSpan(): Span | undefined { return undefined; } - const op = spanToJSON(rootSpan).op; + const op = spanToJSON(rootSpan).attributes[SENTRY_OP]; // Only use this root span if it is a pageload or navigation span return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 90713d0c0bdd..3ea3e3c20fed 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -266,7 +266,7 @@ function getActiveRootSpan(): Span | undefined { return undefined; } - const op = spanToJSON(rootSpan).op; + const op = spanToJSON(rootSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; // Only use this root span if it is a pageload or navigation span return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index 820e524f578e..ae497a350f6e 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -123,7 +123,7 @@ function makeWrappedDocumentRequestFunction(instrumentTracing?: boolean) { if (instrumentTracing) { const activeSpan = getActiveSpan(); const rootSpan = activeSpan && getRootSpan(activeSpan); - const name = rootSpan ? spanToJSON(rootSpan).description : undefined; + const name = rootSpan ? spanToJSON(rootSpan).name : undefined; response = await startSpan( { @@ -175,7 +175,7 @@ function updateSpanWithRoute(args: DataFunctionArgs, build: ServerBuild): void { // Preserve the HTTP method prefix if the span already has one const method = args.request.method.toUpperCase(); - const currentSpanName = spanToJSON(rootSpan).description; + const currentSpanName = spanToJSON(rootSpan).name; const newSpanName = currentSpanName?.startsWith(method) ? `${method} ${transactionName}` : transactionName; rootSpan.updateName(newSpanName); diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 403332f87cee..828a127615ac 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -118,7 +118,7 @@ function enrichActiveSpanWithRoute(result: unknown): void { // oxlint-disable-next-line typescript/no-deprecated span.setAttribute(HTTP_ROUTE, route.path); // oxlint-disable-next-line typescript/no-deprecated - const method = spanToJSON(span).data[HTTP_METHOD]; + const method = spanToJSON(span).attributes[HTTP_METHOD]; span.updateName(typeof method === 'string' ? `${method} ${route.path}` : route.path); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index 439ae124f4a5..9269268288bd 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -840,13 +840,14 @@ export class ReplayContainer implements ReplayContainerInterface { const lastActiveSpan = this.lastActiveSpan || getActiveSpan(); const lastRootSpan = lastActiveSpan && getRootSpan(lastActiveSpan); - const attributes = (lastRootSpan && spanToJSON(lastRootSpan).data) || {}; - const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const spanJson = lastRootSpan && spanToJSON(lastRootSpan); + const attributes = spanJson?.attributes || {}; + const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as string | undefined; if (!lastRootSpan || !source || !['route', 'custom'].includes(source)) { return undefined; } - return spanToJSON(lastRootSpan).description; + return spanJson?.name; } /** diff --git a/packages/server-utils/src/graphql/utils.ts b/packages/server-utils/src/graphql/utils.ts index 96e2cc0a7d35..51902a91bda7 100644 --- a/packages/server-utils/src/graphql/utils.ts +++ b/packages/server-utils/src/graphql/utils.ts @@ -1,5 +1,5 @@ import { SENTRY_GRAPHQL_OPERATION } from '@sentry/conventions/attributes'; -import type { Span } from '@sentry/core'; +import type { Span, SpanAttributeValue } from '@sentry/core'; import { getClient, isObjectLike, getRootSpan, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; // Same key the OTel path uses, so renames stay consistent across both. @@ -43,7 +43,7 @@ export function renameRootSpanWithOperation(span: Span, operationType: string, o const newOperation = operationName ? `${operationType} ${operationName}` : operationType; // A single operation is stored as a string, multiple as an array. - const existingOperations = rootSpanJson.data[SENTRY_GRAPHQL_OPERATION]; + const existingOperations = rootSpanJson.attributes[SENTRY_GRAPHQL_OPERATION]; let operations: string | string[]; if (Array.isArray(existingOperations)) { operations = [...(existingOperations as string[]), newOperation]; @@ -56,15 +56,15 @@ export function renameRootSpanWithOperation(span: Span, operationType: string, o // Keep the pre-rename name so repeated renames don't compound. const originalDescription = - (rootSpanJson.data[ORIGINAL_DESCRIPTION_ATTRIBUTE] as string | undefined) ?? rootSpanJson.description; - if (!rootSpanJson.data[ORIGINAL_DESCRIPTION_ATTRIBUTE]) { + (rootSpanJson.attributes[ORIGINAL_DESCRIPTION_ATTRIBUTE] as string | undefined) ?? rootSpanJson.name; + if (!rootSpanJson.attributes[ORIGINAL_DESCRIPTION_ATTRIBUTE]) { rootSpan.setAttribute(ORIGINAL_DESCRIPTION_ATTRIBUTE, originalDescription); } // `updateName` stamps `source: 'custom'`, so re-set the original source afterwards to preserve it. - const source = rootSpanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const source = rootSpanJson.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; rootSpan.updateName(`${originalDescription} (${getGraphqlOperationNamesFromAttribute(operations)})`); - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source as SpanAttributeValue); } /** Format the accumulated operations for the root span name: up to 5 sorted names, then `+N`. */ diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index ec253f78f394..951a79b1261e 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -1,4 +1,4 @@ -import { GEN_AI_REQUEST_MODEL } from '@sentry/conventions/attributes'; +import { GEN_AI_REQUEST_MODEL, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { GoogleGenAIOptions, GoogleGenAIResponse, IntegrationFn, Span } from '@sentry/core'; import { @@ -93,7 +93,9 @@ function createGenAiSpan( if (operation !== 'chat') { const activeSpan = getActiveSpan(); if (activeSpan) { - const { op, origin } = spanToJSON(activeSpan); + const { + attributes: { [SENTRY_OP]: op, [SENTRY_ORIGIN]: origin }, + } = spanToJSON(activeSpan); if (origin === ORIGIN && op === 'gen_ai.chat') { return undefined; } diff --git a/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts b/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts index 0d7cf7273e29..a62f49374094 100644 --- a/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts +++ b/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts @@ -1,11 +1,5 @@ -import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { - getActiveSpan, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - spanToJSON, -} from '@sentry/core'; +import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; /** * Set the `http.route` attribute on the root HTTP server span for the current trace. @@ -24,8 +18,8 @@ export function setHttpServerSpanRouteAttribute(route: string): void { return; } - const attributes = spanToJSON(rootSpan).data; - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { + const attributes = spanToJSON(rootSpan).attributes; + if (attributes[SENTRY_OP] !== 'http.server') { return; } diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index 79ded795c817..d9128f237878 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -5,6 +5,7 @@ // Sentry product consumes today; migrating to the new names is a separate, coordinated change. /* eslint-disable typescript-eslint/no-deprecated */ import { + GEN_AI_CONVERSATION_ID, GEN_AI_EMBEDDINGS_INPUT, GEN_AI_FUNCTION_ID, GEN_AI_INPUT_MESSAGES, @@ -31,7 +32,6 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { _INTERNAL_skipAiProviderWrapping, captureException, - GEN_AI_CONVERSATION_ID_ATTRIBUTE, getClient, getProviderMetadataAttributes, getTruncatedJsonString, @@ -347,7 +347,7 @@ function addTokensToSpan(span: Span, attribute: string, value: number | undefine if (value === undefined) { return; } - const current = spanToJSON(span).data[attribute]; + const current = spanToJSON(span).attributes[attribute]; span.setAttribute(attribute, (typeof current === 'number' ? current : 0) + value); } @@ -568,12 +568,9 @@ export function enrichSpanOnEnd( const providerAttributes = getProviderMetadataAttributes(providerMetadata); // Don't overwrite a conversation id already set on span start (e.g. by `conversationIdIntegration` // from a user-set scope value); the provider-derived id is only a fallback. Matches the OTel path. - if ( - GEN_AI_CONVERSATION_ID_ATTRIBUTE in providerAttributes && - spanToJSON(span).data[GEN_AI_CONVERSATION_ID_ATTRIBUTE] - ) { + if (GEN_AI_CONVERSATION_ID in providerAttributes && spanToJSON(span).attributes[GEN_AI_CONVERSATION_ID]) { // oxlint-disable-next-line typescript/no-dynamic-delete - delete providerAttributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]; + delete providerAttributes[GEN_AI_CONVERSATION_ID]; } span.setAttributes(providerAttributes); diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index 6041253a2d28..8f1a697b2929 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -144,8 +144,8 @@ function withSentryRouterRoot(Root: Component): Component { const activeSpan = getActiveSpan(); const spanData = activeSpan ? spanToJSON(activeSpan) : undefined; - if (activeSpan && spanData?.op === 'function.tanstackstart') { + if (activeSpan && spanData?.attributes[SENTRY_OP] === 'function.tanstackstart') { if (serverFnMeta?.name) { - const method = spanData.description?.split(' ')[0] || 'GET'; + const method = spanData.name.split(' ')[0] || 'GET'; updateSpanName(activeSpan, `${method} /_serverFn/${serverFnMeta.name}`); activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } diff --git a/packages/tanstackstart-react/src/server/routeParametrization.ts b/packages/tanstackstart-react/src/server/routeParametrization.ts index 395451a1bc15..98883c79a8a5 100644 --- a/packages/tanstackstart-react/src/server/routeParametrization.ts +++ b/packages/tanstackstart-react/src/server/routeParametrization.ts @@ -53,7 +53,7 @@ export function updateSpanWithRouteParametrization(method: string, pathname: str } const rootSpan = getRootSpan(activeSpan); - const rootSpanData = spanToJSON(rootSpan).data; + const rootSpanData = spanToJSON(rootSpan).attributes; if (rootSpanData?.[HTTP_ROUTE]) { return; } diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 9a1c5335d81e..6cdda672fe54 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -2,6 +2,7 @@ import { captureException, getAbsoluteUrl } from '@sentry/browser'; import { NAVIGATION_ROUTE_ID, PARAMS_KEY_BASE, + SENTRY_OP, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, } from '@sentry/conventions/attributes'; @@ -114,7 +115,7 @@ export function instrumentVueRouter( // Update the existing page load span with parametrized route information if (options.instrumentPageLoad && activePageLoadSpan) { - const existingAttributes = spanToJSON(activePageLoadSpan).data; + const existingAttributes = spanToJSON(activePageLoadSpan).attributes; if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { activePageLoadSpan.updateName(spanName); activePageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); @@ -167,7 +168,7 @@ function getActivePageLoadSpan(): Span | undefined { return undefined; } - const op = spanToJSON(rootSpan).op; + const op = spanToJSON(rootSpan).attributes[SENTRY_OP]; return op === 'pageload' ? rootSpan : undefined; }