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' },
- });
- });
-});