From 598470e78d9031698562ea0477d650b645587ded Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 7 Aug 2026 17:32:09 +0200 Subject: [PATCH] feat(core)!: Return `StreamedSpanJSON` from `spanToJSON` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `spanToJSON` now returns the intermediate `StreamedSpanJSON` representation instead of the legacy `SpanJSON`. The previous behaviour is still available as `spanToStaticSpanJSON`, and the old `spanToStreamedSpanJSON` export is gone (it *is* `spanToJSON` now). Field mapping for call sites: `description` -> `name`, `data` -> `attributes`, `timestamp` -> `end_timestamp`, and `op`/`origin` move into `attributes` under `sentry.op` / `sentry.origin`. `status` is narrowed to `'ok' | 'error'`. `end_timestamp` is optional on `StreamedSpanJSON` and only required on `SerializedStreamedSpan`, so the guarantee we actually care about — every span we *send* has an end timestamp — is enforced at the single serialization boundary (`streamedSpanJsonToSerializedSpan`) rather than baked into every span representation. That keeps `end_timestamp` usable as the "has this span ended?" signal it already was, so the open-span checks in `browserTracingIntegration`, `idleSpan` and the React Router instrumentation stay simple field reads. --- .../async-spans/test.ts | 8 +- .../backgroundtab-custom/test.ts | 23 ++-- packages/angular/src/tracing.ts | 6 +- packages/angular/test/tracing.test.ts | 8 +- packages/astro/src/server/middleware.ts | 4 +- .../browser-utils/src/performance/entries.ts | 19 ++- .../src/performance/userTiming.ts | 6 +- .../browser-utils/src/web-vitals/spans.ts | 8 +- .../browser-utils/src/web-vitals/tracking.ts | 3 +- .../browser-utils/test/browser/utils.test.ts | 6 +- .../test/performance/browserMetrics.test.ts | 100 ++++++--------- .../test/performance/userTiming.test.ts | 18 +-- .../test/web-vitals/spans.test.ts | 17 ++- .../browser/src/integrations/graphqlClient.ts | 10 +- .../src/profiling/startProfileForSpan.ts | 11 +- packages/browser/src/profiling/utils.ts | 3 +- packages/browser/src/tracing/backgroundtab.ts | 6 +- .../src/tracing/browserTracingIntegration.ts | 11 +- packages/browser/src/tracing/linkedTraces.ts | 5 +- packages/browser/src/tracing/request.ts | 2 +- .../test/integrations/graphqlClient.test.ts | 24 ++-- .../test/profiling/integration.test.ts | 2 +- .../tracing/browserTracingIntegration.test.ts | 119 ++++++++++-------- .../browser/test/tracing/linkedTraces.test.ts | 8 +- .../test/integrations/bunHttpServer.test.ts | 6 +- .../core/src/integrations/conversationId.ts | 4 +- packages/core/src/shared-exports.ts | 3 +- .../src/tracing/dynamicSamplingContext.ts | 4 +- packages/core/src/tracing/idleSpan.ts | 11 +- packages/core/src/tracing/logSpans.ts | 12 +- packages/core/src/tracing/sentrySpan.ts | 11 +- .../core/src/tracing/spans/captureSpan.ts | 12 +- .../tracing/spans/spanJsonToStreamedSpan.ts | 2 +- packages/core/src/tracing/trace.ts | 12 +- packages/core/src/types/samplingcontext.ts | 4 +- packages/core/src/types/span.ts | 11 +- packages/core/src/utils/featureFlags.ts | 4 +- packages/core/src/utils/scopeData.ts | 4 +- packages/core/src/utils/spanUtils.ts | 16 ++- .../lib/integrations/conversationId.test.ts | 18 +-- packages/core/test/lib/tracing/errors.test.ts | 16 ++- .../core/test/lib/tracing/idleSpan.test.ts | 105 +++++++++------- .../tracing/sentryNonRecordingSpan.test.ts | 6 +- .../core/test/lib/tracing/sentrySpan.test.ts | 52 ++++---- .../core/test/lib/tracing/spanstatus.test.ts | 44 +++---- packages/core/test/lib/tracing/trace.test.ts | 43 ++++--- .../core/test/lib/utils/spanUtils.test.ts | 75 +++++++---- .../orchestrion-subscriber.test.ts | 110 ++++++++-------- .../utils/dropMiddlewareTunnelRequests.ts | 17 ++- .../utils/forkIsolationScopeForRootSpan.ts | 7 +- .../nextjs/src/common/utils/tracingUtils.ts | 10 +- packages/nextjs/src/edge/index.ts | 2 +- .../nextjs/src/server/handleOnSpanStart.ts | 4 +- .../src/server/vercelCronsMonitoring.ts | 2 +- .../src/server/vercelQueuesMonitoring.ts | 4 +- .../server/vercelQueuesMonitoring.test.ts | 2 +- .../src/integrations/tracing/redis/cache.ts | 19 ++- .../opentelemetry/src/applyOtelSpanData.ts | 10 +- .../src/utils/backfillStreamedSpanData.ts | 6 +- packages/opentelemetry/src/utils/mapStatus.ts | 4 +- .../src/utils/parseSpanDescription.ts | 14 +-- packages/opentelemetry/test/trace.test.ts | 10 +- .../opentelemetry/test/tracerProvider.test.ts | 37 +++--- packages/profiling-node/src/integration.ts | 5 +- .../profiling-node/src/spanProfileUtils.ts | 13 +- .../src/client/createClientInstrumentation.ts | 3 +- .../react-router/src/client/hydratedRouter.ts | 15 +-- .../createClientInstrumentation.test.ts | 6 +- .../test/client/hydratedRouter.test.ts | 22 ++-- packages/react/src/profiler.tsx | 4 +- .../instrumentation.tsx | 43 ++++--- .../src/reactrouter-compat-utils/utils.ts | 3 +- packages/react/src/reactrouter.tsx | 2 +- .../instrumentation.test.tsx | 66 +++++----- .../test/reactrouter-cross-usage.test.tsx | 3 + .../reactrouter-descendant-routes.test.tsx | 3 + packages/react/test/reactrouterv3.test.tsx | 3 + packages/react/test/reactrouterv4.test.tsx | 3 + packages/react/test/reactrouterv5.test.tsx | 3 + packages/react/test/reactrouterv6.test.tsx | 3 + packages/remix/src/server/instrumentServer.ts | 4 +- .../server/integrations/tracing-channel.ts | 2 +- .../test/server/tracing-channel-test-utils.ts | 1 + packages/replay-internal/src/replay.ts | 11 +- .../server-utils/src/ai/vercel-ai/index.ts | 6 +- packages/server-utils/src/graphql/utils.ts | 12 +- .../tracing-channel/google-genai.ts | 6 +- .../utils/setHttpServerSpanRouteAttribute.ts | 14 +-- .../src/vercel-ai/vercel-ai-dc-subscriber.ts | 11 +- .../test/ai/lib/tracing/workers-ai.test.ts | 6 +- .../test/mysql2/mysql2-dc-subscriber.test.ts | 54 ++++---- .../server-utils/test/tracing-channel.test.ts | 79 ++++++------ packages/solid/src/solidrouter.ts | 4 +- packages/solid/test/solidrouter.test.tsx | 25 ++-- .../server/withServerActionInstrumentation.ts | 2 +- .../test/client/solidrouter.test.tsx | 25 ++-- .../withServerActionInstrumentation.test.ts | 5 +- .../sveltekit/src/server-common/handle.ts | 10 +- .../integrations/svelteKitSpans.ts | 4 +- .../test/server-common/handle.test.ts | 38 +++--- .../src/server/globalMiddleware.ts | 5 +- .../src/server/routeParametrization.ts | 2 +- packages/vue/src/router.ts | 5 +- packages/vue/test/router.test.ts | 23 ++-- 104 files changed, 912 insertions(+), 802 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts index eaf14c124260..5a2f34abecba 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts @@ -23,14 +23,12 @@ sentryTest( expect(envelope).toBeDefined(); const firstWaitingSpanValue = await page.evaluate( - () => (window as unknown as WindowWithSpan).firstWaitingSpan.description, + () => (window as unknown as WindowWithSpan).firstWaitingSpan.name, ); const secondWaitingSpanName = await page.evaluate( - () => (window as unknown as WindowWithSpan).secondWaitingSpan.description, - ); - const thirdWaitingSpanName = await page.evaluate( - () => (window as unknown as WindowWithSpan).thirdWaitingSpan.description, + () => (window as unknown as WindowWithSpan).secondWaitingSpan.name, ); + const thirdWaitingSpanName = await page.evaluate(() => (window as unknown as WindowWithSpan).thirdWaitingSpan.name); expect(firstWaitingSpanValue).toBe('span 2'); expect(secondWaitingSpanName).toBe('span 1'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts index aefa640c2974..b6661c2638e2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { SpanJSON } from '@sentry/core'; +import type { StreamedSpanJSON } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -12,25 +12,26 @@ sentryTest('should finish a custom transaction when the page goes background', a await page.goto(url); await page.locator('#start-span').click(); - const spanJsonBefore: SpanJSON = await page.evaluate('window.getSpanJson()'); + const spanJsonBefore: StreamedSpanJSON = await page.evaluate('window.getSpanJson()'); const id_before = spanJsonBefore.span_id; - const description_before = spanJsonBefore.description; + const name_before = spanJsonBefore.name; const status_before = spanJsonBefore.status; - expect(description_before).toBe('test-span'); + expect(name_before).toBe('test-span'); expect(status_before).toBe('ok'); await page.locator('#go-background').click(); - const spanJsonAfter: SpanJSON = await page.evaluate('window.getSpanJson()'); + const spanJsonAfter: StreamedSpanJSON = await page.evaluate('window.getSpanJson()'); const id_after = spanJsonAfter.span_id; - const description_after = spanJsonAfter.description; - const status_after = spanJsonAfter.status; - const data_after = spanJsonAfter.data; + const name_after = spanJsonAfter.name; + const attributes_after = spanJsonAfter.attributes; expect(id_before).toBe(id_after); - expect(description_after).toBe(description_before); - expect(status_after).toBe('cancelled'); - expect(data_after?.['sentry.cancellation_reason']).toBe('document.hidden'); + expect(name_after).toBe(name_before); + // a cancelled span is reported as `ok`, with the raw status kept as an attribute + expect(spanJsonAfter.status).toBe('ok'); + expect(attributes_after['sentry.status.message']).toBeUndefined(); + expect(attributes_after['sentry.cancellation_reason']).toBe('document.hidden'); }); diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 627d2ba67490..47fd6014eb61 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -68,7 +68,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); @@ -76,7 +76,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]: filterCollectedUrl(absoluteUrl), [URL_PATH]: parseStringToURLObject(absoluteUrl)?.pathname, @@ -259,7 +259,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/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 9d34fba938eb..a460c5afb68a 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -84,7 +84,7 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ - data: expect.objectContaining({ + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.undefined.angular', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_TEMPLATE]: route, @@ -92,7 +92,7 @@ describe('Angular Tracing', () => { [URL_FULL]: expect.stringContaining('/users/123/'), [URL_PATH]: '/users/123/', }), - description: route, + name: route, }), ); }); @@ -109,11 +109,11 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ - data: { + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'sample-source', }, - description: 'initial-span-name', + name: 'initial-span-name', }), ); }); diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 60924dd332cb..74fc38ff5612 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, @@ -98,7 +98,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/performance/entries.ts b/packages/browser-utils/src/performance/entries.ts index 343d45fed333..9a5d504d0d05 100644 --- a/packages/browser-utils/src/performance/entries.ts +++ b/packages/browser-utils/src/performance/entries.ts @@ -73,13 +73,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 @@ -117,7 +117,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. @@ -220,7 +223,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); @@ -232,7 +235,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; } @@ -479,7 +486,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/performance/userTiming.ts b/packages/browser-utils/src/performance/userTiming.ts index 28c697cd60cc..02d6fc25694d 100644 --- a/packages/browser-utils/src/performance/userTiming.ts +++ b/packages/browser-utils/src/performance/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/web-vitals/spans.ts b/packages/browser-utils/src/web-vitals/spans.ts index e6963142cbb4..f6ad68b7ec1c 100644 --- a/packages/browser-utils/src/web-vitals/spans.ts +++ b/packages/browser-utils/src/web-vitals/spans.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'; @@ -105,7 +105,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; } @@ -343,9 +343,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-utils/src/web-vitals/tracking.ts b/packages/browser-utils/src/web-vitals/tracking.ts index 824953e9d4c4..607eb3830809 100644 --- a/packages/browser-utils/src/web-vitals/tracking.ts +++ b/packages/browser-utils/src/web-vitals/tracking.ts @@ -1,5 +1,6 @@ import type { Client, Measurements, Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, debug, setMeasurement, spanToJSON } from '@sentry/core'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../debug-build'; import { htmlTreeAsString } from '../htmlTreeAsString'; import { @@ -150,7 +151,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) { diff --git a/packages/browser-utils/test/browser/utils.test.ts b/packages/browser-utils/test/browser/utils.test.ts index ec0457512cf7..b676a3265702 100644 --- a/packages/browser-utils/test/browser/utils.test.ts +++ b/packages/browser-utils/test/browser/utils.test.ts @@ -26,9 +26,9 @@ describe('startAndEndSpan()', () => { expect(span).toBeDefined(); expect(span).toBeInstanceOf(SentrySpan); - expect(spanToJSON(span).description).toBe('evaluation'); - expect(spanToJSON(span).op).toBe('script'); - expect(spanToJSON(span).op).toBe('script'); + expect(spanToJSON(span).name).toBe('evaluation'); + expect(spanToJSON(span).attributes['sentry.op']).toBe('script'); + expect(spanToJSON(span).attributes['sentry.op']).toBe('script'); }); it('adjusts the start timestamp if child span starts before transaction', () => { diff --git a/packages/browser-utils/test/performance/browserMetrics.test.ts b/packages/browser-utils/test/performance/browserMetrics.test.ts index d4ff3e591e41..4517bcdd3f89 100644 --- a/packages/browser-utils/test/performance/browserMetrics.test.ts +++ b/packages/browser-utils/test/performance/browserMetrics.test.ts @@ -134,8 +134,8 @@ describe('addWebVitalsToSpan', () => { spanStreamingEnabled: true, }); - expect(spanToJSON(nextPageloadSpan).data['browser.web_vital.fp.value']).toBeUndefined(); - expect(spanToJSON(nextPageloadSpan).data['browser.web_vital.fcp.value']).toBeUndefined(); + expect(spanToJSON(nextPageloadSpan).attributes['browser.web_vital.fp.value']).toBeUndefined(); + expect(spanToJSON(nextPageloadSpan).attributes['browser.web_vital.fcp.value']).toBeUndefined(); }); }); @@ -240,12 +240,10 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - description: '/assets/to/css', + name: '/assets/to/css', start_timestamp: timeOrigin + startTime, - timestamp: timeOrigin + startTime + duration, - op: 'resource.css', - origin: 'auto.resource.browser.metrics', - data: { + end_timestamp: timeOrigin + startTime + duration, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -292,8 +290,8 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); const json = spanToJSON(spans[0]!); - expect(json.description).toBe('/assets/app.js?v=42#main'); - expect(json.data['url.full']).toBe('https://example.com/assets/app.js?v=42#main'); + expect(json.name).toBe('/assets/app.js?v=42#main'); + expect(json.attributes['url.full']).toBe('https://example.com/assets/app.js?v=42#main'); }); it('sets url.full to the full cross-origin URL', () => { @@ -312,9 +310,9 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); const json = spanToJSON(spans[0]!); - expect(json.description).toBe('https://cdn.example.org/static/logo.png'); - expect(json.data['url.full']).toBe('https://cdn.example.org/static/logo.png'); - expect(json.data['url.same_origin']).toBe(false); + expect(json.name).toBe('https://cdn.example.org/static/logo.png'); + expect(json.attributes['url.full']).toBe('https://cdn.example.org/static/logo.png'); + expect(json.attributes['url.same_origin']).toBe(false); }); it('creates a variety of resource spans', () => { @@ -355,7 +353,7 @@ describe('_addResourceSpans', () => { _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465); expect(spans).toHaveLength(i + 1); - expect(spanToJSON(spans[i]!)).toEqual(expect.objectContaining({ op })); + expect(spanToJSON(spans[i]!).attributes).toEqual(expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op })); } }); @@ -400,7 +398,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(table.length - ignoredResourceSpans.length); const spanOps = new Set( spans.map(s => { - return spanToJSON(s).op; + return spanToJSON(s).attributes['sentry.op']; }), ); expect(spanOps).toEqual(new Set(['resource.css', 'resource.image'])); @@ -427,7 +425,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - data: expect.objectContaining({ + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -465,7 +463,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - data: expect.objectContaining({ + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', @@ -475,10 +473,8 @@ describe('_addResourceSpans', () => { ['network.protocol.name']: 'http', ['network.protocol.version']: '3', }), - description: '/assets/to/css', - timestamp: 468, - op: 'resource.css', - origin: 'auto.resource.browser.metrics', + name: '/assets/to/css', + end_timestamp: 468, start_timestamp: 445, }), ); @@ -518,7 +514,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - data: { + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', @@ -541,10 +537,8 @@ describe('_addResourceSpans', () => { 'http.request.time_to_first_byte': 1.008, 'http.request.worker_start': expect.any(Number), }, - description: '/assets/to/css', - timestamp: 468, - op: 'resource.css', - origin: 'auto.resource.browser.metrics', + name: '/assets/to/css', + end_timestamp: 468, start_timestamp: 445, }), ); @@ -573,7 +567,7 @@ describe('_addResourceSpans', () => { _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); expect(spans).toHaveLength(1); - expect(spanToJSON(spans[0]!).data).toMatchObject({ 'http.response_delivery_type': deliveryType }); + expect(spanToJSON(spans[0]!).attributes).toMatchObject({ 'http.response_delivery_type': deliveryType }); }, ); }); @@ -656,102 +650,84 @@ describe('_addNavigationSpans', () => { expect(spans.map(spanToJSON)).toEqual( expect.arrayContaining([ expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.dom_content_loaded_event', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.dom_content_loaded_event', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.load_event', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.load_event', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.connect', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.connect', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.tls_ssl', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.tls_ssl', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.cache', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.cache', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.dns', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.dns', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.request', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.request', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'sentry.op': 'browser.response', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.response', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), expect.objectContaining({ - data: { + attributes: { 'http.redirect_count': 2, 'sentry.op': 'browser.redirect', 'sentry.origin': 'auto.ui.browser.metrics', }, - description: 'https://santry.com/test', - op: 'browser.redirect', - origin: 'auto.ui.browser.metrics', + name: 'https://santry.com/test', parent_span_id, trace_id, }), diff --git a/packages/browser-utils/test/performance/userTiming.test.ts b/packages/browser-utils/test/performance/userTiming.test.ts index 0e9a8d64eec4..f2bd5e2f3684 100644 --- a/packages/browser-utils/test/performance/userTiming.test.ts +++ b/packages/browser-utils/test/performance/userTiming.test.ts @@ -42,10 +42,10 @@ describe('userTimingIntegration', () => { client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(2); - expect(spans.map(span => spanToJSON(span).description)).toEqual(['app-ready', 'hydrate']); - expect(spans.map(span => spanToJSON(span).op)).toEqual(['mark', 'measure']); - expect(spanToJSON(spans[0]!).timestamp).toBe(spanToJSON(spans[0]!).start_timestamp); - expect(spanToJSON(spans[1]!).timestamp! - spanToJSON(spans[1]!).start_timestamp).toBeCloseTo(0.025); + expect(spans.map(span => spanToJSON(span).name)).toEqual(['app-ready', 'hydrate']); + expect(spans.map(span => spanToJSON(span).attributes['sentry.op'])).toEqual(['mark', 'measure']); + expect(spanToJSON(spans[0]!).end_timestamp).toBe(spanToJSON(spans[0]!).start_timestamp); + expect(spanToJSON(spans[1]!).end_timestamp - spanToJSON(spans[1]!).start_timestamp).toBeCloseTo(0.025); expect(spanToJSON(spans[1]!).parent_span_id).toBe(parentSpan.spanContext().spanId); }); @@ -66,7 +66,7 @@ describe('userTimingIntegration', () => { ); expect(spans).toHaveLength(2); - expect(spans.map(span => spanToJSON(span).description)).toEqual(['initial-render', 'route-render']); + expect(spans.map(span => spanToJSON(span).name)).toEqual(['initial-render', 'route-render']); }); it('reads the latest entries immediately before the segment ends', () => { @@ -77,7 +77,7 @@ describe('userTimingIntegration', () => { client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(1); - expect(spanToJSON(spans[0]!).description).toBe('last-moment-work'); + expect(spanToJSON(spans[0]!).name).toBe('last-moment-work'); }); it('does not capture entries for unrelated idle spans', () => { @@ -103,7 +103,7 @@ describe('userTimingIntegration', () => { client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(2); - expect(spans.map(span => spanToJSON(span).description)).toEqual(['application-mark', 'application-render']); + expect(spans.map(span => spanToJSON(span).name)).toEqual(['application-mark', 'application-render']); }); it('does not attach entries preceding a navigation span', () => { @@ -123,7 +123,7 @@ describe('userTimingIntegration', () => { client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(1); - expect(spanToJSON(spans[0]!).description).toBe('current-route'); + expect(spanToJSON(spans[0]!).name).toBe('current-route'); }); }); @@ -159,7 +159,7 @@ describe('_addUserTimingSpan', () => { _addUserTimingSpan(parentSpan, entry, 0.012, 0.01, 100, 0, []); expect(spans).toHaveLength(1); - expect(spanToJSON(spans[0]!).data).toEqual({ + expect(spanToJSON(spans[0]!).attributes).toEqual({ 'sentry.browser.measure.detail.phase': 'client', 'sentry.browser.measure.detail.counts': '{"components":4}', 'sentry.op': 'measure', diff --git a/packages/browser-utils/test/web-vitals/spans.test.ts b/packages/browser-utils/test/web-vitals/spans.test.ts index ffb6e08d5b71..d23949c3307d 100644 --- a/packages/browser-utils/test/web-vitals/spans.test.ts +++ b/packages/browser-utils/test/web-vitals/spans.test.ts @@ -24,7 +24,6 @@ vi.mock('@sentry/core', async () => { getActiveSpan: vi.fn(), getRootSpan: vi.fn(), spanToJSON: vi.fn(), - spanToStreamedSpanJSON: vi.fn(), }; }); @@ -64,7 +63,7 @@ describe('_emitWebVitalSpan', () => { beforeEach(() => { vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ attributes: {} } as any); + vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: {} } as any); vi.mocked(SentryCore.getClient).mockReturnValue({ getIntegrationByName: () => undefined } as any); }); @@ -187,7 +186,7 @@ describe('_emitWebVitalSpan', () => { it('includes pageload span id when parentSpan is a pageload span', () => { const mockPageloadSpan = createMockPageloadSpan('abc123'); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ + vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: { 'sentry.op': 'pageload' }, } as any); @@ -213,7 +212,7 @@ describe('_emitWebVitalSpan', () => { it('does not include pageload span id when parentSpan is not a pageload span', () => { const mockNonPageloadSpan = createMockPageloadSpan('xyz789'); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ + vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: { 'sentry.op': 'ui.interaction.click' }, } as any); @@ -308,7 +307,7 @@ describe('_sendLcpSpan', () => { vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ + vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: { 'sentry.op': 'pageload' }, } as any); }); @@ -395,7 +394,7 @@ describe('_sendClsSpan', () => { vi.mocked(SentryCore.timestampInSeconds).mockReturnValue(1.5); vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ + vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: { 'sentry.op': 'pageload' }, } as any); }); @@ -478,7 +477,7 @@ describe('_sendInpSpan', () => { vi.mocked(htmlTreeAsString).mockReturnValue('