diff --git a/CHANGELOG.md b/CHANGELOG.md index ff421e99bb3a..c8d963537e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ ## Unreleased +### Important Changes + +- **ref(react, browser): make `@sentry/react` tree-shakeable by default ([#22168](https://github.com/getsentry/sentry-javascript/pull/22168))** + + The `@sentry/react` root entry no longer star-exports the full `@sentry/browser` surface. Optional/heavy features + (Session Replay, Feedback, AI instrumenters, feature-flag integrations, browser profiling, …) and React + Router/Redux helpers are off the root export list so bundlers that materialize a dynamic `import('@sentry/react')` + namespace (e.g. Rolldown when destructuring) do not pull them into the critical path. + + **Migration** — import optional browser APIs from `@sentry/browser`, `@sentry/react/optional-browser-api`, or a + dedicated package; import router/Redux helpers from package subpaths: + + ```ts + // Before + import { replayIntegration, tanstackRouterBrowserTracingIntegration, createReduxEnhancer } from '@sentry/react'; + + // After + import { replayIntegration } from '@sentry/browser'; // or @sentry/react/optional-browser-api / @sentry/replay + import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; + import { createReduxEnhancer } from '@sentry/react/redux'; + ``` + + Framework SDKs (`@sentry/nextjs`, `@sentry/gatsby`, `@sentry/remix`, `@sentry/tanstackstart-react`) + re-export `optional-browser-api` (and `createReduxEnhancer` on nextjs/gatsby/remix) so + `import * as Sentry from '@sentry/…'` keeps the previous surface for those symbols. + - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott Work in this release was contributed by @martijnwalraven. Thank you for your contribution! diff --git a/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx b/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx index 352c53c7208b..634ab96c4459 100644 --- a/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; import { createRoot } from 'react-dom/client'; import App from './App'; @@ -19,7 +20,7 @@ if (import.meta.env.MODE === 'tracing-replay') { dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', - integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], + integrations: [Sentry.browserTracingIntegration(), replayIntegration()], tracesSampleRate: 1.0, replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx index ca6918c46106..11b72fbd7c57 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom'; import { @@ -13,13 +15,13 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -41,7 +43,7 @@ Sentry.init({ dataCollection: { userInfo: true }, }); -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); function App() { return ( diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx index c7ad16eebcf7..b5d78fd69af8 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, wrapCreateBrowserRouterV6 } from '@sentry/react/reactrouterv6'; import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -12,13 +14,13 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ // environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -41,7 +43,7 @@ Sentry.init({ debug: !!process.env.DEBUG, }); -const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter); +const sentryCreateBrowserRouter = wrapCreateBrowserRouterV6(createBrowserRouter); const LazyLoadedUser = lazy(() => import('./pages/LazyLoadedUser')); const router = sentryCreateBrowserRouter( diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx index 915cb720974a..a9a433d30975 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx @@ -1,8 +1,8 @@ -import * as Sentry from '@sentry/react'; +import { withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import * as React from 'react'; import { Route, Routes } from 'react-router-dom'; -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); const InnerRoute = () => ( diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx index 636f99d9c8cb..41c8ec6eb36a 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx @@ -1,8 +1,8 @@ -import * as Sentry from '@sentry/react'; +import { withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import * as React from 'react'; import { Route, Routes } from 'react-router-dom'; -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); const InnerRoute = React.lazy(() => import('./LazyLoadedInnerRoute')); const LazyLoadedUser = () => { diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx index 86de5f20378d..a08a80ef17f5 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, wrapCreateBrowserRouterV6 } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -13,13 +15,13 @@ import Index from './pages/Index'; import User from './pages/User'; import Group from './pages/Group'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ // environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -42,7 +44,7 @@ Sentry.init({ debug: !!process.env.DEBUG, }); -const sentryCreateHashRouter = Sentry.wrapCreateBrowserRouterV6(createHashRouter); +const sentryCreateHashRouter = wrapCreateBrowserRouterV6(createHashRouter); const router = sentryCreateHashRouter([ { diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx index f71572f9dc1f..5cdb770d4525 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, wrapCreateMemoryRouterV6 } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -12,13 +14,13 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ // environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -41,7 +43,7 @@ Sentry.init({ debug: !!process.env.DEBUG, }); -const sentryCreateMemoryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter); +const sentryCreateMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); const router = sentryCreateMemoryRouter( [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-5/src/index.tsx index cc9b307e620a..cfbfadd52d19 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-5/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-5/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV5BrowserTracingIntegration, withSentryRouting } from '@sentry/react/reactrouter'; import { createBrowserHistory } from 'history'; import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -6,7 +8,7 @@ import { Route, Router, Switch } from 'react-router-dom'; import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); const history = createBrowserHistory(); @@ -15,7 +17,7 @@ Sentry.init({ dsn: process.env.REACT_APP_E2E_TEST_DSN || 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', - integrations: [Sentry.reactRouterV5BrowserTracingIntegration({ history }), replay], + integrations: [reactRouterV5BrowserTracingIntegration({ history }), replay], // We recommend adjusting this value in production, or using tracesSampler // for finer control tracesSampleRate: 1.0, @@ -28,7 +30,7 @@ Sentry.init({ }); // Create Custom Sentry Route component -export const SentryRoute = Sentry.withSentryRouting(Route); +export const SentryRoute = withSentryRouting(Route); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx index 581014169a78..2b994012c030 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -13,13 +15,13 @@ import { } from 'react-router-dom'; import Index from './pages/Index'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -41,7 +43,7 @@ Sentry.init({ tunnel: 'http://localhost:3031', }); -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); const DetailsRoutes = () => ( diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx index 0ac33b9f6c5f..c4b9fef54d9e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterBrowserTracingIntegration, wrapUseRoutes } from '@sentry/react/reactrouter.compat'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -12,13 +14,13 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterBrowserTracingIntegration({ + reactRouterBrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -39,7 +41,7 @@ Sentry.init({ tunnel: 'http://localhost:3031', // proxy server }); -const useSentryRoutes = Sentry.wrapUseRoutes(useRoutes); +const useSentryRoutes = wrapUseRoutes(useRoutes); function App() { return useSentryRoutes([ diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx index 1430a44935b6..31009d222639 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -15,13 +17,13 @@ import Products from './pages/Products'; import SSE from './pages/SSE'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -44,7 +46,7 @@ Sentry.init({ dataCollection: { userInfo: true }, }); -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx index 5c381487587f..be2849dfac12 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx @@ -1,4 +1,11 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { + reactRouterBrowserTracingIntegration, + wrapReactRouterRouting, + wrapUseRoutes, + wrapCreateBrowserRouter, +} from '@sentry/react/reactrouter.compat'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -15,13 +22,13 @@ import { } from 'react-router-dom'; import Index from './pages/Index'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterBrowserTracingIntegration({ + reactRouterBrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -43,9 +50,9 @@ Sentry.init({ tunnel: 'http://localhost:3031', }); -const SentryRoutes = Sentry.wrapReactRouterRouting(Routes); -const sentryUseRoutes = Sentry.wrapUseRoutes(useRoutes); -const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter); +const SentryRoutes = wrapReactRouterRouting(Routes); +const sentryUseRoutes = wrapUseRoutes(useRoutes); +const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createBrowserRouter); const DetailsRoutes = () => sentryUseRoutes([ diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx index 4d47ddbf58bf..2dacb4c335ab 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +import { reactRouterV7BrowserTracingIntegration, wrapCreateBrowserRouterV7 } from '@sentry/react/reactrouterv7'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -81,7 +82,7 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV7BrowserTracingIntegration({ + reactRouterV7BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -102,7 +103,7 @@ Sentry.init({ tunnel: 'http://localhost:3031', }); -const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter); +const sentryCreateBrowserRouter = wrapCreateBrowserRouterV7(createBrowserRouter); const router = sentryCreateBrowserRouter( [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/src/main.tsx index e55bd5f50bf3..cf26f5c09797 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/src/main.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration, spanStreamingIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV7BrowserTracingIntegration, withSentryReactRouterV7Routing } from '@sentry/react/reactrouterv7'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -14,13 +16,13 @@ import Index from './pages/Index'; import SSE from './pages/SSE'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV7BrowserTracingIntegration({ + reactRouterV7BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -28,7 +30,7 @@ Sentry.init({ matchRoutes, trackFetchStreamPerformance: true, }), - Sentry.spanStreamingIntegration(), + spanStreamingIntegration(), replay, ], // We recommend adjusting this value in production, or using tracesSampler @@ -43,7 +45,7 @@ Sentry.init({ dataCollection: { userInfo: true }, }); -const SentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes); +const SentryRoutes = withSentryReactRouterV7Routing(Routes); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx index 202de7be19b4..598ffc692d98 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterBrowserTracingIntegration, wrapReactRouterRouting } from '@sentry/react/reactrouter.compat'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -15,13 +17,13 @@ import Products from './pages/Products'; import SSE from './pages/SSE'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterBrowserTracingIntegration({ + reactRouterBrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -43,7 +45,7 @@ Sentry.init({ dataCollection: { userInfo: true }, }); -const SentryRoutes = Sentry.wrapReactRouterRouting(Routes); +const SentryRoutes = wrapReactRouterRouting(Routes); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx index 46be7181b87c..c7888e90be73 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx @@ -1,4 +1,11 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { + reactRouterBrowserTracingIntegration, + wrapReactRouterRouting, + wrapUseRoutes, + wrapCreateBrowserRouter, +} from '@sentry/react/reactrouter.compat'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -15,13 +22,13 @@ import { } from 'react-router'; import Index from './pages/Index'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterBrowserTracingIntegration({ + reactRouterBrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -43,9 +50,9 @@ Sentry.init({ tunnel: 'http://localhost:3031', }); -const SentryRoutes = Sentry.wrapReactRouterRouting(Routes); -const sentryUseRoutes = Sentry.wrapUseRoutes(useRoutes); -const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter); +const SentryRoutes = wrapReactRouterRouting(Routes); +const sentryUseRoutes = wrapUseRoutes(useRoutes); +const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createBrowserRouter); const DetailsRoutes = () => sentryUseRoutes([ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx index 202de7be19b4..598ffc692d98 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterBrowserTracingIntegration, wrapReactRouterRouting } from '@sentry/react/reactrouter.compat'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -15,13 +17,13 @@ import Products from './pages/Products'; import SSE from './pages/SSE'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterBrowserTracingIntegration({ + reactRouterBrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -43,7 +45,7 @@ Sentry.init({ dataCollection: { userInfo: true }, }); -const SentryRoutes = Sentry.wrapReactRouterRouting(Routes); +const SentryRoutes = wrapReactRouterRouting(Routes); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx index 3a87a53ffdfa..609ca37cbaee 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx @@ -1,4 +1,6 @@ import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/react/optional-browser-api'; +import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing } from '@sentry/react/reactrouterv6'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { @@ -13,13 +15,13 @@ import { import Index from './pages/Index'; import User from './pages/User'; -const replay = Sentry.replayIntegration(); +const replay = replayIntegration(); Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - Sentry.reactRouterV6BrowserTracingIntegration({ + reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, @@ -59,7 +61,7 @@ Sentry.addEventProcessor(event => { return event; }); -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); +const SentryRoutes = withSentryReactRouterV6Routing(Routes); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx b/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx index 3c2ed2905383..5088d3f91b6f 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; import { Link, Outlet, @@ -96,7 +97,7 @@ declare const __APP_DSN__: string; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: __APP_DSN__, - integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)], + integrations: [tanstackRouterBrowserTracingIntegration(router)], // We recommend adjusting this value in production, or using tracesSampler // for finer control tracesSampleRate: 1.0, diff --git a/packages/browser/package.json b/packages/browser/package.json index 18b3bad89dca..ebdd8328c5b1 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -32,12 +32,31 @@ "import": "./build/npm/esm/prod/index.js", "require": "./build/npm/cjs/prod/index.js" } + }, + "./core": { + "types": "./build/npm/types/core.d.ts", + "react-native": "./build/npm/esm/prod/core.js", + "development": { + "import": "./build/npm/esm/dev/core.js", + "require": "./build/npm/cjs/dev/core.js" + }, + "production": { + "import": "./build/npm/esm/prod/core.js", + "require": "./build/npm/cjs/prod/core.js" + }, + "default": { + "import": "./build/npm/esm/prod/core.js", + "require": "./build/npm/cjs/prod/core.js" + } } }, "typesVersions": { "<5.0": { "build/npm/types/index.d.ts": [ "build/npm/types-ts3.8/index.d.ts" + ], + "core": [ + "build/npm/types-ts3.8/core.d.ts" ] } }, @@ -69,7 +88,7 @@ "build:bundle:watch": "rollup -c rollup.bundle.config.mjs --watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts", + "circularDepCheck": "madge --circular src/index.ts && madge --circular src/core.ts", "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", "lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware", "lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware", diff --git a/packages/browser/rollup.npm.config.mjs b/packages/browser/rollup.npm.config.mjs index a9df26ae0bae..96403071be2a 100644 --- a/packages/browser/rollup.npm.config.mjs +++ b/packages/browser/rollup.npm.config.mjs @@ -4,6 +4,8 @@ export default makeNPMConfigVariants( makeBaseNPMConfig({ // packages with bundles have a different build directory structure hasBundles: true, + // `core` is a slim entry used by framework SDKs for tree-shakeable re-exports. + entrypoints: ['src/index.ts', 'src/core.ts'], packageSpecificConfig: { output: { // set exports to 'named' or 'auto' so that rollup doesn't warn diff --git a/packages/browser/src/core.ts b/packages/browser/src/core.ts new file mode 100644 index 000000000000..2c142cd1d6e7 --- /dev/null +++ b/packages/browser/src/core.ts @@ -0,0 +1,57 @@ +/** + * Tree-shakeable core browser SDK surface (`@sentry/browser/core`). + * + * Includes error monitoring, default integrations, and the tracing APIs that + * almost every production app needs — but **not** optional heavy features + * (Session Replay, Feedback UI, AI instrumenters, feature-flag integrations, + * profiling, etc.). + * + * Framework SDKs re-export from this entry so that bundlers which materialize + * a full dynamic-import namespace (e.g. Rolldown when destructuring + * `await import('@sentry/react')`) cannot pull optional packages into the + * critical path. + */ +export * from './exports'; + +export { cultureContextIntegration } from './integrations/culturecontext'; +export { normalizeStringifyValue } from './normalizeStringifyValue'; +export { getAbsoluteUrl } from '@sentry/browser-utils'; + +// --- Tracing (commonly used; still tree-shakeable if unused) --- +export { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './tracing/request'; +export type { RequestInstrumentationOptions } from './tracing/request'; +export { + browserTracingIntegration, + isBotUserAgent, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} from './tracing/browserTracingIntegration'; +export { reportPageLoaded } from './tracing/reportPageLoaded'; +export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; + +// --- Filtering / enrichment used by most production setups --- +export { + registerSpanErrorInstrumentation, + getActiveSpan, + getRootSpan, + startSpan, + startInactiveSpan, + startSpanManual, + withActiveSpan, + startNewTrace, + bindScopeToEmitter, + getSpanDescendants, + setMeasurement, + getSpanStatusFromHttpCode, + setHttpStatus, + makeMultiplexedTransport, + MULTIPLEXED_TRANSPORT_EXTRA_KEY, + moduleMetadataIntegration, + thirdPartyErrorFilterIntegration, + captureConsoleIntegration, + extraErrorDataIntegration, + rewriteFramesIntegration, + consoleLoggingIntegration, + logger, +} from '@sentry/core/browser'; +export type { Span } from '@sentry/core/browser'; diff --git a/packages/gatsby/src/index.ts b/packages/gatsby/src/index.ts index 2299b46b7d64..f2f0503a9ac1 100644 --- a/packages/gatsby/src/index.ts +++ b/packages/gatsby/src/index.ts @@ -2,5 +2,13 @@ // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ export * from '@sentry/react'; +// Optional browser features and Redux are no longer star-exported through +// `@sentry/react`. Re-export them so `import * as Sentry from '@sentry/gatsby'` +// keeps the historical surface (including `createReduxEnhancer`). +export * from '@sentry/react/optional-browser-api'; +// `uiProfiler` is on both `@sentry/react` and `optional-browser-api`; dual +// `export *` would omit the name under ESM rules, so re-export it explicitly. +export { uiProfiler } from '@sentry/react'; +export { createReduxEnhancer } from '@sentry/react/redux'; export { init } from './sdk'; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index b7f9c482b816..410c11b7aa4e 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -16,6 +16,16 @@ import { removeIsrSsgTraceMetaTags } from './routing/isrRoutingTracing'; import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; +// Optional browser features and Redux are no longer star-exported through +// `@sentry/react` (so the React entry tree-shakes under Rolldown dynamic imports). +// Re-export them here so `import * as Sentry from '@sentry/nextjs'` keeps the +// historical surface (including `createReduxEnhancer`, which is dual-declared +// against the client SDK in `index.types.ts`). +export * from '@sentry/react/optional-browser-api'; +// `uiProfiler` is on both `@sentry/react` and `optional-browser-api`; dual +// `export *` would omit the name under ESM rules, so re-export it explicitly. +export { uiProfiler } from '@sentry/react'; +export { createReduxEnhancer } from '@sentry/react/redux'; export * from '../common'; export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error'; diff --git a/packages/react/README.md b/packages/react/README.md index b8d9879aa231..4ea7b3eadb42 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -12,8 +12,37 @@ ## General -This package is a wrapper around `@sentry/browser`, with added functionality related to React. All methods available in -`@sentry/browser` can be imported from `@sentry/react`. +This package is a wrapper around `@sentry/browser`, with added functionality related to React. + +The default `@sentry/react` entry is intentionally **tree-shakeable**: it re-exports core error monitoring and common +tracing APIs, but **not** optional heavy browser features (Session Replay, User Feedback UI, AI instrumenters, feature +flag integrations, …) or router/Redux helpers. Import those from a dedicated entry when you need them: + +```javascript +import * as Sentry from '@sentry/react'; +import { replayIntegration } from '@sentry/browser'; +// or: import { replayIntegration } from '@sentry/react/optional-browser-api'; +// or: import { replayIntegration } from '@sentry/replay'; + +Sentry.init({ + dsn: '__DSN__', + integrations: [replayIntegration()], +}); +``` + +Router and Redux integrations are on package subpaths so they can be code-split: + +```javascript +import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; +import { createReduxEnhancer } from '@sentry/react/redux'; +import { reactRouterV6BrowserTracingIntegration } from '@sentry/react/reactrouterv6'; +``` + +**Breaking change:** symbols that previously came from the `@sentry/react` root +(`replayIntegration`, `feedbackIntegration`, router helpers, `createReduxEnhancer`, …) must now be imported from +`@sentry/browser`, `@sentry/react/optional-browser-api`, or the matching subpath above. Framework SDKs +(`@sentry/nextjs`, `@sentry/gatsby`) still re-export the optional browser surface and `createReduxEnhancer` so +`import * as Sentry from '@sentry/nextjs'` keeps working for those symbols. To use this SDK, call `Sentry.init(options)` before you mount your React component. diff --git a/packages/react/optional-browser-api.d.ts b/packages/react/optional-browser-api.d.ts new file mode 100644 index 000000000000..30eebdb67d51 --- /dev/null +++ b/packages/react/optional-browser-api.d.ts @@ -0,0 +1 @@ +export * from './build/types/optional-browser-api'; diff --git a/packages/react/package.json b/packages/react/package.json index eed0a949a151..45dd4b3d6aa1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -10,7 +10,8 @@ "node": ">=18" }, "files": [ - "/build" + "/build", + "/*.d.ts" ], "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -30,6 +31,86 @@ "types": "./build/types/index.d.ts", "default": "./build/cjs/index.js" } + }, + "./optional-browser-api": { + "import": { + "types": "./optional-browser-api.d.ts", + "default": "./build/esm/optional-browser-api.js" + }, + "require": { + "types": "./optional-browser-api.d.ts", + "default": "./build/cjs/optional-browser-api.js" + } + }, + "./tanstackrouter": { + "import": { + "types": "./tanstackrouter.d.ts", + "default": "./build/esm/tanstackrouter.js" + }, + "require": { + "types": "./tanstackrouter.d.ts", + "default": "./build/cjs/tanstackrouter.js" + } + }, + "./redux": { + "import": { + "types": "./redux.d.ts", + "default": "./build/esm/redux.js" + }, + "require": { + "types": "./redux.d.ts", + "default": "./build/cjs/redux.js" + } + }, + "./reactrouterv3": { + "import": { + "types": "./reactrouterv3.d.ts", + "default": "./build/esm/reactrouterv3.js" + }, + "require": { + "types": "./reactrouterv3.d.ts", + "default": "./build/cjs/reactrouterv3.js" + } + }, + "./reactrouter": { + "import": { + "types": "./reactrouter.d.ts", + "default": "./build/esm/reactrouter.js" + }, + "require": { + "types": "./reactrouter.d.ts", + "default": "./build/cjs/reactrouter.js" + } + }, + "./reactrouterv6": { + "import": { + "types": "./reactrouterv6.d.ts", + "default": "./build/esm/reactrouterv6.js" + }, + "require": { + "types": "./reactrouterv6.d.ts", + "default": "./build/cjs/reactrouterv6.js" + } + }, + "./reactrouterv7": { + "import": { + "types": "./reactrouterv7.d.ts", + "default": "./build/esm/reactrouterv7.js" + }, + "require": { + "types": "./reactrouterv7.d.ts", + "default": "./build/cjs/reactrouterv7.js" + } + }, + "./reactrouter.compat": { + "import": { + "types": "./reactrouter.compat.d.ts", + "default": "./build/esm/reactrouter.compat.js" + }, + "require": { + "types": "./reactrouter.compat.d.ts", + "default": "./build/cjs/reactrouter.compat.js" + } } }, "typesVersions": { @@ -82,7 +163,7 @@ "build:dev:watch": "yarn build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts", + "circularDepCheck": "madge --circular src/index.ts && madge --circular src/tanstackrouter.ts && madge --circular src/redux.ts", "clean": "rimraf build coverage sentry-react-*.tgz", "lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware", "lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware", diff --git a/packages/react/reactrouter.compat.d.ts b/packages/react/reactrouter.compat.d.ts new file mode 100644 index 000000000000..ef0a756b14cc --- /dev/null +++ b/packages/react/reactrouter.compat.d.ts @@ -0,0 +1 @@ +export * from './build/types/reactrouter.compat'; diff --git a/packages/react/reactrouter.d.ts b/packages/react/reactrouter.d.ts new file mode 100644 index 000000000000..299f84a46ed4 --- /dev/null +++ b/packages/react/reactrouter.d.ts @@ -0,0 +1 @@ +export * from './build/types/reactrouter'; diff --git a/packages/react/reactrouterv3.d.ts b/packages/react/reactrouterv3.d.ts new file mode 100644 index 000000000000..517a29703bcd --- /dev/null +++ b/packages/react/reactrouterv3.d.ts @@ -0,0 +1 @@ +export * from './build/types/reactrouterv3'; diff --git a/packages/react/reactrouterv6.d.ts b/packages/react/reactrouterv6.d.ts new file mode 100644 index 000000000000..4613606f5a7b --- /dev/null +++ b/packages/react/reactrouterv6.d.ts @@ -0,0 +1 @@ +export * from './build/types/reactrouterv6'; diff --git a/packages/react/reactrouterv7.d.ts b/packages/react/reactrouterv7.d.ts new file mode 100644 index 000000000000..16e168e3bb19 --- /dev/null +++ b/packages/react/reactrouterv7.d.ts @@ -0,0 +1 @@ +export * from './build/types/reactrouterv7'; diff --git a/packages/react/redux.d.ts b/packages/react/redux.d.ts new file mode 100644 index 000000000000..fe5edaed00ff --- /dev/null +++ b/packages/react/redux.d.ts @@ -0,0 +1 @@ +export * from './build/types/redux'; diff --git a/packages/react/rollup.npm.config.mjs b/packages/react/rollup.npm.config.mjs index 66c3b16aba58..2bf245a95cb1 100644 --- a/packages/react/rollup.npm.config.mjs +++ b/packages/react/rollup.npm.config.mjs @@ -6,6 +6,19 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu // https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html export default makeNPMConfigVariants( makeBaseNPMConfig({ + // Separate entrypoints for router/redux integrations so they can be imported + // via package subpaths without sitting on the main export list (tree-shaking). + entrypoints: [ + 'src/index.ts', + 'src/optional-browser-api.ts', + 'src/tanstackrouter.ts', + 'src/redux.ts', + 'src/reactrouterv3.ts', + 'src/reactrouter.tsx', + 'src/reactrouterv6.tsx', + 'src/reactrouterv7.tsx', + 'src/reactrouter.compat.tsx', + ], packageSpecificConfig: { external: ['react', 'react/jsx-runtime'], }, diff --git a/packages/react/src/browser-api.ts b/packages/react/src/browser-api.ts new file mode 100644 index 000000000000..610fa7a46f5a --- /dev/null +++ b/packages/react/src/browser-api.ts @@ -0,0 +1,167 @@ +/** + * Browser APIs re-exported from `@sentry/react`. + * + * This is intentionally a **curated** surface rather than `export * from '@sentry/browser'`. + * + * Why: bundlers that treat a dynamic `import('@sentry/react')` namespace as using every + * export (Rolldown when the namespace is destructured) would otherwise pull optional + * `@sentry/browser` features into the critical path — Replay (~265KB), Feedback, AI + * instrumenters, feature-flag integrations, etc. — even when the app never references them. + * + * Optional features remain available from `@sentry/browser` (full entry), + * `@sentry/react/optional-browser-api`, or dedicated packages (`@sentry/replay`, …). + * + * Everything here is re-exported from `@sentry/browser/core` so this module never + * links the full browser barrel. + */ + +export type { + Breadcrumb, + BreadcrumbHint, + BrowserOptions, + CaptureContext, + Context, + Contexts, + ErrorEvent, + Event, + EventHint, + Exception, + ExclusiveEventHintOrCaptureContext, + Log, + LogSeverityLevel, + ReportDialogOptions, + RequestEventData, + RequestInstrumentationOptions, + SdkInfo, + Session, + SeverityLevel, + Span, + StackFrame, + Stacktrace, + Thread, + User, +} from '@sentry/browser/core'; + +export { + // Core client / scope APIs + addBreadcrumb, + addEventProcessor, + addIntegration, + BrowserClient, + captureEvent, + captureException, + captureFeedback, + captureMessage, + captureSession, + close, + continueTrace, + createTransport, + createUserFeedbackEnvelope, + endSession, + eventFromException, + eventFromMessage, + exceptionFromError, + flush, + forceLoad, + getClient, + getCurrentScope, + getDefaultIntegrations, + getGlobalScope, + getIsolationScope, + getTraceData, + isEnabled, + isInitialized, + lastEventId, + // eslint-disable-next-line typescript/no-deprecated + inboundFiltersIntegration, + eventFiltersIntegration, + functionToStringIntegration, + dedupeIntegration, + // Default browser integrations + breadcrumbsIntegration, + browserApiErrorsIntegration, + browserSessionIntegration, + cultureContextIntegration, + globalHandlersIntegration, + httpContextIntegration, + linkedErrorsIntegration, + // Transport / parsing + defaultStackLineParsers, + defaultStackParser, + chromeStackLineParser, + geckoStackLineParser, + opera10StackLineParser, + opera11StackLineParser, + winjsStackLineParser, + makeFetchTransport, + // Manual UI profiling API (pairs with browserProfilingIntegration from + // optional-browser-api / @sentry/browser). Was on the pre-curation public + // surface via export * from @sentry/browser. + uiProfiler, + getAbsoluteUrl, + // Scope helpers + metrics, + onLoad, + parameterize, + Scope, + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + setAttribute, + setAttributes, + setContext, + setConversationId, + setCurrentClient, + setExtra, + setExtras, + setTag, + setTags, + setUser, + showReportDialog, + spanToBaggageHeader, + spanToJSON, + spanToTraceHeader, + startSession, + suppressTracing, + updateSpanName, + withIsolationScope, + withScope, + withStreamedSpan, + WINDOW, + lazyLoadIntegration, + normalizeStringifyValue, + // Tracing + browserTracingIntegration, + defaultRequestInstrumentationOptions, + instrumentOutgoingRequests, + isBotUserAgent, + reportPageLoaded, + setActiveSpanInBrowser, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + bindScopeToEmitter, + getActiveSpan, + getRootSpan, + getSpanDescendants, + getSpanStatusFromHttpCode, + registerSpanErrorInstrumentation, + setHttpStatus, + setMeasurement, + startInactiveSpan, + startNewTrace, + startSpan, + startSpanManual, + withActiveSpan, + // Filtering / enrichment + thirdPartyErrorFilterIntegration, + moduleMetadataIntegration, + rewriteFramesIntegration, + captureConsoleIntegration, + extraErrorDataIntegration, + consoleLoggingIntegration, + logger, + makeMultiplexedTransport, + MULTIPLEXED_TRANSPORT_EXTRA_KEY, +} from '@sentry/browser/core'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a25f05e62a2f..7e2d9318709b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,41 +1,34 @@ // import/export got a false positive, and affects most of our index barrel files // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ -export * from '@sentry/browser'; + +/** + * @sentry/react public entry — tree-shakeable by default. + * + * Deliberately does **not** use `export * from '@sentry/browser'`. That star-export + * put every optional browser feature (Session Replay, Feedback UI, AI SDKs, feature + * flags, …) on this module's export list. Bundlers that materialize the full + * namespace for `import('@sentry/react')` (Rolldown when destructuring the result) + * then shipped hundreds of KB of unused code on the critical path. + * + * Optional browser features: import from `@sentry/browser` or the dedicated package. + * Router-specific integrations: import from a subpath, e.g. + * `@sentry/react/tanstackrouter`. + */ + +export * from './browser-api'; export { init } from './sdk'; export { captureReactException, reactErrorHandler } from './error'; export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; -export { createReduxEnhancer } from './redux'; -export { reactRouterV3BrowserTracingIntegration } from './reactrouterv3'; -export { tanstackRouterBrowserTracingIntegration } from './tanstackrouter'; -export { - withSentryRouting, - reactRouterV4BrowserTracingIntegration, - reactRouterV5BrowserTracingIntegration, -} from './reactrouter'; -/* oxlint-disable typescript/no-deprecated -- Intentional re-exports for backwards compatibility */ -export { - reactRouterV6BrowserTracingIntegration, - withSentryReactRouterV6Routing, - wrapUseRoutesV6, - wrapCreateBrowserRouterV6, - wrapCreateMemoryRouterV6, -} from './reactrouterv6'; -export { - reactRouterV7BrowserTracingIntegration, - withSentryReactRouterV7Routing, - wrapCreateBrowserRouterV7, - wrapCreateMemoryRouterV7, - wrapUseRoutesV7, -} from './reactrouterv7'; -/* oxlint-enable typescript/no-deprecated */ -export { - reactRouterBrowserTracingIntegration, - wrapReactRouterRouting, - wrapCreateBrowserRouter, - wrapCreateMemoryRouter, - wrapUseRoutes, -} from './reactrouter.compat'; + +// Router / Redux integrations live on subpaths so they are not part of this +// entry's export list (see package.json "exports"). Re-exporting them here would +// re-introduce the tree-shaking hole for dynamic `import('@sentry/react')`. +// +// import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; +// import { createReduxEnhancer } from '@sentry/react/redux'; +// import { reactRouterV6BrowserTracingIntegration } from '@sentry/react/reactrouterv6'; +// … diff --git a/packages/react/src/optional-browser-api.ts b/packages/react/src/optional-browser-api.ts new file mode 100644 index 000000000000..82f41a0753d6 --- /dev/null +++ b/packages/react/src/optional-browser-api.ts @@ -0,0 +1,76 @@ +/** + * Optional `@sentry/browser` features that are **not** part of the default + * `@sentry/react` entry (for tree-shaking). + * + * Framework SDKs (`@sentry/nextjs`, `@sentry/gatsby`, …) re-export these so + * `import * as Sentry from '@sentry/nextjs'` keeps the historical surface. + * + * Application code may also import from here, or directly from `@sentry/browser` + * / dedicated packages (`@sentry/replay`, `@sentry/feedback`, …). + */ +export { + // Replay + getReplay, + replayCanvasIntegration, + replayIntegration, + // Feedback + feedbackAsyncIntegration, + feedbackIntegration, + feedbackSyncIntegration, + getFeedback, + sendFeedback, + // Optional integrations + // uiProfiler is also on the root surface; re-export here so + // `import { uiProfiler, browserProfilingIntegration } from '@sentry/react/optional-browser-api'` + // keeps working for manual UI profiling setups. + browserProfilingIntegration, + uiProfiler, + contextLinesIntegration, + createConsolaReporter, + diagnoseSdkConnectivity, + elementTimingIntegration, + featureFlagsIntegration, + fetchStreamPerformanceIntegration, + graphqlClientIntegration, + httpClientIntegration, + makeBrowserOfflineTransport, + registerWebWorker, + reportingObserverIntegration, + spanStreamingIntegration, + spotlightBrowserIntegration, + supabaseIntegration, + instrumentSupabaseClient, + viewHierarchyIntegration, + webVitalsIntegration, + webWorkerIntegration, + zodErrorsIntegration, + // Feature flags + buildLaunchDarklyFlagUsedHandler, + growthbookIntegration, + launchDarklyIntegration, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, + // AI instrumenters + createLangChainCallbackHandler, + instrumentAnthropicAiClient, + instrumentCreateReactAgent, + instrumentGoogleGenAIClient, + instrumentLangChainEmbeddings, + instrumentLangGraph, + instrumentOpenAiClient, +} from '@sentry/browser'; + +export type { + FeatureFlagsIntegration, + ReplayBreadcrumbFrame, + ReplayBreadcrumbFrameEvent, + ReplayEventType, + ReplayEventWithTime, + ReplayFrame, + ReplayFrameEvent, + ReplayOptionFrameEvent, + ReplaySpanFrame, + ReplaySpanFrameEvent, +} from '@sentry/browser'; diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 42993182cd55..fffa8c6f90ed 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -25,8 +25,25 @@ interface TanstackRouterLocation { * * The minimum compatible version of `@tanstack/react-router` is `1.64.0`. * + * Import from `@sentry/react/tanstackrouter` (not the package root) so this code is not + * part of the main `@sentry/react` export list. That keeps dynamic + * `import('@sentry/react')` namespaces tree-shakeable. + * * @param router A TanStack Router `Router` instance that should be used for routing instrumentation. * @param options Sentry browser tracing configuration. + * + * @example + * ```ts + * import { init, addIntegration } from '@sentry/react'; + * import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; + * + * init({ dsn: '...' }); + * + * // Safe to defer: only the tanstack subpath is loaded + * void import('@sentry/react/tanstackrouter').then(({ tanstackRouterBrowserTracingIntegration }) => { + * addIntegration(tanstackRouterBrowserTracingIntegration(router)); + * }); + * ``` */ export function tanstackRouterBrowserTracingIntegration( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/tanstackrouter.d.ts b/packages/react/tanstackrouter.d.ts new file mode 100644 index 000000000000..fd91c8d1b102 --- /dev/null +++ b/packages/react/tanstackrouter.d.ts @@ -0,0 +1 @@ +export * from './build/types/tanstackrouter'; diff --git a/packages/react/test/exports.treeshake.test.ts b/packages/react/test/exports.treeshake.test.ts new file mode 100644 index 000000000000..9da465e8ae6d --- /dev/null +++ b/packages/react/test/exports.treeshake.test.ts @@ -0,0 +1,65 @@ +/** + * @vitest-environment node + * + * Guard the curated `@sentry/react` export surface. + * + * Optional heavy browser features must not appear on the package root so that + * bundlers which materialize the full module namespace for + * `import('@sentry/react')` (Rolldown + destructuring) cannot pull them into + * the critical path. + */ +import { describe, expect, it } from 'vitest'; +import * as SentryReact from '../src/index'; + +describe('@sentry/react export surface (tree-shaking)', () => { + it('exposes core error monitoring and tracing APIs', () => { + expect(typeof SentryReact.init).toBe('function'); + expect(typeof SentryReact.captureException).toBe('function'); + expect(typeof SentryReact.browserApiErrorsIntegration).toBe('function'); + expect(typeof SentryReact.breadcrumbsIntegration).toBe('function'); + expect(typeof SentryReact.browserTracingIntegration).toBe('function'); + expect(typeof SentryReact.thirdPartyErrorFilterIntegration).toBe('function'); + expect(typeof SentryReact.ErrorBoundary).toBe('function'); + expect(typeof SentryReact.reactErrorHandler).toBe('function'); + // Public API previously re-exported via export * from @sentry/browser + expect(SentryReact.uiProfiler).toBeDefined(); + expect(typeof SentryReact.uiProfiler.startProfiler).toBe('function'); + expect(typeof SentryReact.uiProfiler.stopProfiler).toBe('function'); + expect(typeof SentryReact.getAbsoluteUrl).toBe('function'); + }); + + it('does not export optional heavy browser features from the root entry', () => { + const root = SentryReact as Record; + + // These live on `@sentry/browser` / dedicated packages / optional-browser-api + expect(root.replayIntegration).toBeUndefined(); + expect(root.getReplay).toBeUndefined(); + expect(root.feedbackIntegration).toBeUndefined(); + expect(root.getFeedback).toBeUndefined(); + expect(root.instrumentOpenAiClient).toBeUndefined(); + expect(root.instrumentAnthropicAiClient).toBeUndefined(); + expect(root.launchDarklyIntegration).toBeUndefined(); + expect(root.browserProfilingIntegration).toBeUndefined(); + expect(root.diagnoseSdkConnectivity).toBeUndefined(); + }); + + it('re-exports uiProfiler from optional-browser-api with browserProfilingIntegration', async () => { + const optional = await import('../src/optional-browser-api'); + expect(optional.uiProfiler).toBeDefined(); + expect(typeof optional.browserProfilingIntegration).toBe('function'); + expect(typeof optional.featureFlagsIntegration).toBe('function'); + // Type-only export is erased at runtime; the value companion is what consumers use + // with getIntegrationByName(...). + expect(optional).toHaveProperty('featureFlagsIntegration'); + }); + + it('does not export router integrations from the root entry', () => { + const root = SentryReact as Record; + + // Import from `@sentry/react/tanstackrouter`, `/reactrouterv6`, etc. + expect(root.tanstackRouterBrowserTracingIntegration).toBeUndefined(); + expect(root.reactRouterV6BrowserTracingIntegration).toBeUndefined(); + expect(root.reactRouterV7BrowserTracingIntegration).toBeUndefined(); + expect(root.createReduxEnhancer).toBeUndefined(); + }); +}); diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 779adfb31559..69f23c8c5f61 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -16,7 +16,8 @@ import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-4'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { BrowserClient, reactRouterV4BrowserTracingIntegration, withSentryRouting } from '../src'; +import { BrowserClient } from '../src'; +import { reactRouterV4BrowserTracingIntegration, withSentryRouting } from '../src/reactrouter'; import type { RouteConfig } from '../src/reactrouter'; const mockStartBrowserTracingPageLoadSpan = vi.fn(); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 7841833fb6cc..7cfa8b41c88e 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -16,7 +16,8 @@ import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { BrowserClient, reactRouterV5BrowserTracingIntegration, withSentryRouting } from '../src'; +import { BrowserClient } from '../src'; +import { reactRouterV5BrowserTracingIntegration, withSentryRouting } from '../src/reactrouter'; import type { RouteConfig } from '../src/reactrouter'; const mockStartBrowserTracingPageLoadSpan = vi.fn(); diff --git a/packages/remix/src/client/index.ts b/packages/remix/src/client/index.ts index 65e6b9fbc184..e7e2f05e1d7f 100644 --- a/packages/remix/src/client/index.ts +++ b/packages/remix/src/client/index.ts @@ -5,6 +5,14 @@ import { debug } from '@sentry/core'; import { DEBUG_BUILD } from '../utils/debug-build'; export * from '@sentry/react'; +// Optional browser features and Redux are no longer star-exported through +// `@sentry/react`. Re-export them so `import * as Sentry from '@sentry/remix'` +// keeps the historical surface. +export * from '@sentry/react/optional-browser-api'; +// `uiProfiler` is on both `@sentry/react` and `optional-browser-api`; dual +// `export *` would omit the name under ESM rules, so re-export it explicitly. +export { uiProfiler } from '@sentry/react'; +export { createReduxEnhancer } from '@sentry/react/redux'; export { init } from './sdk'; export { captureRemixErrorBoundaryError } from './errors'; diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index ef2f89cee2a5..a2c14fb07151 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -8,6 +8,13 @@ import { } from '../server/instrumentServer'; export * from '@sentry/react'; +// Optional browser features and Redux are no longer star-exported through +// `@sentry/react`. Re-export them so the cloudflare entry keeps the historical surface. +export * from '@sentry/react/optional-browser-api'; +// `uiProfiler` is on both `@sentry/react` and `optional-browser-api`; dual +// `export *` would omit the name under ESM rules, so re-export it explicitly. +export { uiProfiler } from '@sentry/react'; +export { createReduxEnhancer } from '@sentry/react/redux'; export { captureRemixErrorBoundaryError } from '../client/errors'; export { withSentry } from '../client/performance'; diff --git a/packages/tanstackstart-react/src/client/index.ts b/packages/tanstackstart-react/src/client/index.ts index 73c7e4cf7b24..47cbe9a5347b 100644 --- a/packages/tanstackstart-react/src/client/index.ts +++ b/packages/tanstackstart-react/src/client/index.ts @@ -9,6 +9,15 @@ import type { import type { CreateSentryTunnelRouteOptions } from '../server/tunnelRoute'; export * from '@sentry/react'; +// Optional browser features and router helpers are no longer star-exported through +// `@sentry/react`. Re-export them so `import * as Sentry from '@sentry/tanstackstart-react'` +// keeps the historical surface (including `tanstackRouterBrowserTracingIntegration`, +// which is dual-declared against the client SDK in `index.types.ts`). +export * from '@sentry/react/optional-browser-api'; +// `uiProfiler` is on both `@sentry/react` and `optional-browser-api`; dual +// `export *` would omit the name under ESM rules, so re-export it explicitly. +export { uiProfiler } from '@sentry/react'; +export { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter'; export { init } from './sdk';