Repository navigation
feat(sveltekit): Mark pageloads of prerendered pages #26307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+297
−52
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3927a11
feat(sveltekit): Mark pageloads of prerendered pages
s1gr1d 395abdb
feat(sveltekit): Mark and link pageloads of Vercel ISR pages
s1gr1d 1cab2c9
fixes
s1gr1d e86a32b
clear up comments
s1gr1d ce3f204
pnpm file
s1gr1d e91fb79
Merge remote-tracking branch 'origin/develop' into sig/sveltekit-verc…
s1gr1d 04bcbab
Merge branch 'develop' into sig/sveltekit-vercel-isr
s1gr1d e59e40b
fix(sveltekit): Link the root span of a Vercel ISR render
s1gr1d File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 51 additions & 8 deletions
59
dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,55 +1,98 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; | ||
| import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; | ||
| import { HTTP_SERVER, PAGELOAD } from '@sentry/conventions/op'; | ||
|
|
||
| function sentryMetaTagNames(html: string): string[] { | ||
| return [...html.matchAll(/<meta name="(sentry-[\w-]+|baggage)"/g)].map(match => match[1] ?? ''); | ||
| } | ||
|
|
||
| const VERCEL_ISR_HEADERS = { 'x-vercel-isr': '1' }; | ||
|
|
||
| [ | ||
| { path: '/prerendered', kind: 'a prerendered page', expected: ['sentry-cache-origin'] }, | ||
| { path: '/prerendered-shell', kind: 'a prerendered client-side shell', expected: [] }, | ||
| { path: '/', kind: 'a server-rendered page', expected: ['sentry-trace', 'baggage'] }, | ||
| ].forEach(({ path, kind, expected }) => { | ||
| { path: '/', headers: VERCEL_ISR_HEADERS, kind: 'a Vercel ISR render', expected: ['sentry-cache-origin'] }, | ||
| ].forEach(({ path, headers, kind, expected }) => { | ||
| test(`writes the expected Sentry meta tags into ${kind} (${path})`, async ({ request }) => { | ||
| const html = await (await request.get(path)).text(); | ||
| const html = await (await request.get(path, { headers })).text(); | ||
|
|
||
| expect(sentryMetaTagNames(html)).toEqual(expected); | ||
| }); | ||
| }); | ||
|
|
||
| test('marks the pageload of a prerendered page as statically rendered, without a link', async ({ page }) => { | ||
| const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { | ||
| return span.name === '/prerendered' && getSpanOp(span) === 'pageload' && span.is_segment; | ||
| return span.name === '/prerendered' && getSpanOp(span) === PAGELOAD && span.is_segment; | ||
| }); | ||
|
|
||
| await page.goto('/prerendered'); | ||
| const pageloadSpan = await pageloadSpanPromise; | ||
|
|
||
| expect(pageloadSpan.attributes?.['page.server_rendering.type']).toEqual({ type: 'string', value: 'static' }); | ||
| expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toEqual({ type: 'string', value: 'static' }); | ||
| expect(pageloadSpan.links).toBeUndefined(); | ||
| expect(pageloadSpan.parent_span_id).toBeUndefined(); | ||
| }); | ||
|
|
||
| test("doesn't mark the pageload of a prerendered client-side shell", async ({ page }) => { | ||
| const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { | ||
| return span.name === '/prerendered-shell' && getSpanOp(span) === 'pageload' && span.is_segment; | ||
| return span.name === '/prerendered-shell' && getSpanOp(span) === PAGELOAD && span.is_segment; | ||
| }); | ||
|
|
||
| await page.goto('/prerendered-shell'); | ||
| const pageloadSpan = await pageloadSpanPromise; | ||
|
|
||
| expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); | ||
| expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined(); | ||
| expect(pageloadSpan.parent_span_id).toBeUndefined(); | ||
| }); | ||
|
|
||
| test("doesn't mark the pageload of a server-rendered page, which continues the server trace", async ({ page }) => { | ||
| const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { | ||
| return span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment; | ||
| return span.name === '/' && getSpanOp(span) === PAGELOAD && span.is_segment; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| const pageloadSpan = await pageloadSpanPromise; | ||
|
|
||
| expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); | ||
| expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined(); | ||
| expect(pageloadSpan.parent_span_id).toMatch(/^[a-f0-9]{16}$/); | ||
| }); | ||
|
|
||
| test('marks the pageload of a Vercel ISR page as statically rendered and links the render', async ({ page }) => { | ||
| // The event proxy replays spans from this time on, so the server span of the render can be awaited | ||
| // after its trace id is known from the HTML. Same clock as the proxy (`process.hrtime`). | ||
| const [seconds, nanoseconds] = process.hrtime(); | ||
| const requestStart = seconds * 1e9 + nanoseconds; | ||
| const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { | ||
| return span.name === '/' && getSpanOp(span) === PAGELOAD && span.is_segment; | ||
| }); | ||
|
|
||
| // Only on the document: on the SDK's cross-origin envelope requests, the header would need a preflight. | ||
| await page.route( | ||
| url => url.pathname === '/', | ||
| route => route.continue({ headers: { ...route.request().headers(), ...VERCEL_ISR_HEADERS } }), | ||
| ); | ||
| await page.goto('/'); | ||
| const cacheOrigin = await page.locator('meta[name="sentry-cache-origin"]').getAttribute('content'); | ||
| const [originTraceId, originSpanId] = cacheOrigin?.split('-') ?? []; | ||
| const [originSpan, pageloadSpan] = await Promise.all([ | ||
| waitForStreamedSpan('sveltekit-3', span => span.span_id === originSpanId, requestStart), | ||
| pageloadSpanPromise, | ||
| ]); | ||
|
|
||
| expect(originSpan.trace_id).toBe(originTraceId); | ||
| expect(getSpanOp(originSpan)).toBe(HTTP_SERVER); | ||
| expect(originSpan.is_segment).toBe(true); | ||
| expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toEqual({ type: 'string', value: 'static' }); | ||
| expect(pageloadSpan.parent_span_id).toBeUndefined(); | ||
| expect(pageloadSpan.trace_id).not.toBe(originTraceId); | ||
| expect(pageloadSpan.links).toEqual([ | ||
| { | ||
| trace_id: originTraceId, | ||
| span_id: originSpanId, | ||
| sampled: true, | ||
| attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'cache_origin' } }, | ||
| }, | ||
| ]); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,39 @@ | ||
| import { PAGE_SERVER_RENDERING_TYPE } from '@sentry/conventions/attributes'; | ||
| import type { SpanAttributes } from '@sentry/core'; | ||
| 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'; | ||
|
|
||
| /** Set at span start, so `tracesSampler` sees it. */ | ||
| export function getServerRenderingAttributes(): SpanAttributes { | ||
| const isPrerendered = !!WINDOW.document?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`); | ||
| return isPrerendered ? { [PAGE_SERVER_RENDERING_TYPE]: 'static' } : {}; | ||
| // `@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' }, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /** | ||
| * Replaces the trace meta tags of pages prerendered during the build. Empty, unlike in Next.js: spans | ||
| * from the build are not reliably sent, so there is no origin to link. | ||
| * 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'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we additionally check that
expect(getSpanOp(originSpan)).toBe('http.server')here? SInce we do not control the spans from sveltekit this would verify that actually attach it to the segment/root span