From b9edf887be2277cab9909f56cee33b2372106b67 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Fri, 13 Nov 2026 16:00:00 +0100
Subject: [PATCH] ref: Share the cache origin helpers of Next.js, Nuxt and
SvelteKit
Next.js, Nuxt and SvelteKit each had their own copy of the sentry-cache-origin tag name, the code
that writes the tag, and the code that marks and links the pageload. Move them into one module in
@sentry/core, so the server writer and the client reader share one format. The helpers are
SDK-internal: the tag name is in @sentry/core, the client part in @sentry/core/browser and the
server part in @sentry/core/server.
Nuxt now links the root span of the request explicitly, like SvelteKit, through a shared helper.
It already linked the root span, because no child span is active while Nuxt renders the page. The
Nuxt E2E tests now check that the tag holds the http.server root span.
No behavior change for users.
---
.../nuxt-4/tests/tracing.cached-html.test.ts | 6 +-
.../nuxt-5/tests/tracing.cached-html.test.ts | 6 +-
packages/core/src/browser.ts | 1 +
packages/core/src/index.ts | 1 +
packages/core/src/server.ts | 1 +
packages/core/src/utils/cacheOrigin.ts | 72 +++++++++
.../core/test/lib/utils/cacheOrigin.test.ts | 153 ++++++++++++++++++
.../appRouterPageloadInstrumentation.ts | 9 +-
.../src/client/routing/traceMetaTags.ts | 63 ++------
packages/nextjs/src/common/traceMetaTags.ts | 6 -
.../getFinalConfigObjectUtils.ts | 5 +-
.../nextjs/src/server/nextSentryPropagator.ts | 4 +-
.../nextjs/test/client/traceMetaTags.test.ts | 63 ++------
.../nuxt/src/runtime/plugins/sentry.client.ts | 9 +-
.../src/runtime/utils/cacheOriginMetaTag.ts | 5 -
.../src/runtime/utils/cacheOriginPageload.ts | 46 ------
.../nuxt/src/runtime/utils/pageMetaTags.ts | 25 +--
.../nuxt/test/runtime/plugins/client.test.ts | 46 ++++--
.../runtime/utils/cacheOriginPageload.test.ts | 100 ------------
.../test/runtime/utils/pageMetaTags.test.ts | 17 +-
.../src/client/svelte4BrowserTracing.ts | 23 +--
.../src/client/svelte5BrowserTracing.ts | 23 +--
.../sveltekit/src/client/traceMetaTags.ts | 39 -----
.../sveltekit/src/common/traceMetaTags.ts | 5 -
.../sveltekit/src/server-common/handle.ts | 15 +-
.../client/browserTracingIntegration.test.ts | 4 +-
.../test/client/svelte5BrowserTracing.test.ts | 4 +-
.../test/client/traceMetaTags.test.ts | 56 -------
28 files changed, 361 insertions(+), 446 deletions(-)
create mode 100644 packages/core/src/utils/cacheOrigin.ts
create mode 100644 packages/core/test/lib/utils/cacheOrigin.test.ts
delete mode 100644 packages/nextjs/src/common/traceMetaTags.ts
delete mode 100644 packages/nuxt/src/runtime/utils/cacheOriginMetaTag.ts
delete mode 100644 packages/nuxt/src/runtime/utils/cacheOriginPageload.ts
delete mode 100644 packages/nuxt/test/runtime/utils/cacheOriginPageload.test.ts
delete mode 100644 packages/sveltekit/src/client/traceMetaTags.ts
delete mode 100644 packages/sveltekit/src/common/traceMetaTags.ts
delete mode 100644 packages/sveltekit/test/client/traceMetaTags.test.ts
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' },
- });
- });
-});