Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 72 additions & 0 deletions packages/core/src/utils/cacheOrigin.ts
Original file line number Diff line number Diff line change
@@ -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 `<meta name="${_INTERNAL_CACHE_ORIGIN_META_TAG_NAME}" content="${renderTrace ?? ''}"/>`;
}

/**
* 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' },
};
}
153 changes: 153 additions & 0 deletions packages/core/test/lib/utils/cacheOrigin.test.ts
Original file line number Diff line number Diff line change
@@ -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(
`<meta name="sentry-cache-origin" content="${TRACE_ID}-${SPAN_ID}-1"/>`,
);
});

it('is empty without a sentry-trace value', () => {
expect(_INTERNAL_getCacheOriginMetaTag(undefined)).toBe('<meta name="sentry-cache-origin" content=""/>');
});
});

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' },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading