diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts index d85d9d82747d..3d4f0466917b 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts @@ -44,30 +44,30 @@ test('Sends a navigation transaction with parameterized route to Sentry', async expect(transactionEvent.transaction).toBeTruthy(); }); -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => { + const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200); + await page.goto('/'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => { + const responsePromise = page.waitForResponse( + response => response.url().includes('/user/123') && response.status() === 200, + ); + await page.goto('/user/123'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts index 4213aae3e3de..63e80a72d082 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts @@ -54,6 +54,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page expect(httpServerSpanId).toBeDefined(); expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadParentSpanId).toEqual(loaderSpanId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); expect(pageLoadSpanId).not.toEqual(httpServerSpanId); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts index 68237301b635..c5b37502f5b8 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => { expect(transactionEvent).toBeDefined(); }); -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => { + const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200); + await page.goto('/'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => { + const responsePromise = page.waitForResponse( + response => response.url().includes('/user/123') && response.status() === 200, + ); + await page.goto('/user/123'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts index ddb866e5dbaa..ea728b31ab50 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts @@ -96,7 +96,7 @@ test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => { expect(httpServerSpanId).toBeDefined(); expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadParentSpanId).toEqual(loaderSpanId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); expect(pageLoadSpanId).not.toEqual(httpServerSpanId); }); @@ -139,6 +139,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page expect(loaderParentSpanId).toEqual(httpServerSpanId); expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadParentSpanId).toEqual(loaderSpanId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); expect(pageLoadSpanId).not.toEqual(httpServerSpanId); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/client-transactions.test.ts index cf5098686759..2ae118c25523 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/client-transactions.test.ts @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => { expect(transactionEvent).toBeDefined(); }); -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => { + const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200); + await page.goto('/'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => { + const responsePromise = page.waitForResponse( + response => response.url().includes('/user/123') && response.status() === 200, + ); + await page.goto('/user/123'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts index e624c578ce19..3bc716a4517e 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts @@ -53,6 +53,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page expect(httpServerSpanId).toBeDefined(); expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadParentSpanId).toEqual(loaderSpanId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); expect(pageLoadSpanId).not.toEqual(httpServerSpanId); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts index 3619368f81bb..f9f8b3998e6c 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => { expect(transactionEvent).toBeDefined(); }); -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => { + const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200); + await page.goto('/'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { +test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => { + const responsePromise = page.waitForResponse( + response => response.url().includes('/user/123') && response.status() === 200, + ); + await page.goto('/user/123'); - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts index 75d8fa0d2b9e..479ea3899424 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts @@ -53,6 +53,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page expect(httpServerSpanId).toBeDefined(); expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadParentSpanId).toEqual(loaderSpanId); + expect(pageLoadParentSpanId).toEqual(httpServerSpanId); expect(pageLoadSpanId).not.toEqual(httpServerSpanId); }); diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.server.tsx index afae990db239..be8c6b8702c7 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.server.tsx @@ -1,4 +1,5 @@ import { RemixServer } from '@remix-run/react'; +import { addSentryServerTimingHeader } from '@sentry/remix/cloudflare'; import { createContentSecurityPolicy } from '@shopify/hydrogen'; import type { EntryContext } from '@shopify/remix-oxygen'; import isbot from 'isbot'; @@ -43,8 +44,11 @@ export default async function handleRequest( // This is required for Sentry's profiling integration responseHeaders.set('Document-Policy', 'js-profiling'); - return new Response(body, { + const response = new Response(body, { headers: responseHeaders, status: responseStatusCode, }); + + // Add Server-Timing header with Sentry trace context for client-side trace propagation + return addSentryServerTimingHeader(response); } diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-timing-header.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-timing-header.test.ts new file mode 100644 index 000000000000..dd5239a697f4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-timing-header.test.ts @@ -0,0 +1,72 @@ +import { expect, test } from '@playwright/test'; + +test.describe.configure({ mode: 'serial' }); + +test('Server-Timing header contains sentry-trace on page load (Cloudflare)', async ({ page }) => { + // Intercept the document response (not data requests or other resources) + const responsePromise = page.waitForResponse( + response => + response.url().endsWith('/') && response.status() === 200 && response.request().resourceType() === 'document', + ); + + await page.goto('/'); + + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; + + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); + expect(serverTimingHeader).toContain('baggage'); +}); + +test('Server-Timing header contains valid trace ID format (Cloudflare)', async ({ page }) => { + // Match only the document response for /user/123 (not .data requests) + const responsePromise = page.waitForResponse( + response => + response.url().endsWith('/user/123') && + response.status() === 200 && + response.request().resourceType() === 'document', + ); + + await page.goto('/user/123'); + + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; + + expect(serverTimingHeader).toBeDefined(); + + // Extract sentry-trace value from header + // Format: sentry-trace;desc="traceid-spanid" or sentry-trace;desc="traceid-spanid-sampled" + const sentryTraceMatch = serverTimingHeader.match(/sentry-trace;desc="([^"]+)"/); + expect(sentryTraceMatch).toBeTruthy(); + + const sentryTraceValue = sentryTraceMatch![1]; + + // Validate sentry-trace format: traceid-spanid or traceid-spanid-sampled (case insensitive) + // The format is: 32 hex chars, dash, 16 hex chars, optionally followed by dash and 0 or 1 + const traceIdMatch = sentryTraceValue.match(/^([a-fA-F0-9]{32})-([a-fA-F0-9]{16})(?:-([01]))?$/); + expect(traceIdMatch).toBeTruthy(); + + // Verify the trace ID and span ID parts + const [, traceId, spanId] = traceIdMatch!; + expect(traceId).toHaveLength(32); + expect(spanId).toHaveLength(16); +}); + +test('Server-Timing header is present on parameterized routes (Cloudflare)', async ({ page }) => { + // Match only the document response for /user/456 (not .data requests) + const responsePromise = page.waitForResponse( + response => + response.url().endsWith('/user/456') && + response.status() === 200 && + response.request().resourceType() === 'document', + ); + + await page.goto('/user/456'); + + const response = await responsePromise; + const serverTimingHeader = response.headers()['server-timing']; + + expect(serverTimingHeader).toBeDefined(); + expect(serverTimingHeader).toContain('sentry-trace'); +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/.eslintrc.js b/dev-packages/e2e-tests/test-applications/remix-server-timing/.eslintrc.js new file mode 100644 index 000000000000..126b07218de8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/.eslintrc.js @@ -0,0 +1,7 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'], + rules: { + 'import/no-unresolved': 'off', + }, +}; diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/.gitignore b/dev-packages/e2e-tests/test-applications/remix-server-timing/.gitignore new file mode 100644 index 000000000000..a735ebed5b56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +.env diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc b/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx new file mode 100644 index 000000000000..85c29d310c1a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx @@ -0,0 +1,44 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` + * For more information, see https://remix.run/file-conventions/entry.client + */ + +// Extend the Window interface to include ENV +declare global { + interface Window { + ENV: { + SENTRY_DSN: string; + [key: string]: unknown; + }; + } +} + +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { StrictMode, startTransition, useEffect } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: window.ENV.SENTRY_DSN, + integrations: [ + Sentry.browserTracingIntegration({ + useEffect, + useLocation, + useMatches, + }), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.server.tsx new file mode 100644 index 000000000000..13dcf4b0d7cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.server.tsx @@ -0,0 +1,130 @@ +import * as Sentry from '@sentry/remix'; + +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from 'node:stream'; + +import type { AppLoadContext, EntryContext } from '@remix-run/node'; +import { createReadableStreamFromReadable } from '@remix-run/node'; +import { installGlobals } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import isbot from 'isbot'; +import { renderToPipeableStream } from 'react-dom/server'; + +installGlobals(); + +const ABORT_DELAY = 5_000; + +const handleErrorImpl = () => { + Sentry.setTag('remix-test-tag', 'remix-test-value'); +}; + +export const handleError = Sentry.wrapHandleErrorWithSentry(handleErrorImpl); + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext, +) { + return isbot(request.headers.get('user-agent')) + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/app/root.tsx b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/root.tsx new file mode 100644 index 000000000000..a77e663bf4b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/root.tsx @@ -0,0 +1,67 @@ +import { cssBundleHref } from '@remix-run/css-bundle'; +import { LinksFunction, json } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, + useRouteError, +} from '@remix-run/react'; +import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'; + +export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])]; + +export const loader = () => { + return json({ + ENV: { + SENTRY_DSN: process.env.E2E_TEST_DSN, + }, + }); +}; + +// NOTE: We intentionally do NOT use meta tags for trace propagation in this test app +// to verify that Server-Timing header propagation works correctly. +// The trace context is propagated via Server-Timing header instead. + +export function ErrorBoundary() { + const error = useRouteError(); + const eventId = captureRemixErrorBoundaryError(error); + + return ( +
+ ErrorBoundary Error + {eventId} +
+ ); +} + +function App() { + const { ENV } = useLoaderData() as { ENV: { SENTRY_DSN: string } }; + + return ( + + + + +