diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts index f990f2bd3c3e..b6fae84d4a54 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts @@ -168,14 +168,14 @@ export async function testCacheOriginOnRuntimeCachedPage( const [originTraceId, originSpanId, originSampled] = cacheOrigin1?.split('-') ?? []; const cacheOriginLink = { trace_id: serverSpan1.trace_id, - span_id: originSpanId, + span_id: serverSpan1.span_id, sampled: true, attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'cache_origin' } }, }; - await test.step('The cache origin tag holds the sampled render of the 1. request', () => { + await test.step('The cache origin tag holds the `http.server` root span of the 1. request', () => { expect(originTraceId).toBe(serverSpan1.trace_id); - expect(originSpanId).toMatch(/^[a-f0-9]{16}$/); + expect(originSpanId).toBe(serverSpan1.span_id); expect(originSampled).toBe('1'); expect(cacheOrigin2).toBe(cacheOrigin1); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.cached-html.test.ts index e7e209b35468..14e714ba8f13 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.cached-html.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.cached-html.test.ts @@ -161,14 +161,14 @@ export async function testCacheOriginOnRuntimeCachedPage( const [originTraceId, originSpanId, originSampled] = cacheOrigin1?.split('-') ?? []; const cacheOriginLink = { trace_id: serverSpan1.trace_id, - span_id: originSpanId, + span_id: serverSpan1.span_id, sampled: true, attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'cache_origin' } }, }; - await test.step('The cache origin tag holds the sampled render of the 1. request', () => { + await test.step('The cache origin tag holds the `http.server` root span of the 1. request', () => { expect(originTraceId).toBe(serverSpan1.trace_id); - expect(originSpanId).toMatch(/^[a-f0-9]{16}$/); + expect(originSpanId).toBe(serverSpan1.span_id); expect(originSampled).toBe('1'); expect(cacheOrigin2).toBe(cacheOrigin1); }); diff --git a/packages/core/src/browser.ts b/packages/core/src/browser.ts index 450a20d1b38c..1a16c1165894 100644 --- a/packages/core/src/browser.ts +++ b/packages/core/src/browser.ts @@ -12,6 +12,7 @@ export { } from './tracing/browserSpanApi'; export { startIdleSpan } from './tracing/idleSpan'; +export { _INTERNAL_withCacheOrigin } from './utils/cacheOrigin'; export type { XhrBreadcrumbData, XhrBreadcrumbHint } from './types/breadcrumb'; export type { BrowserClientReplayOptions } from './types/browseroptions'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5e01bde701e6..11fd3f6cad1f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -134,6 +134,7 @@ export { applySdkMetadata } from './utils/sdkMetadata'; export { getTraceData } from './utils/traceData'; export { matchesTracePropagationTargets, shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; export { getTraceMetaTags } from './utils/meta'; +export { _INTERNAL_CACHE_ORIGIN_META_TAG_NAME } from './utils/cacheOrigin'; export { debounce } from './utils/debounce'; export { uniq } from './utils/array'; export { makeWeakRef, derefWeakRef } from './utils/weakRef'; diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index f7f74af63dee..50dfb6639b05 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -35,6 +35,7 @@ export { wrapMcpServerWithSentry } from './integrations/mcp-server'; export { isNodeEnv, loadModule } from './utils/node'; export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; export { vercelWaitUntil } from './utils/vercelWaitUntil'; +export { _INTERNAL_getCacheOriginMetaTag, _INTERNAL_getRootSpanSentryTrace } from './utils/cacheOrigin'; export { getReleaseFromCommitEnv as _INTERNAL_getReleaseFromCommitEnv, readCommitEnvVar as _INTERNAL_readCommitEnvVar, diff --git a/packages/core/src/utils/cacheOrigin.ts b/packages/core/src/utils/cacheOrigin.ts new file mode 100644 index 000000000000..d0e08f9dad26 --- /dev/null +++ b/packages/core/src/utils/cacheOrigin.ts @@ -0,0 +1,72 @@ +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import type { SpanLink } from '../types/link'; +import type { StartSpanOptions } from '../types/startSpanOptions'; +import { getActiveSpan, getRootSpan, TRACE_FLAG_SAMPLED } from './spanUtils'; +import { getTraceData } from './traceData'; +import { extractTraceparentData } from './tracing'; +import { GLOBAL_OBJ } from './worldwide'; + +/** + * Replaces `sentry-trace` and `baggage` in a cached or prerendered page, so that no visitor continues the + * trace of the render. + * + * - No tag: the server rendered the page for this request, or rendered no content (a client-side shell) + * - Empty `content`: there is no trace of the render to link, for example for a page prerendered in the build + * - A `sentry-trace` value: the client links the render + */ +export const _INTERNAL_CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; + +/** Returns the `sentry-cache-origin` meta tag. Without `renderTrace`, the tag is empty. */ +export function _INTERNAL_getCacheOriginMetaTag(renderTrace: string | undefined): string { + return ``; +} + +/** + * Returns the `sentry-trace` of the request's root span, which a cached page links as its render. Unlike + * `getTraceData()`, it ignores an active child span, such as SvelteKit's `sveltekit.resolve`. + */ +export function _INTERNAL_getRootSpanSentryTrace(): string | undefined { + const activeSpan = getActiveSpan(); + return getTraceData({ span: activeSpan && getRootSpan(activeSpan) })['sentry-trace']; +} + +/** + * Sets `page.server_rendering.type` on a pageload whose document has a `sentry-cache-origin` tag, even an + * empty one. Links the render if the tag holds the `sentry-trace` of a sampled render. Pass `undefined` as + * `serverRenderingType` while the type is not known. + * + * Call it before the span starts, so `tracesSampler` and `ignoreSpans` see the attribute. + */ +export function _INTERNAL_withCacheOrigin( + spanOptions: StartSpanOptions, + serverRenderingType: 'static' | 'partially_static' | undefined, +): StartSpanOptions { + const renderTrace = (GLOBAL_OBJ as { document?: Document }).document + ?.querySelector(`meta[name="${_INTERNAL_CACHE_ORIGIN_META_TAG_NAME}"]`) + ?.getAttribute('content'); + if (renderTrace == null) { + return spanOptions; + } + + const link = getSampledRenderLink(renderTrace); + return { + ...spanOptions, + attributes: { + ...spanOptions.attributes, + ...(serverRenderingType && { [PAGE_SERVER_RENDERING_TYPE]: serverRenderingType }), + }, + ...(link && { links: [...(spanOptions.links ?? []), link] }), + }; +} + +// Only a sampled render sent spans, so a link to any other render would point at nothing. +function getSampledRenderLink(renderTrace: string): SpanLink | undefined { + const traceparent = extractTraceparentData(renderTrace); + if (!traceparent?.traceId || !traceparent.parentSpanId || !traceparent.parentSampled) { + return undefined; + } + return { + context: { traceId: traceparent.traceId, spanId: traceparent.parentSpanId, traceFlags: TRACE_FLAG_SAMPLED }, + attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, + }; +} diff --git a/packages/core/test/lib/utils/cacheOrigin.test.ts b/packages/core/test/lib/utils/cacheOrigin.test.ts new file mode 100644 index 000000000000..e4cd8572ccbb --- /dev/null +++ b/packages/core/test/lib/utils/cacheOrigin.test.ts @@ -0,0 +1,153 @@ +/** + * @vitest-environment jsdom + */ + +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { setCurrentClient, startSpan } from '../../../src'; +import type { StartSpanOptions } from '../../../src/types/startSpanOptions'; +import { + _INTERNAL_getCacheOriginMetaTag, + _INTERNAL_getRootSpanSentryTrace, + _INTERNAL_withCacheOrigin, +} from '../../../src/utils/cacheOrigin'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; +import { resetGlobals } from '../../testutils'; + +const TRACE_ID = '0b37aaf4f0d74c0e80875099e7d7ca68'; +const SPAN_ID = 'bc3a52360214dcf3'; + +const spanOptions: StartSpanOptions = { name: '/pricing', attributes: { [SENTRY_OP]: PAGELOAD } }; + +const cacheOriginLink = { + context: { traceId: TRACE_ID, spanId: SPAN_ID, traceFlags: 1 }, + attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, +}; + +function addCacheOriginTag(content: string): void { + const tag = document.createElement('meta'); + tag.name = 'sentry-cache-origin'; + tag.content = content; + document.head.appendChild(tag); +} + +describe('_INTERNAL_getCacheOriginMetaTag', () => { + it('holds the sentry-trace value of the render', () => { + expect(_INTERNAL_getCacheOriginMetaTag(`${TRACE_ID}-${SPAN_ID}-1`)).toBe( + ``, + ); + }); + + it('is empty without a sentry-trace value', () => { + expect(_INTERNAL_getCacheOriginMetaTag(undefined)).toBe(''); + }); +}); + +describe('_INTERNAL_getRootSpanSentryTrace', () => { + beforeEach(() => { + resetGlobals(); + const client = new TestClient( + getDefaultTestClientOptions({ dsn: 'https://123@sentry.io/42', tracesSampleRate: 1 }), + ); + setCurrentClient(client); + client.init(); + }); + + it('returns the sentry-trace of the root span while a child span is active', () => { + startSpan({ name: 'GET /pricing' }, rootSpan => + startSpan({ name: 'sveltekit.resolve' }, () => { + const { traceId, spanId } = rootSpan.spanContext(); + expect(_INTERNAL_getRootSpanSentryTrace()).toBe(`${traceId}-${spanId}-1`); + }), + ); + }); +}); + +describe('_INTERNAL_withCacheOrigin', () => { + beforeEach(() => { + document.head.innerHTML = ''; + }); + + it('returns the span options unchanged for a page without a cache origin tag', () => { + expect(_INTERNAL_withCacheOrigin(spanOptions, 'static')).toBe(spanOptions); + }); + + // HTML requires `content` on a named meta tag, so the SDK cannot have written this one. + it('returns the span options unchanged for a cache origin tag without `content`', () => { + const tag = document.createElement('meta'); + tag.name = 'sentry-cache-origin'; + document.head.appendChild(tag); + + expect(_INTERNAL_withCacheOrigin(spanOptions, 'static')).toBe(spanOptions); + }); + + it('sets the rendering type and links a sampled render', () => { + addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); + + expect(_INTERNAL_withCacheOrigin(spanOptions, 'partially_static')).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'partially_static' }, + links: [cacheOriginLink], + }); + }); + + it.each([ + { kind: 'an empty tag (prerendered during the build)', content: '' }, + { kind: 'an unsampled render', content: `${TRACE_ID}-${SPAN_ID}-0` }, + { kind: 'a render without a sampling decision', content: `${TRACE_ID}-${SPAN_ID}` }, + { kind: 'a malformed value', content: 'not-a-trace' }, + ])('sets the rendering type without a link for $kind', ({ content }) => { + addCacheOriginTag(content); + + expect(_INTERNAL_withCacheOrigin(spanOptions, 'static')).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + }); + }); + + it('links the render but leaves the rendering type unset without one', () => { + addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); + + expect(_INTERNAL_withCacheOrigin(spanOptions, undefined)).toEqual({ ...spanOptions, links: [cacheOriginLink] }); + }); + + it('keeps the links that the span options already have', () => { + addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); + const previousTraceLink = { + context: { traceId: '0d5c7c7a6f0b4a57a3f1e9c2b8d4e6f1', spanId: 'b1a2c3d4e5f60718', traceFlags: 1 }, + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, + }; + + expect(_INTERNAL_withCacheOrigin({ ...spanOptions, links: [previousTraceLink] }, 'static').links).toEqual([ + previousTraceLink, + cacheOriginLink, + ]); + }); +}); + +// The server writes the tag and the client reads it, so the HTML format must stay readable by the client. +describe('the tag that _INTERNAL_getCacheOriginMetaTag writes', () => { + beforeEach(() => { + document.head.innerHTML = ''; + }); + + it('makes _INTERNAL_withCacheOrigin link the render', () => { + document.head.innerHTML = _INTERNAL_getCacheOriginMetaTag(`${TRACE_ID}-${SPAN_ID}-1`); + + expect(_INTERNAL_withCacheOrigin(spanOptions, 'static')).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + links: [cacheOriginLink], + }); + }); + + it('makes _INTERNAL_withCacheOrigin set the rendering type without a link when it is empty', () => { + document.head.innerHTML = _INTERNAL_getCacheOriginMetaTag(undefined); + + expect(_INTERNAL_withCacheOrigin(spanOptions, 'static')).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + }); + }); +}); diff --git a/packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts index 20e7a0e727e0..da1d0f1a92ab 100644 --- a/packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts @@ -14,15 +14,16 @@ import { hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK, } from '@sentry/core'; +import { _INTERNAL_withCacheOrigin } from '@sentry/core/browser'; import { resolveCurrentRoute, startBrowserTracingPageLoadSpan, WINDOW } from '@sentry/react'; import { stripTrailingSlash } from './parameterization'; import type { TraceMetaTagValues, TraceMetaTagWait } from './traceMetaTags'; import { addTraceMetaTagLink, canWaitForTraceMetaTag, + getServerRenderingType, readTraceMetaTags, waitForTraceMetaTag, - withCacheOrigin, } from './traceMetaTags'; /** @@ -52,7 +53,11 @@ function startPageloadSpan( documentUrl: string, traceMetaTags?: TraceMetaTagValues, ): Span | undefined { - const span = startBrowserTracingPageLoadSpan(client, withCacheOrigin(spanOptions, traceMetaTags), traceMetaTags); + const span = startBrowserTracingPageLoadSpan( + client, + _INTERNAL_withCacheOrigin(spanOptions, getServerRenderingType(traceMetaTags)), + traceMetaTags, + ); const scope = getCurrentScope(); const { normalizedRequest } = scope.getScopeData().sdkProcessingMetadata; if (normalizedRequest) { diff --git a/packages/nextjs/src/client/routing/traceMetaTags.ts b/packages/nextjs/src/client/routing/traceMetaTags.ts index c276a8cef03e..8980d781862a 100644 --- a/packages/nextjs/src/client/routing/traceMetaTags.ts +++ b/packages/nextjs/src/client/routing/traceMetaTags.ts @@ -1,16 +1,12 @@ -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import type { Span, SpanLink, StartSpanOptions } from '@sentry/core'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import type { Span } from '@sentry/core'; import { extractTraceparentData, GLOBAL_OBJ } from '@sentry/core'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../../common/traceMetaTags'; import { WINDOW } from '@sentry/react'; // `@opentelemetry/api` `TraceFlags`, inlined because core does not export its copies. const TRACE_FLAG_SAMPLED = 0x1; const TRACE_FLAG_NONE = 0x0; -const SERVER_REQUEST_LINK_TYPE = 'server_request'; -const CACHE_ORIGIN_LINK_TYPE = 'cache_origin'; - export interface TraceMetaTagValues { sentryTrace: string; baggage: string | undefined; @@ -132,65 +128,34 @@ export function waitForTraceMetaTag( return { giveUp: () => settle(onGiveUp) }; } -/** The span link to the span a `sentry-trace` value describes, if the value is one. */ -function traceMetaTagLink(sentryTrace: string, linkType: string): SpanLink | undefined { +/** + * Adds a span link to the span described by a `sentry-trace` value. Used when the tag arrives after + * the pageload span exists, which can only link the server request, not continue it. + */ +export function addTraceMetaTagLink(span: Span, { sentryTrace }: TraceMetaTagValues): void { const traceparent = extractTraceparentData(sentryTrace); if (!traceparent?.traceId || !traceparent.parentSpanId) { - return undefined; + return; } - return { + span.addLink({ context: { traceId: traceparent.traceId, spanId: traceparent.parentSpanId, traceFlags: traceparent.parentSampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, }, - attributes: { [SENTRY_LINK_TYPE]: linkType }, - }; -} - -/** - * Adds a span link to the span described by a `sentry-trace` value. Used when the tag arrives after - * the pageload span exists, which can only link the server request, not continue it. - */ -export function addTraceMetaTagLink(span: Span, { sentryTrace }: TraceMetaTagValues): void { - const link = traceMetaTagLink(sentryTrace, SERVER_REQUEST_LINK_TYPE); - if (link) { - span.addLink(link); - } + attributes: { [SENTRY_LINK_TYPE]: 'server_request' }, + }); } /** * How the server rendered a document with a `sentry-cache-origin` tag. With trace meta tags of this * request, only the shell was reused. Unset while a resumed part can still bring those tags. */ -function getServerRenderingType(traceMetaTags: TraceMetaTagValues | undefined): string | undefined { +export function getServerRenderingType( + traceMetaTags: TraceMetaTagValues | undefined, +): 'static' | 'partially_static' | undefined { if (traceMetaTags) { return 'partially_static'; } return canWaitForTraceMetaTag() ? undefined : 'static'; } - -/** - * Marks a pageload whose document carries a `sentry-cache-origin` tag with how the server rendered - * it, and links the render that the page reuses. Applied at span start, so `tracesSampler` and - * `ignoreSpans` see both. Like a `use cache` hit, the pageload links only a sampled render. - */ -export function withCacheOrigin(spanOptions: StartSpanOptions, traceMetaTags?: TraceMetaTagValues): StartSpanOptions { - const document = getDocument(); - const cacheOrigin = document && lastMetaContent(document, CACHE_ORIGIN_META_TAG_NAME); - if (!cacheOrigin) { - return spanOptions; - } - - const link = traceMetaTagLink(cacheOrigin, CACHE_ORIGIN_LINK_TYPE); - const isSampled = link?.context.traceFlags === TRACE_FLAG_SAMPLED; - const serverRenderingType = getServerRenderingType(traceMetaTags); - return { - ...spanOptions, - attributes: { - ...spanOptions.attributes, - ...(serverRenderingType && { [PAGE_SERVER_RENDERING_TYPE]: serverRenderingType }), - }, - links: link && isSampled ? [...(spanOptions.links ?? []), link] : spanOptions.links, - }; -} diff --git a/packages/nextjs/src/common/traceMetaTags.ts b/packages/nextjs/src/common/traceMetaTags.ts deleted file mode 100644 index 90ae909bf962..000000000000 --- a/packages/nextjs/src/common/traceMetaTags.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Name of the meta tag the server writes in place of `sentry-trace` while Next.js prerenders a document. - * Its content is the `sentry-trace` value of the prerender. The client turns it into a `cache_origin` - * link and never continues it, because the document is served to every later visitor. - */ -export const CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index 91e33d4d85c2..b9b9062e95a6 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -1,5 +1,4 @@ -import { CACHE_ORIGIN_META_TAG_NAME } from '../../common/traceMetaTags'; -import { debug, isMatchingPattern, parseSemver } from '@sentry/core'; +import { _INTERNAL_CACHE_ORIGIN_META_TAG_NAME, debug, isMatchingPattern, parseSemver } from '@sentry/core'; import { getSentryRelease } from '@sentry/node'; import * as fs from 'fs'; import * as path from 'path'; @@ -188,7 +187,7 @@ export function maybeSetClientTraceMetadataOption( incomingUserNextConfigObject.experimental.clientTraceMetadata = [ 'baggage', 'sentry-trace', - CACHE_ORIGIN_META_TAG_NAME, + _INTERNAL_CACHE_ORIGIN_META_TAG_NAME, ...(incomingUserNextConfigObject.experimental?.clientTraceMetadata || []), ]; } diff --git a/packages/nextjs/src/server/nextSentryPropagator.ts b/packages/nextjs/src/server/nextSentryPropagator.ts index b583beff3465..6df02637197e 100644 --- a/packages/nextjs/src/server/nextSentryPropagator.ts +++ b/packages/nextjs/src/server/nextSentryPropagator.ts @@ -1,7 +1,7 @@ import type { Context, TextMapSetter, TracerProvider } from '@opentelemetry/api'; import { defaultTextMapSetter, propagation } from '@opentelemetry/api'; +import { _INTERNAL_CACHE_ORIGIN_META_TAG_NAME } from '@sentry/core'; import { SentryPropagator } from '@sentry/opentelemetry'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; import { isPrerenderContext, markPrerendersOnTracerProvider } from './prerenderContext'; /** @@ -28,7 +28,7 @@ export class NextSentryPropagator extends SentryPropagator { super.inject(ctx, values, defaultTextMapSetter); const sentryTrace = values['sentry-trace']; if (sentryTrace) { - setter.set(carrier, CACHE_ORIGIN_META_TAG_NAME, sentryTrace); + setter.set(carrier, _INTERNAL_CACHE_ORIGIN_META_TAG_NAME, sentryTrace); } } } diff --git a/packages/nextjs/test/client/traceMetaTags.test.ts b/packages/nextjs/test/client/traceMetaTags.test.ts index ec821cc2312f..6710468e0fbf 100644 --- a/packages/nextjs/test/client/traceMetaTags.test.ts +++ b/packages/nextjs/test/client/traceMetaTags.test.ts @@ -1,14 +1,13 @@ // @vitest-environment jsdom -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; -import { PAGELOAD } from '@sentry/conventions/op'; -import type { Span, StartSpanOptions } from '@sentry/core'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import type { Span } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { addTraceMetaTagLink, canWaitForTraceMetaTag, + getServerRenderingType, readTraceMetaTags, waitForTraceMetaTag, - withCacheOrigin, } from '../../src/client/routing/traceMetaTags'; const HEAD_TRACE = '11111111111111111111111111111111-1111111111111111-1'; @@ -222,62 +221,20 @@ describe('addTraceMetaTagLink', () => { }); }); -describe('withCacheOrigin', () => { - const PAGELOAD_OPTIONS: StartSpanOptions = { name: '/products/:id', attributes: { [SENTRY_OP]: PAGELOAD } }; - const PRERENDER_LINK = { - context: { traceId: '11111111111111111111111111111111', spanId: '1111111111111111', traceFlags: 1 }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }; - - it('returns the options unchanged for a document without a sentry-cache-origin tag', () => { - appendMeta(document.head, 'sentry-trace', HEAD_TRACE); - - expect(withCacheOrigin(PAGELOAD_OPTIONS, { sentryTrace: HEAD_TRACE, baggage: undefined })).toBe(PAGELOAD_OPTIONS); - }); - - it('marks a fully reused page as static and links a sampled render as cache origin', () => { - appendMeta(document.head, 'sentry-cache-origin', HEAD_TRACE); - - expect(withCacheOrigin(PAGELOAD_OPTIONS)).toEqual({ - name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - links: [PRERENDER_LINK], - }); +describe('getServerRenderingType', () => { + it('is static for a fully reused page', () => { + expect(getServerRenderingType(undefined)).toBe('static'); }); - it('marks a page as partially static when the pageload continues the trace of this request', () => { - appendMeta(document.head, 'sentry-cache-origin', HEAD_TRACE); - - expect(withCacheOrigin(PAGELOAD_OPTIONS, { sentryTrace: BODY_TRACE, baggage: undefined })).toEqual({ - name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'partially_static' }, - links: [PRERENDER_LINK], - }); + it('is partially static when the pageload continues the trace of this request', () => { + expect(getServerRenderingType({ sentryTrace: BODY_TRACE, baggage: undefined })).toBe('partially_static'); }); // A navigation or the wait cap can start the pageload before the resumed part arrives. - it('leaves the type unset while a resumed part can still bring the trace of this request', () => { - appendMeta(document.head, 'sentry-cache-origin', HEAD_TRACE); + it('is unset while a resumed part can still bring the trace of this request', () => { setReadyState('loading'); vi.spyOn(performance, 'getEntriesByType').mockReturnValue([]); - expect(withCacheOrigin(PAGELOAD_OPTIONS)).toEqual({ - name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD }, - links: [PRERENDER_LINK], - }); - }); - - it.each([ - { tag: 'holds an unsampled trace', content: '11111111111111111111111111111111-1111111111111111-0' }, - { tag: 'holds a trace without a sampling decision', content: '11111111111111111111111111111111-1111111111111111' }, - { tag: 'holds no sentry-trace value', content: 'prerendered' }, - ])('marks the page without a link when the tag $tag', ({ content }) => { - appendMeta(document.head, 'sentry-cache-origin', content); - - expect(withCacheOrigin(PAGELOAD_OPTIONS)).toEqual({ - name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - }); + expect(getServerRenderingType(undefined)).toBeUndefined(); }); }); diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index 2df161d56a01..16fae57beb68 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -1,4 +1,6 @@ +import { PAGELOAD } from '@sentry/conventions/op'; import { getClient, GLOBAL_OBJ } from '@sentry/core'; +import { _INTERNAL_withCacheOrigin } from '@sentry/core/browser'; import { browserTracingIntegration, _INTERNAL_createVueRouteProvider, @@ -9,7 +11,6 @@ import { import { defineNuxtPlugin, isNuxtError } from 'nuxt/app'; import type { GlobalObjWithIntegrationOptions } from '../../client/vueIntegration'; import { reportNuxtError } from '../utils'; -import { withCacheOrigin } from '../utils/cacheOriginPageload'; // --- Types are copied from @sentry/vue (so it does not need to be exported) --- // The following type is an intersection of the Route type from VueRouter v2, v3, and v4. @@ -77,7 +78,11 @@ export default defineNuxtPlugin({ browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path', - beforeStartSpan: options => withCacheOrigin(options, nuxtApp.payload.serverRendered === true), + // A client-side shell (`ssr: false`) has no server-rendered content to mark. + beforeStartSpan: options => + options.op === PAGELOAD && nuxtApp.payload.serverRendered === true + ? _INTERNAL_withCacheOrigin(options, 'static') + : options, }), ); } diff --git a/packages/nuxt/src/runtime/utils/cacheOriginMetaTag.ts b/packages/nuxt/src/runtime/utils/cacheOriginMetaTag.ts deleted file mode 100644 index 860a747209d9..000000000000 --- a/packages/nuxt/src/runtime/utils/cacheOriginMetaTag.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Replaces `sentry-trace` and `baggage` in a page that other visitors get too. Holds the `sentry-trace` - * value of the render, or nothing for a build prerender. The client links it and never continues it. - */ -export const CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; diff --git a/packages/nuxt/src/runtime/utils/cacheOriginPageload.ts b/packages/nuxt/src/runtime/utils/cacheOriginPageload.ts deleted file mode 100644 index 3ef0ddb3a28a..000000000000 --- a/packages/nuxt/src/runtime/utils/cacheOriginPageload.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { PAGELOAD } from '@sentry/conventions/op'; -import type { SpanLink, StartSpanOptions } from '@sentry/core'; -import { extractTraceparentData } from '@sentry/core'; -import { WINDOW } from '@sentry/vue'; -import { CACHE_ORIGIN_META_TAG_NAME } from './cacheOriginMetaTag'; - -// `@opentelemetry/api` `TraceFlags`, inlined because core does not export its copies. -const TRACE_FLAG_SAMPLED = 0x1; - -/** The `cache_origin` link to the sampled render that a `sentry-trace` value describes. */ -function getCacheOriginLink(sentryTrace: string): SpanLink | undefined { - const traceparent = extractTraceparentData(sentryTrace); - if (!traceparent?.traceId || !traceparent.parentSpanId || !traceparent.parentSampled) { - return undefined; - } - return { - context: { traceId: traceparent.traceId, spanId: traceparent.parentSpanId, traceFlags: TRACE_FLAG_SAMPLED }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }; -} - -/** - * Marks the pageload of a reused page as `static` and links the render if it was sampled (as in Next.js). - * Runs at span start, so `tracesSampler` and `ignoreSpans` see the attribute. A client-side shell - * (`ssr: false`) stays unmarked, because the server rendered none of its content. - */ -export function withCacheOrigin(spanOptions: StartSpanOptions, isServerRendered: boolean): StartSpanOptions { - if (spanOptions.op !== PAGELOAD || !isServerRendered) { - return spanOptions; - } - - const cacheOrigin = WINDOW.document - ?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`) - ?.getAttribute('content'); - if (typeof cacheOrigin !== 'string') { - return spanOptions; - } - - const link = getCacheOriginLink(cacheOrigin); - return { - ...spanOptions, - attributes: { ...spanOptions.attributes, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - links: link ? [...(spanOptions.links ?? []), link] : spanOptions.links, - }; -} diff --git a/packages/nuxt/src/runtime/utils/pageMetaTags.ts b/packages/nuxt/src/runtime/utils/pageMetaTags.ts index 7c9cb5d019e9..6b88ed637701 100644 --- a/packages/nuxt/src/runtime/utils/pageMetaTags.ts +++ b/packages/nuxt/src/runtime/utils/pageMetaTags.ts @@ -1,9 +1,9 @@ import type { SerializedTraceData } from '@sentry/core'; -import { debug, getTraceData, GLOBAL_OBJ } from '@sentry/core'; +import { debug, GLOBAL_OBJ } from '@sentry/core'; +import { _INTERNAL_getCacheOriginMetaTag, _INTERNAL_getRootSpanSentryTrace } from '@sentry/core/server'; import type { H3Event } from 'h3'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { addSentryTracingMetaTags, getEventRequestInfo } from '../utils'; -import { CACHE_ORIGIN_META_TAG_NAME } from './cacheOriginMetaTag'; /** Global flag set by the generated runtime-flags module to the Nitro preset of the server build. */ export const NUXT_NITRO_PRESET_FLAG = '__SENTRY_NUXT_NITRO_PRESET__'; @@ -27,32 +27,23 @@ interface RouteRules { /** * Adds the Sentry meta tags to a rendered page. A page that other visitors get too has a * `sentry-cache-origin` tag instead of `sentry-trace` and `baggage`, so that no visitor continues the - * trace of the render. The tag is empty for build prerenders, because the SDK does not run in the build. + * trace of the render. */ export function addSentryPageMetaTags( head: NuxtRenderHTMLContext['head'], event: H3Event, traceData?: SerializedTraceData, ): void { - const cacheOrigin = getCacheOrigin(event, traceData); - if (cacheOrigin === undefined) { + const isPrerender = isPrerenderRequest(event); + if (!isPrerender && !isCachedRoute(event)) { addSentryTracingMetaTags(head, traceData); return; } debug.log(`Adding the Sentry cache origin meta tag to the reused page ${getEventRequestInfo(event).path}.`); - head.push(``); -} - -/** The `sentry-trace` value of the render of a reused page, or `undefined` for a page rendered per request. */ -function getCacheOrigin(event: H3Event, traceData: SerializedTraceData | undefined): string | undefined { - if (isPrerenderRequest(event)) { - return ''; - } - if (!isCachedRoute(event)) { - return undefined; - } - return (traceData ?? getTraceData())['sentry-trace'] ?? ''; + // Sentry skips its server init in the prerender build, so a prerendered page has no trace to link. + const renderTrace = isPrerender ? undefined : (traceData?.['sentry-trace'] ?? _INTERNAL_getRootSpanSentryTrace()); + head.push(_INTERNAL_getCacheOriginMetaTag(renderTrace)); } // The Nitro prerenderer sends this header with each route it renders (Nitro v2 and v3). h3 v2 deprecates diff --git a/packages/nuxt/test/runtime/plugins/client.test.ts b/packages/nuxt/test/runtime/plugins/client.test.ts index cbc8b35aa117..e0eef316a7ff 100644 --- a/packages/nuxt/test/runtime/plugins/client.test.ts +++ b/packages/nuxt/test/runtime/plugins/client.test.ts @@ -1,11 +1,12 @@ +import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Client, StartSpanOptions } from '@sentry/core'; import type { isNuxtError as nuxtIsNuxtError, NuxtApp } from 'nuxt/app'; import { afterEach, describe, expect, it, vi } from 'vitest'; import clientPlugin from '../../../src/runtime/plugins/sentry.client'; import * as utils from '../../../src/runtime/utils'; -import { withCacheOrigin } from '../../../src/runtime/utils/cacheOriginPageload'; const getClient = vi.hoisted(() => vi.fn<() => Client | undefined>()); +const withCacheOriginMock = vi.hoisted(() => vi.fn()); const browserTracingIntegration = vi.hoisted(() => vi.fn()); const vueIntegration = vi.hoisted(() => vi.fn()); const createVueRouteProvider = vi.hoisted(() => vi.fn()); @@ -29,6 +30,11 @@ vi.mock(import('@sentry/core'), async importOriginal => { }; }); +vi.mock(import('@sentry/core/browser'), async importOriginal => ({ + ...(await importOriginal()), + _INTERNAL_withCacheOrigin: withCacheOriginMock, +})); + vi.mock(import('@sentry/vue'), async importOriginal => { const mod = await importOriginal(); return { @@ -41,10 +47,6 @@ vi.mock(import('@sentry/vue'), async importOriginal => { }; }); -vi.mock(import('../../../src/runtime/utils/cacheOriginPageload'), () => ({ - withCacheOrigin: vi.fn(), -})); - vi.mock(import('nuxt/app'), () => ({ defineNuxtPlugin: vi.fn(plugin => plugin), isNuxtError: vi.fn(() => false) as unknown as typeof nuxtIsNuxtError, @@ -139,9 +141,10 @@ describe('Sentry Nuxt client plugin', () => { expect(client.addIntegration).toHaveBeenCalledWith(integration); }); - it.each([true, false])( - 'passes `serverRendered: %s` from the Nuxt payload to the cache origin check of the pageload', - async serverRendered => { + describe('beforeStartSpan', () => { + async function setupBeforeStartSpan( + serverRendered: boolean, + ): Promise<(options: StartSpanOptions) => StartSpanOptions> { let beforeStartSpan: ((options: StartSpanOptions) => StartSpanOptions) | undefined; browserTracingIntegration.mockImplementation(options => { beforeStartSpan = options.beforeStartSpan; @@ -150,14 +153,31 @@ describe('Sentry Nuxt client plugin', () => { getClient.mockReturnValue(makeClient()); const nuxtApp = makeNuxtApp({ beforeEach: vi.fn(), onError: vi.fn() }); nuxtApp.payload = { serverRendered } as NuxtApp['payload']; - const pageloadOptions = { name: '/rendering-modes/swr-cached-page', op: 'pageload' }; await (clientPlugin as NuxtPluginShape).setup(nuxtApp); - beforeStartSpan?.(pageloadOptions); + return beforeStartSpan!; + } - expect(withCacheOrigin).toHaveBeenCalledWith(pageloadOptions, serverRendered); - }, - ); + it('passes the pageload of a server-rendered page to the cache origin helper as static', async () => { + const beforeStartSpan = await setupBeforeStartSpan(true); + const pageloadOptions = { name: '/rendering-modes/swr-cached-page', op: PAGELOAD }; + + beforeStartSpan(pageloadOptions); + + expect(withCacheOriginMock).toHaveBeenCalledWith(pageloadOptions, 'static'); + }); + + it.each([ + { kind: 'the pageload of a client-side shell', serverRendered: false, op: PAGELOAD }, + { kind: 'a navigation', serverRendered: true, op: NAVIGATION }, + ])('does not change $kind', async ({ serverRendered, op }) => { + const beforeStartSpan = await setupBeforeStartSpan(serverRendered); + const options = { name: '/rendering-modes/swr-cached-page', op }; + + expect(beforeStartSpan(options)).toBe(options); + expect(withCacheOriginMock).not.toHaveBeenCalled(); + }); + }); it('does not add browserTracingIntegration without an initialized client', async () => { getClient.mockReturnValue(undefined); diff --git a/packages/nuxt/test/runtime/utils/cacheOriginPageload.test.ts b/packages/nuxt/test/runtime/utils/cacheOriginPageload.test.ts deleted file mode 100644 index f90c87afac01..000000000000 --- a/packages/nuxt/test/runtime/utils/cacheOriginPageload.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; -import type { StartSpanOptions } from '@sentry/core'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { withCacheOrigin } from '../../../src/runtime/utils/cacheOriginPageload'; - -const TRACE_ID = '4c79f60c11214eb38604f4ae0781bfb2'; -const SPAN_ID = 'fa90fdead5f74052'; - -const pageloadOptions: StartSpanOptions = { - name: '/rendering-modes/swr-cached-page', - op: PAGELOAD, - attributes: { [SENTRY_OP]: PAGELOAD, [SENTRY_ORIGIN]: 'auto.pageload.vue' }, -}; - -function addCacheOriginTag(content: string): void { - const tag = document.createElement('meta'); - tag.name = 'sentry-cache-origin'; - tag.content = content; - document.head.appendChild(tag); -} - -describe('withCacheOrigin', () => { - beforeEach(() => { - document.head.innerHTML = ''; - }); - - it('marks the pageload as static and links the sampled render', () => { - addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); - - const options = withCacheOrigin(pageloadOptions, true); - - expect(options).toEqual({ - ...pageloadOptions, - attributes: { ...pageloadOptions.attributes, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - links: [ - { - context: { traceId: TRACE_ID, spanId: SPAN_ID, traceFlags: 1 }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }, - ], - }); - }); - - it.each([ - { origin: 'an unsampled render', content: `${TRACE_ID}-${SPAN_ID}-0` }, - { origin: 'a render without a sampling decision', content: `${TRACE_ID}-${SPAN_ID}` }, - { origin: 'a build prerender', content: '' }, - ])('marks the pageload as static without a link for $origin', ({ content }) => { - addCacheOriginTag(content); - - const options = withCacheOrigin(pageloadOptions, true); - - expect(options).toEqual({ - ...pageloadOptions, - attributes: { ...pageloadOptions.attributes, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - }); - }); - - it('keeps the links that the pageload already has', () => { - addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); - const previousTraceLink = { - context: { traceId: '0d5c7c7a6f0b4a57a3f1e9c2b8d4e6f1', spanId: 'b1a2c3d4e5f60718', traceFlags: 1 }, - attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, - }; - - const options = withCacheOrigin({ ...pageloadOptions, links: [previousTraceLink] }, true); - - expect(options.links).toEqual([ - previousTraceLink, - { - context: { traceId: TRACE_ID, spanId: SPAN_ID, traceFlags: 1 }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }, - ]); - }); - - it('does not change a pageload without a cache origin tag', () => { - const options = withCacheOrigin(pageloadOptions, true); - - expect(options).toBe(pageloadOptions); - }); - - it('does not change the pageload of a client-side shell', () => { - addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); - - const options = withCacheOrigin(pageloadOptions, false); - - expect(options).toBe(pageloadOptions); - }); - - it('does not change a navigation', () => { - addCacheOriginTag(`${TRACE_ID}-${SPAN_ID}-1`); - const navigationOptions: StartSpanOptions = { name: '/checkout', op: NAVIGATION }; - - const options = withCacheOrigin(navigationOptions, true); - - expect(options).toBe(navigationOptions); - }); -}); diff --git a/packages/nuxt/test/runtime/utils/pageMetaTags.test.ts b/packages/nuxt/test/runtime/utils/pageMetaTags.test.ts index cfe1fe0a5f4b..2aca9037af9d 100644 --- a/packages/nuxt/test/runtime/utils/pageMetaTags.test.ts +++ b/packages/nuxt/test/runtime/utils/pageMetaTags.test.ts @@ -1,4 +1,5 @@ -import { getTraceData, getTraceMetaTags } from '@sentry/core'; +import { getTraceMetaTags } from '@sentry/core'; +import { _INTERNAL_getRootSpanSentryTrace } from '@sentry/core/server'; import type { H3Event } from 'h3'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { addSentryPageMetaTags, NUXT_NITRO_PRESET_FLAG } from '../../../src/runtime/utils/pageMetaTags'; @@ -7,11 +8,15 @@ vi.mock(import('@sentry/core'), async importOriginal => { const mod = await importOriginal(); return { ...mod, - getTraceData: vi.fn(), getTraceMetaTags: vi.fn(), }; }); +vi.mock(import('@sentry/core/server'), async importOriginal => ({ + ...(await importOriginal()), + _INTERNAL_getRootSpanSentryTrace: vi.fn(), +})); + const SENTRY_TRACE = '4c79f60c11214eb38604f4ae0781bfb2-fa90fdead5f74052-1'; const LIVE_TRACE_META_TAGS = [ ``, @@ -73,7 +78,7 @@ describe('addSentryPageMetaTags', () => { { nitro: 'Nitro v2', event: h3v1Event({ cache: { swr: true, maxAge: 3600 } }) }, { nitro: 'Nitro v3', event: h3v2Event(nitroV3Rule('cache', { swr: true, maxAge: 3600 })) }, ])('replaces the tracing meta tags with the render as cache origin on a cached route ($nitro)', ({ event }) => { - (getTraceData as Mock).mockReturnValue({ 'sentry-trace': SENTRY_TRACE, baggage: 'sentry-environment=production' }); + (_INTERNAL_getRootSpanSentryTrace as Mock).mockReturnValue(SENTRY_TRACE); const head: string[] = []; addSentryPageMetaTags(head, event); @@ -97,11 +102,11 @@ describe('addSentryPageMetaTags', () => { addSentryPageMetaTags(head, h3v1Event({ cache: {} }), { 'sentry-trace': storedSentryTrace }); expect(head).toEqual([``]); - expect(getTraceData).not.toHaveBeenCalled(); + expect(_INTERNAL_getRootSpanSentryTrace).not.toHaveBeenCalled(); }); it('adds an empty cache origin tag to a cached route when the server has no trace data', () => { - (getTraceData as Mock).mockReturnValue({}); + (_INTERNAL_getRootSpanSentryTrace as Mock).mockReturnValue(undefined); const head: string[] = []; addSentryPageMetaTags(head, h3v1Event({ cache: {} })); @@ -131,7 +136,7 @@ describe('addSentryPageMetaTags', () => { it.each(['vercel', 'netlify'])('replaces the tracing meta tags on an `isr` route on the %s preset', preset => { vi.stubGlobal(NUXT_NITRO_PRESET_FLAG, preset); - (getTraceData as Mock).mockReturnValue({ 'sentry-trace': SENTRY_TRACE }); + (_INTERNAL_getRootSpanSentryTrace as Mock).mockReturnValue(SENTRY_TRACE); const head: string[] = []; addSentryPageMetaTags(head, h3v1Event({ isr: 3600 })); diff --git a/packages/sveltekit/src/client/svelte4BrowserTracing.ts b/packages/sveltekit/src/client/svelte4BrowserTracing.ts index 06a1c24b03b2..f92a42468742 100644 --- a/packages/sveltekit/src/client/svelte4BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte4BrowserTracing.ts @@ -5,6 +5,7 @@ import { PAGELOAD_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, } from '@sentry/core'; +import { _INTERNAL_withCacheOrigin } from '@sentry/core/browser'; import { getCurrentScope, startBrowserTracingNavigationSpan, @@ -26,7 +27,6 @@ import type { Navigation, Page } from '@sveltejs/kit'; // eslint-disable-next-line typescript/no-deprecated import { navigating, page } from '$app/stores'; import type { Readable } from 'svelte/store'; -import { withCacheOrigin } from './traceMetaTags'; /** * SvelteKit 2 / Svelte 4 browser tracing (`$app/stores`). Selected at build time, so it's only @@ -53,16 +53,19 @@ function _instrumentPageload(client: Client, pageStore: Readable): void { const pageloadSpan = startBrowserTracingPageLoadSpan( client, - withCacheOrigin({ - // With span streaming, span names have to be low cardinality. The route id is only available - // asynchronously, which updates the span name then. - name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, - attributes: { - [SENTRY_OP]: PAGELOAD, - [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + _INTERNAL_withCacheOrigin( + { + // With span streaming, span names have to be low cardinality. The route id is only available + // asynchronously, which updates the span name then. + name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, + attributes: { + [SENTRY_OP]: PAGELOAD, + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + }, }, - }), + 'static', + ), ); if (!pageloadSpan) { return; diff --git a/packages/sveltekit/src/client/svelte5BrowserTracing.ts b/packages/sveltekit/src/client/svelte5BrowserTracing.ts index 135928af8c7e..1cfcf792c883 100644 --- a/packages/sveltekit/src/client/svelte5BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte5BrowserTracing.ts @@ -5,6 +5,7 @@ import { PAGELOAD_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, } from '@sentry/core'; +import { _INTERNAL_withCacheOrigin } from '@sentry/core/browser'; import { getCurrentScope, startBrowserTracingNavigationSpan, @@ -24,7 +25,6 @@ import { import { NAVIGATION, PAGELOAD, ROUTER } from '@sentry/conventions/op'; import type { Navigation } from '@sveltejs/kit'; import { getCurrentNavigation, onNavigationChange, onPageRouteChange } from './navigationState.svelte'; -import { withCacheOrigin } from './traceMetaTags'; /** * SvelteKit 3 / Svelte 5 browser tracing (`$app/state` runes). Selected at build time, so it's only @@ -49,16 +49,19 @@ function _instrumentPageLoad(client: Client): void { const pageLoadSpan = startBrowserTracingPageLoadSpan( client, - withCacheOrigin({ - // With span streaming, span names have to be low cardinality. The route id is only available - // asynchronously, which updates the span name then. - name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, - attributes: { - [SENTRY_OP]: PAGELOAD, - [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + _INTERNAL_withCacheOrigin( + { + // With span streaming, span names have to be low cardinality. The route id is only available + // asynchronously, which updates the span name then. + name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, + attributes: { + [SENTRY_OP]: PAGELOAD, + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + }, }, - }), + 'static', + ), ); if (!pageLoadSpan) { diff --git a/packages/sveltekit/src/client/traceMetaTags.ts b/packages/sveltekit/src/client/traceMetaTags.ts deleted file mode 100644 index c21dbdc73077..000000000000 --- a/packages/sveltekit/src/client/traceMetaTags.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import type { SpanLink, StartSpanOptions } from '@sentry/core'; -import { extractTraceparentData } from '@sentry/core'; -import { WINDOW } from '@sentry/svelte'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; - -// `@opentelemetry/api` `TraceFlags.SAMPLED`, inlined because core does not export its copy. -const TRACE_FLAG_SAMPLED = 0x1; - -/** - * Marks the pageload of a reused page as `static` at span start, so `tracesSampler` sees it. Links the - * render only if it was sampled, like every `cache_origin` link. - */ -export function withCacheOrigin(spanOptions: StartSpanOptions): StartSpanOptions { - const cacheOrigin = WINDOW.document - ?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`) - ?.getAttribute('content'); - if (cacheOrigin == null) { - return spanOptions; - } - - const link = getSampledCacheOriginLink(cacheOrigin); - return { - ...spanOptions, - attributes: { ...spanOptions.attributes, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - ...(link && { links: [link] }), - }; -} - -function getSampledCacheOriginLink(sentryTrace: string): SpanLink | undefined { - const traceparent = extractTraceparentData(sentryTrace); - if (!traceparent?.traceId || !traceparent.parentSpanId || !traceparent.parentSampled) { - return undefined; - } - return { - context: { traceId: traceparent.traceId, spanId: traceparent.parentSpanId, traceFlags: TRACE_FLAG_SAMPLED }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }; -} diff --git a/packages/sveltekit/src/common/traceMetaTags.ts b/packages/sveltekit/src/common/traceMetaTags.ts deleted file mode 100644 index 16292b31274c..000000000000 --- a/packages/sveltekit/src/common/traceMetaTags.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Replaces `sentry-trace` and `baggage` in a page that many visitors get. Holds the `sentry-trace` of the - * render to link, or is empty. The client links it but never continues it. - */ -export const CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index e70cf289c09f..404e41b45cbb 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -2,13 +2,10 @@ import type { Span } from '@sentry/core'; import { continueTrace, debug, - getActiveSpan, getClient, getCurrentScope, getDefaultIsolationScope, getIsolationScope, - getRootSpan, - getTraceData, getTraceMetaTags, hasSpanStreamingEnabled, httpHeadersToSpanAttributes, @@ -24,11 +21,12 @@ import { } from '@sentry/core'; import { flushIfServerless, + _INTERNAL_getCacheOriginMetaTag, + _INTERNAL_getRootSpanSentryTrace, _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { getTunnelBasePath } from '../common/tunnel'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; import type { Handle, ResolveOptions } from './handleTypes'; import { DEBUG_BUILD } from '../common/debug-build'; import { getTracePropagationData, sendErrorToSentry } from './utils'; @@ -117,14 +115,7 @@ function getPageMetaTags(html: string, isVercelIsrRender: boolean): string { // The build does not reliably send spans, so a prerendered page has no trace to link. // The empty tag still marks the pageload as static. - const originTrace = isVercelIsrRender ? getRootSpanTrace() : undefined; - return ``; -} - -/** With Kit tracing, the active span while the page renders is Kit's `sveltekit.resolve`, not the root span. */ -function getRootSpanTrace(): string | undefined { - const activeSpan = getActiveSpan(); - return getTraceData({ span: activeSpan && getRootSpan(activeSpan) })['sentry-trace']; + return _INTERNAL_getCacheOriginMetaTag(isVercelIsrRender ? _INTERNAL_getRootSpanSentryTrace() : undefined); } /** Kit writes `node_ids` only if the server rendered content, not for `ssr = false` or the SPA fallback. */ diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index a2b17a736c70..c055649573bb 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -9,7 +9,7 @@ import { writable } from 'svelte/store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { navigating, page } from '$app/stores'; import { browserTracingIntegration } from '../../src/client'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../../src/common/traceMetaTags'; +import { _INTERNAL_CACHE_ORIGIN_META_TAG_NAME } from '@sentry/core'; import { PAGE_SERVER_RENDERING_TYPE, SENTRY_SEGMENT_NAME_SOURCE, @@ -182,7 +182,7 @@ describe('browserTracingIntegration', () => { it('marks the pageload of a prerendered page as statically rendered, without a link', async () => { const marker = document.createElement('meta'); - marker.name = CACHE_ORIGIN_META_TAG_NAME; + marker.name = _INTERNAL_CACHE_ORIGIN_META_TAG_NAME; marker.content = ''; document.head.appendChild(marker); diff --git a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts index 176f7bf5c50a..bdd14979d190 100644 --- a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts +++ b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts @@ -15,7 +15,7 @@ import { import { PAGELOAD } from '@sentry/conventions/op'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentSvelteKitTracing } from '../../src/client/svelte5BrowserTracing'; -import { CACHE_ORIGIN_META_TAG_NAME } from '../../src/common/traceMetaTags'; +import { _INTERNAL_CACHE_ORIGIN_META_TAG_NAME } from '@sentry/core'; // The Svelte 5 variant reads navigation state from rune-backed helpers (`navigationState.svelte.ts`), // which need the Svelte compiler. We mock them here so the variant's orchestration logic (fetch @@ -108,7 +108,7 @@ describe('svelte5 browser tracing', () => { it('marks the pageload of a prerendered page as statically rendered, without a link', () => { const marker = document.createElement('meta'); - marker.name = CACHE_ORIGIN_META_TAG_NAME; + marker.name = _INTERNAL_CACHE_ORIGIN_META_TAG_NAME; marker.content = ''; document.head.appendChild(marker); diff --git a/packages/sveltekit/test/client/traceMetaTags.test.ts b/packages/sveltekit/test/client/traceMetaTags.test.ts deleted file mode 100644 index b491fff60f62..000000000000 --- a/packages/sveltekit/test/client/traceMetaTags.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; -import { PAGELOAD } from '@sentry/conventions/op'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { withCacheOrigin } from '../../src/client/traceMetaTags'; - -const spanOptions = { name: '/pricing', attributes: { [SENTRY_OP]: PAGELOAD } }; - -function addCacheOriginTag(content: string): void { - const tag = document.createElement('meta'); - tag.name = 'sentry-cache-origin'; - tag.content = content; - document.head.appendChild(tag); -} - -describe('withCacheOrigin', () => { - beforeEach(() => { - document.head.innerHTML = ''; - }); - - it('returns the span options unchanged for a page without a cache origin tag', () => { - expect(withCacheOrigin(spanOptions)).toBe(spanOptions); - }); - - it('marks the pageload as static and links a sampled render', () => { - addCacheOriginTag('0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3-1'); - - expect(withCacheOrigin(spanOptions)).toEqual({ - name: '/pricing', - attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - links: [ - { - context: { traceId: '0b37aaf4f0d74c0e80875099e7d7ca68', spanId: 'bc3a52360214dcf3', traceFlags: 1 }, - attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, - }, - ], - }); - }); - - it.each([ - { kind: 'an empty tag (prerendered during the build)', content: '' }, - { kind: 'an unsampled render', content: '0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3-0' }, - { kind: 'a render without a sampling decision', content: '0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3' }, - { kind: 'a malformed value', content: 'not-a-trace' }, - ])('marks the pageload as static without a link for $kind', ({ content }) => { - addCacheOriginTag(content); - - expect(withCacheOrigin(spanOptions)).toEqual({ - name: '/pricing', - attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, - }); - }); -});