From 2be523a7d06e71fec88626aaf05a8a1c1941d13e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 26 May 2026 10:41:36 +0200 Subject: [PATCH 1/4] test(tanstackstart-react): Add failing E2E tests for early client init Reproduces #21088: errors thrown in the client entry before hydration are silently lost because Sentry.init() runs inside getRouter() which is called during hydration. The test also verifies that early console breadcrumbs are attached to the error report. These tests are expected to fail with the current setup and pass once Sentry.init() is moved to the client entry point. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/client.tsx | 18 ++++++++++++ .../src/routes/crash-before-hydration.tsx | 13 +++++++++ .../tests/early-init.test.ts | 23 +++++++++++++++ .../tanstackstart-react/src/client.tsx | 18 ++++++++++++ .../src/routes/crash-before-hydration.tsx | 13 +++++++++ .../tests/early-init.test.ts | 28 +++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/crash-before-hydration.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/early-init.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/crash-before-hydration.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/early-init.test.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx new file mode 100644 index 000000000000..b1dd4a454ca5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx @@ -0,0 +1,18 @@ +import { StartClient } from '@tanstack/react-start/client'; +import { StrictMode, startTransition } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +console.log('early-breadcrumb-from-client-entry'); + +if (window.location.pathname === '/crash-before-hydration') { + throw new Error('Client Entry Crash'); +} + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/crash-before-hydration.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/crash-before-hydration.tsx new file mode 100644 index 000000000000..772a045a7e33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/crash-before-hydration.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/crash-before-hydration')({ + component: CrashPage, +}); + +function CrashPage() { + return ( +
+

This page crashes in client.tsx before hydration

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/early-init.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/early-init.test.ts new file mode 100644 index 000000000000..2b58f0bf594e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/early-init.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('should capture errors thrown in client entry before hydration', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react-cloudflare', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Client Entry Crash'; + }); + + await page.goto('/crash-before-hydration'); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: 'Client Entry Crash', + }); + + const consoleBreadcrumbs = errorEvent.breadcrumbs?.filter( + b => b.category === 'console' && b.message?.includes('early-breadcrumb-from-client-entry'), + ); + + expect(consoleBreadcrumbs?.length).toBeGreaterThanOrEqual(1); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx new file mode 100644 index 000000000000..b1dd4a454ca5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx @@ -0,0 +1,18 @@ +import { StartClient } from '@tanstack/react-start/client'; +import { StrictMode, startTransition } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +console.log('early-breadcrumb-from-client-entry'); + +if (window.location.pathname === '/crash-before-hydration') { + throw new Error('Client Entry Crash'); +} + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/crash-before-hydration.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/crash-before-hydration.tsx new file mode 100644 index 000000000000..772a045a7e33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/crash-before-hydration.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/crash-before-hydration')({ + component: CrashPage, +}); + +function CrashPage() { + return ( +
+

This page crashes in client.tsx before hydration

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/early-init.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/early-init.test.ts new file mode 100644 index 000000000000..ca2c7e44ee14 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/early-init.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +const usesManagedTunnelRoute = + (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; + +test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); + +test('should capture errors thrown in client entry before hydration', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Client Entry Crash'; + }); + + await page.goto('/crash-before-hydration'); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: 'Client Entry Crash', + }); + + const consoleBreadcrumbs = errorEvent.breadcrumbs?.filter( + b => b.category === 'console' && b.message?.includes('early-breadcrumb-from-client-entry'), + ); + + expect(consoleBreadcrumbs?.length).toBeGreaterThanOrEqual(1); +}); From c9e28b61eff0a9959ef6ea0de1ac8622637f03af Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 26 May 2026 10:52:44 +0200 Subject: [PATCH 2/4] fix(tanstackstart-react): Move Sentry.init() to client entry point Moves client-side Sentry.init() from getRouter() to client.tsx so it runs before hydration. The browser tracing integration is added later via addIntegration() once the router is available. This ensures errors and breadcrumbs from code that runs before hydration (e.g. third-party library inits) are captured. Fixes #21088 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tanstackstart-react-cloudflare/src/client.tsx | 9 +++++++++ .../tanstackstart-react-cloudflare/src/router.tsx | 9 +-------- .../tanstackstart-react/src/client.tsx | 11 +++++++++++ .../tanstackstart-react/src/router.tsx | 11 +---------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx index b1dd4a454ca5..5cc8b2a9c7f7 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/client.tsx @@ -1,7 +1,16 @@ +import * as Sentry from '@sentry/browser'; import { StartClient } from '@tanstack/react-start/client'; import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; +Sentry.init({ + environment: 'qa', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + release: 'e2e-test', + tunnel: 'http://localhost:3031/', +}); + console.log('early-breadcrumb-from-client-entry'); if (window.location.pathname === '/crash-before-hydration') { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx index c18a1a0b8167..386bebc8c363 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx @@ -9,14 +9,7 @@ export const getRouter = () => { }); if (!router.isServer) { - Sentry.init({ - environment: 'qa', - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, - release: 'e2e-test', - tunnel: 'http://localhost:3031/', - }); + Sentry.addIntegration(Sentry.browserTracingIntegration()); } return router; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx index b1dd4a454ca5..318bfbdf2285 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/client.tsx @@ -1,7 +1,18 @@ +import * as Sentry from '@sentry/tanstackstart-react'; import { StartClient } from '@tanstack/react-start/client'; import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: __APP_DSN__, + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + tunnel: __APP_TUNNEL__, +}); + console.log('early-breadcrumb-from-client-entry'); if (window.location.pathname === '/crash-before-hydration') { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx index 9a39b6f35c42..670b22bc688d 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx @@ -9,16 +9,7 @@ export const getRouter = () => { }); if (!router.isServer) { - Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: __APP_DSN__, - integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)], - // We recommend adjusting this value in production, or using tracesSampler - // for finer control - tracesSampleRate: 1.0, - release: 'e2e-test', - tunnel: __APP_TUNNEL__, - }); + Sentry.addIntegration(Sentry.tanstackRouterBrowserTracingIntegration(router)); } return router; From 7ca834c21792e51e9db3524f5265bb2358d60b49 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 26 May 2026 12:16:32 +0200 Subject: [PATCH 3/4] docs: Add changelog entry for early client init Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab67237668e..80070bb9acd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- **test(tanstackstart-react): Move initialization to client entry point ([#21161](https://github.com/getsentry/sentry-javascript/pull/21161))** + + Change the recommended setup for the SDK to do `Sentry.init()` in the client entry file to capture telemetry that is emitted ahead of page hydration. + ## 10.54.0 ### Important Changes @@ -16,6 +20,8 @@ Adds a new `dataCollection` client option for controlling what data the SDK collects and sends to Sentry. This provides a centralized way to configure data collection behavior across different SDK features. In the future, this option will be used for fine-granular data filtering, while the simple `sendDefaultPii` boolean option will be deprecated and removed in a future release. +Work in this release was contributed by @abcang, @ahmadio, @victorgarciaesgi, @delorge, and @mdnanocom. Thank you for your contributions! + - **feat(core): Support array attributes for spans, logs, and metrics ([#20427](https://github.com/getsentry/sentry-javascript/pull/20427))** Arrays of primitive values (`string`, `number`, `boolean`) are now accepted as attribute values. Arrays containing non-primitive elements will be dropped and won't show up in Sentry. Note that array attributes on logs and metrics were previously stringified in certain cases and will now be sent as arrays instead. From e1f999bcfda82930f2ee161e954e7411eaf6a4ac Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 26 May 2026 15:11:14 +0200 Subject: [PATCH 4/4] ci: retrigger --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80070bb9acd2..b737ba882ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,6 @@ Adds a new `dataCollection` client option for controlling what data the SDK collects and sends to Sentry. This provides a centralized way to configure data collection behavior across different SDK features. In the future, this option will be used for fine-granular data filtering, while the simple `sendDefaultPii` boolean option will be deprecated and removed in a future release. -Work in this release was contributed by @abcang, @ahmadio, @victorgarciaesgi, @delorge, and @mdnanocom. Thank you for your contributions! - - **feat(core): Support array attributes for spans, logs, and metrics ([#20427](https://github.com/getsentry/sentry-javascript/pull/20427))** Arrays of primitive values (`string`, `number`, `boolean`) are now accepted as attribute values. Arrays containing non-primitive elements will be dropped and won't show up in Sentry. Note that array attributes on logs and metrics were previously stringified in certain cases and will now be sent as arrays instead.