From 6633ca78c4a9e3a2f7d8f7a908620e9b739fcbe0 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 23 Jul 2026 14:04:53 +0200 Subject: [PATCH 1/2] feat(react-router)!: Remove server OTel instrumentation Remove the server-side OTel instrumentation now that the instrumentation API is the default and the minimum React Router version is 7.15. Deletes the `InstrumentationBase` `createRequestHandler` proxy and the OTel data-loader span-creation path (plus its Node-version gate). The `http.route: '*'` cleanup in `processEvent`/`processSegmentSpan` stays (simplified) - the bogus wildcard comes from the `@sentry/node` HTTP root span, not our proxy, and routes without a loader/action still need it stripped. Ref #22290 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../performance/performance.server.test.ts | 15 ++ .../src/server/instrumentation/reactRouter.ts | 142 ------------- .../src/server/instrumentation/util.ts | 53 ----- .../server/integration/reactRouterServer.ts | 62 +----- .../react-router/src/server/serverGlobals.ts | 18 -- .../src/vite/makeServerBuildCapturePlugin.ts | 3 + .../createServerInstrumentation.test.ts | 5 +- .../instrumentation/reactRouterServer.test.ts | 190 ------------------ .../integration/reactRouterServer.test.ts | 165 +-------------- 9 files changed, 37 insertions(+), 616 deletions(-) delete mode 100644 packages/react-router/src/server/instrumentation/reactRouter.ts delete mode 100644 packages/react-router/src/server/instrumentation/util.ts delete mode 100644 packages/react-router/test/server/instrumentation/reactRouterServer.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts index 582dd4771cd9..9bedfa35e212 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts @@ -167,4 +167,19 @@ test.describe('server - instrumentation API performance', () => { expect(httpServerTransactions).toEqual(['GET /performance']); }); + + test('strips the bogus "*" http.route on routes without a loader/action', async ({ page }) => { + // The `@sentry/node` HTTP root span matches React Router's catch-all handler with `http.route: '*'`. + // On routes without a loader/action nothing overwrites it, so the integration must strip it before send. + const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { + return transactionEvent.transaction === 'GET /performance/ssr'; + }); + + await page.goto(`/performance/ssr`); + + const transaction = await txPromise; + + expect(transaction.contexts?.trace?.op).toBe('http.server'); + expect(transaction.contexts?.trace?.data?.['http.route']).toBeUndefined(); + }); }); diff --git a/packages/react-router/src/server/instrumentation/reactRouter.ts b/packages/react-router/src/server/instrumentation/reactRouter.ts deleted file mode 100644 index 41a60a9740bc..000000000000 --- a/packages/react-router/src/server/instrumentation/reactRouter.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { HTTP_TARGET } from '@sentry/conventions/attributes'; -import { - debug, - getActiveSpan, - getRootSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - spanToJSON, - startSpan, - updateSpanName, -} from '@sentry/core'; -import type * as reactRouter from 'react-router'; -import { DEBUG_BUILD } from '../../common/debug-build'; -import { isServerBuildLike, setServerBuild } from '../serverBuild'; -import { isInstrumentationApiUsed, isOtelDataLoaderSpanCreationEnabled } from '../serverGlobals'; -import { getOpName, getSpanName, isDataRequest } from './util'; - -type ReactRouterModuleExports = typeof reactRouter; - -const supportedVersions = ['>=7.0.0']; -const COMPONENT = 'react-router'; - -/** - * Instrumentation for React Router's server request handler. - * This patches the requestHandler function to add Sentry performance monitoring for data loaders. - */ -export class ReactRouterInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('ReactRouterInstrumentation', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the React Router server modules to be patched. - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - protected init(): InstrumentationNodeModuleDefinition { - const reactRouterServerModule = new InstrumentationNodeModuleDefinition( - COMPONENT, - supportedVersions, - (moduleExports: ReactRouterModuleExports) => { - return this._createPatchedModuleProxy(moduleExports); - }, - (_moduleExports: unknown) => { - // nothing to unwrap here - return _moduleExports; - }, - ); - - return reactRouterServerModule; - } - - /** - * Creates a proxy around the React Router module exports that patches the createRequestHandler function. - * This allows us to wrap the request handler to add performance monitoring for data loaders and actions. - */ - private _createPatchedModuleProxy(moduleExports: ReactRouterModuleExports): ReactRouterModuleExports { - return new Proxy(moduleExports, { - get(target, prop, receiver) { - if (prop === 'createRequestHandler') { - const original = target[prop]; - return function sentryWrappedCreateRequestHandler(this: unknown, ...args: unknown[]) { - // Capture the ServerBuild reference for middleware name lookup - const build = args[0]; - if (isServerBuildLike(build)) { - setServerBuild(build); - } else if (typeof build === 'function') { - // Build arg can be a factory function (dev mode HMR). Wrap to capture resolved build. - const originalBuildFn = build as () => unknown; - args[0] = async function sentryWrappedBuildFn() { - const resolvedBuild = await originalBuildFn(); - if (isServerBuildLike(resolvedBuild)) { - setServerBuild(resolvedBuild); - } - return resolvedBuild; - }; - } - - const originalRequestHandler = original.apply(this, args); - - return async function sentryWrappedRequestHandler(request: Request, initialContext?: unknown) { - // Skip OTEL span creation when instrumentation API is active or when span creation is not enabled. - // Checked per-request (not at handler-creation time) because in dev, createRequestHandler runs before entry.server.tsx. - if (isInstrumentationApiUsed() || !isOtelDataLoaderSpanCreationEnabled()) { - return originalRequestHandler(request, initialContext); - } - - let url: URL; - try { - url = new URL(request.url); - } catch { - return originalRequestHandler(request, initialContext); - } - - // We currently just want to trace loaders and actions - if (!isDataRequest(url.pathname)) { - return originalRequestHandler(request, initialContext); - } - - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - - if (!rootSpan) { - DEBUG_BUILD && debug.log('No active root span found, skipping tracing for data request'); - return originalRequestHandler(request, initialContext); - } - - // We cannot rely on the regular span name inferral here, as the express instrumentation sets `*` as the route - // So we force this to be a more sensible name here - // TODO: try to set derived parameterized route from build here (args[0]) - const spanData = spanToJSON(rootSpan); - // eslint-disable-next-line typescript/no-deprecated - const target = spanData.data[HTTP_TARGET] || url.pathname; - updateSpanName(rootSpan, `${request.method} ${target}`); - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.server', - }); - - return startSpan( - { - name: getSpanName(url.pathname, request.method), - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.server', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOpName(url.pathname, request.method), - }, - }, - () => { - return originalRequestHandler(request, initialContext); - }, - ); - }; - }; - } - return Reflect.get(target, prop, receiver); - }, - }); - } -} diff --git a/packages/react-router/src/server/instrumentation/util.ts b/packages/react-router/src/server/instrumentation/util.ts deleted file mode 100644 index 3cad321dcfcc..000000000000 --- a/packages/react-router/src/server/instrumentation/util.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Gets the op name for a request based on whether it's a loader or action request. - * @param pathName The URL pathname to check - * @param requestMethod The HTTP request method - */ -export function getOpName(pathName: string, requestMethod: string): string { - return isLoaderRequest(pathName, requestMethod) - ? 'function.react_router.loader' - : isActionRequest(pathName, requestMethod) - ? 'function.react_router.action' - : 'function.react_router'; -} - -/** - * Gets the span name for a request based on whether it's a loader or action request. - * @param pathName The URL pathname to check - * @param requestMethod The HTTP request method - */ -export function getSpanName(pathName: string, requestMethod: string): string { - return isLoaderRequest(pathName, requestMethod) - ? 'Executing Server Loader' - : isActionRequest(pathName, requestMethod) - ? 'Executing Server Action' - : 'Unknown Data Request'; -} - -/** - * Checks if the request is a server loader request - * @param pathname The URL pathname to check - * @param requestMethod The HTTP request method - */ -export function isLoaderRequest(pathname: string, requestMethod: string): boolean { - return isDataRequest(pathname) && requestMethod === 'GET'; -} - -/** - * Checks if the request is a server action request - * @param pathname The URL pathname to check - * @param requestMethod The HTTP request method - */ -export function isActionRequest(pathname: string, requestMethod: string): boolean { - return isDataRequest(pathname) && requestMethod === 'POST'; -} - -/** - * Checks if the request is a react-router data request - * @param pathname The URL pathname to check - */ -export function isDataRequest(pathname: string): boolean { - return pathname.endsWith('.data'); -} - -export const SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE = 'sentry.overwrite-route'; diff --git a/packages/react-router/src/server/integration/reactRouterServer.ts b/packages/react-router/src/server/integration/reactRouterServer.ts index 2558f8229a58..a2fee6176e50 100644 --- a/packages/react-router/src/server/integration/reactRouterServer.ts +++ b/packages/react-router/src/server/integration/reactRouterServer.ts @@ -1,23 +1,9 @@ import { HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { generateInstrumentOnce, NODE_VERSION } from '@sentry/node'; -import { ReactRouterInstrumentation } from '../instrumentation/reactRouter'; +import { defineIntegration } from '@sentry/core'; import { registerServerBuildGlobal } from '../serverBuild'; -import { enableOtelDataLoaderSpanCreation } from '../serverGlobals'; const INTEGRATION_NAME = 'ReactRouterServer' as const; -const instrumentReactRouter = generateInstrumentOnce(INTEGRATION_NAME, () => { - return new ReactRouterInstrumentation(); -}); - -export const instrumentReactRouterServer = Object.assign( - (): void => { - instrumentReactRouter(); - }, - { id: INTEGRATION_NAME }, -); - /** * Integration capturing tracing data for React Router server functions. */ @@ -25,57 +11,29 @@ export const reactRouterServerIntegration = defineIntegration(() => { return { name: INTEGRATION_NAME, setupOnce() { - // Register global for Vite plugin ServerBuild capture. Registered independently of the OTEL - // patch so this capture path keeps working once the OTEL instrumentation is removed. + // Register global for Vite plugin ServerBuild capture (used for middleware name resolution). registerServerBuildGlobal(); - - // Enable OTEL data-loader spans only on Node versions without the diagnostics_channel-based instrumentation API. - if ( - (NODE_VERSION.major === 20 && NODE_VERSION.minor < 19) || - (NODE_VERSION.major === 22 && NODE_VERSION.minor < 12) - ) { - enableOtelDataLoaderSpanCreation(); - } - - // Always install to capture ServerBuild for middleware names. - // Skips per-request wrapping when instrumentation API is active or OTEL span creation is disabled. - instrumentReactRouterServer(); }, processEvent(event) { - // Express generates bogus `*` routes for data loaders, which we want to remove here - // we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point + // The `@sentry/node` HTTP root span matches React Router's catch-all server handler, so it + // carries a bogus `http.route` of `*`. The instrumentation API sets a proper route on requests + // that hit a loader/action/middleware, but requests without one (e.g. SSR-only routes) keep the + // placeholder - strip it here so it doesn't leak into the transaction. if ( event.type === 'transaction' && event.contexts?.trace?.data && event.contexts.trace.data[HTTP_ROUTE] === '*' ) { - const origin = event.contexts.trace.origin; - const isInstrumentationApiOrigin = origin?.includes('instrumentation_api'); - - // For instrumentation_api, always clean up bogus `*` route since we set better names - // For legacy, only clean up if the name has been adjusted (not METHOD *) - if (isInstrumentationApiOrigin || !event.transaction?.endsWith(' *')) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete event.contexts.trace.data[HTTP_ROUTE]; - } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete event.contexts.trace.data[HTTP_ROUTE]; } return event; }, processSegmentSpan(span) { - // Express generates bogus `*` routes for data loaders, which we want to remove here - // we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point + // See `processEvent`: strip the bogus `*` route from the `@sentry/node` HTTP root span. const attributes = span.attributes; - if (attributes?.[HTTP_ROUTE] !== '*') { - return; - } - - const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; - const isInstrumentationApiOrigin = typeof origin === 'string' && origin.includes('instrumentation_api'); - - // For instrumentation_api, always clean up bogus `*` route since we set better names - // For legacy, only clean up if the name has been adjusted (not METHOD *) - if (isInstrumentationApiOrigin || !span.name?.endsWith(' *')) { + if (attributes?.[HTTP_ROUTE] === '*') { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete attributes[HTTP_ROUTE]; } diff --git a/packages/react-router/src/server/serverGlobals.ts b/packages/react-router/src/server/serverGlobals.ts index e7a7ce019442..f177c6a9ab6a 100644 --- a/packages/react-router/src/server/serverGlobals.ts +++ b/packages/react-router/src/server/serverGlobals.ts @@ -1,11 +1,9 @@ import { GLOBAL_OBJ } from '@sentry/core'; const SENTRY_SERVER_INSTRUMENTATION_FLAG = '__sentryReactRouterServerInstrumentationUsed'; -const SENTRY_OTEL_SPAN_CREATION_FLAG = '__sentryReactRouterOtelSpanCreationEnabled'; type GlobalObjWithFlag = typeof GLOBAL_OBJ & { [SENTRY_SERVER_INSTRUMENTATION_FLAG]?: boolean; - [SENTRY_OTEL_SPAN_CREATION_FLAG]?: boolean; }; /** @@ -22,19 +20,3 @@ export function markInstrumentationApiUsed(): void { export function isInstrumentationApiUsed(): boolean { return !!(GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_SERVER_INSTRUMENTATION_FLAG]; } - -/** - * Enable OTEL data-loader span creation for React Router server. - * @internal - */ -export function enableOtelDataLoaderSpanCreation(): void { - (GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_OTEL_SPAN_CREATION_FLAG] = true; -} - -/** - * Check if OTEL data-loader span creation is enabled for React Router server. - * @internal - */ -export function isOtelDataLoaderSpanCreationEnabled(): boolean { - return !!(GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_OTEL_SPAN_CREATION_FLAG]; -} diff --git a/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts b/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts index e7d081306bfb..d95129a491ff 100644 --- a/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts +++ b/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts @@ -18,6 +18,9 @@ export function makeServerBuildCapturePlugin(): Plugin { }, transform(code, id) { + // TODO: This only captures the server build for production SSR builds. Dev mode + // (`react-router dev`) is not covered yet, so middleware names may be missing there - this + // should be handled for dev too. if (!isSsrBuild) { return null; } diff --git a/packages/react-router/test/server/createServerInstrumentation.test.ts b/packages/react-router/test/server/createServerInstrumentation.test.ts index 6762be8e4e83..37143cb66a26 100644 --- a/packages/react-router/test/server/createServerInstrumentation.test.ts +++ b/packages/react-router/test/server/createServerInstrumentation.test.ts @@ -68,9 +68,8 @@ describe('createSentryServerInstrumentation', () => { createSentryServerInstrumentation(); - // Creating the instrumentation must not mark the API active. On React Router versions that - // don't support the instrumentations API, the registration callbacks are never invoked, so - // the legacy OTel data-loader path must stay active. + // Creating the instrumentation must not mark the API active - the flag should only flip once + // React Router actually invokes the registration callbacks. expect((globalThis as any).__sentryReactRouterServerInstrumentationUsed).toBeUndefined(); }); diff --git a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts b/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts deleted file mode 100644 index f9b9d88c1a2b..000000000000 --- a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import type { Span, SpanJSON } from '@sentry/core'; -import * as SentryCore from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter'; -import * as Util from '../../../src/server/instrumentation/util'; -import * as ServerBuild from '../../../src/server/serverBuild'; -import * as ServerGlobals from '../../../src/server/serverGlobals'; - -vi.mock('@sentry/core', async () => { - return { - getActiveSpan: vi.fn(), - getRootSpan: vi.fn(), - spanToJSON: vi.fn(), - updateSpanName: vi.fn(), - debug: { - log: vi.fn(), - }, - SDK_VERSION: '1.0.0', - SEMANTIC_ATTRIBUTE_SENTRY_OP: 'sentry.op', - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin', - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source', - startSpan: vi.fn((opts, fn) => fn({})), - GLOBAL_OBJ: {}, - }; -}); - -vi.mock('./util', async () => { - return { - getSpanName: vi.fn((pathname: string, method: string) => `span:${pathname}:${method}`), - isDataRequest: vi.fn(), - }; -}); - -const mockSpan = { - spanContext: () => ({ traceId: '1', spanId: '2', traceFlags: 1 }), - setAttributes: vi.fn(), -}; - -function createRequest(url: string, method = 'GET') { - return { url, method } as unknown as Request; -} - -describe('ReactRouterInstrumentation', () => { - let instrumentation: ReactRouterInstrumentation; - let mockModule: any; - let originalHandler: any; - - beforeEach(() => { - instrumentation = new ReactRouterInstrumentation(); - originalHandler = vi.fn(); - mockModule = { - createRequestHandler: vi.fn(() => originalHandler), - }; - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should patch createRequestHandler', () => { - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - expect(typeof proxy.createRequestHandler).toBe('function'); - expect(proxy.createRequestHandler).not.toBe(mockModule.createRequestHandler); - }); - - it('should call original handler for non-data requests', async () => { - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true); - vi.spyOn(Util, 'isDataRequest').mockReturnValue(false); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const wrappedHandler = proxy.createRequestHandler(); - const req = createRequest('https://test.com/page'); - await wrappedHandler(req); - - expect(Util.isDataRequest).toHaveBeenCalledWith('/page'); - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - }); - - it('should call original handler if no active root span', async () => { - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true); - vi.spyOn(Util, 'isDataRequest').mockReturnValue(true); - vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(undefined); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const wrappedHandler = proxy.createRequestHandler(); - const req = createRequest('https://test.com/data'); - await wrappedHandler(req); - - expect(SentryCore.debug.log).toHaveBeenCalledWith('No active root span found, skipping tracing for data request'); - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - }); - - it('should start a span for data requests with active root span', async () => { - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true); - vi.spyOn(Util, 'isDataRequest').mockReturnValue(true); - // @ts-expect-error MockSpan just for testing - vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(mockSpan as Span); - // @ts-expect-error MockSpan just for testing - vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue(mockSpan as Span); - vi.spyOn(SentryCore, 'spanToJSON').mockReturnValue({ data: {} } as SpanJSON); - vi.spyOn(Util, 'getSpanName').mockImplementation((pathname, method) => `span:${pathname}:${method}`); - // @ts-expect-error MockSpan just for testing - vi.spyOn(SentryCore, 'startSpan').mockImplementation((_opts, fn) => fn(mockSpan as Span)); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const wrappedHandler = proxy.createRequestHandler(); - const req = createRequest('https://test.com/data', 'POST'); - await wrappedHandler(req); - - expect(Util.isDataRequest).toHaveBeenCalledWith('/data'); - expect(Util.getSpanName).toHaveBeenCalledWith('/data', 'POST'); - expect(SentryCore.startSpan).toHaveBeenCalled(); - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - }); - - it('should handle invalid URLs gracefully', async () => { - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true); - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const wrappedHandler = proxy.createRequestHandler(); - const req = { url: 'not a url', method: 'GET' } as any; - await wrappedHandler(req); - - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - }); - - it('should call setServerBuild when static ServerBuild is passed', () => { - const spy = vi.spyOn(ServerBuild, 'setServerBuild'); - vi.spyOn(ServerBuild, 'isServerBuildLike').mockReturnValue(true); - - const staticBuild = { routes: { root: { id: 'root' } } }; - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - proxy.createRequestHandler(staticBuild); - - expect(spy).toHaveBeenCalledWith(staticBuild); - }); - - it('should capture ServerBuild from factory function', async () => { - const resolvedBuild = { routes: { root: { id: 'root' } } }; - const buildFactory = vi.fn().mockResolvedValue(resolvedBuild); - vi.spyOn(ServerBuild, 'isServerBuildLike').mockImplementation(val => val === resolvedBuild); - const spy = vi.spyOn(ServerBuild, 'setServerBuild'); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - proxy.createRequestHandler(buildFactory); - - // Factory gets wrapped — invoke it via the arg passed to the original createRequestHandler - const wrappedFactory = mockModule.createRequestHandler.mock.calls[0][0]; - await wrappedFactory(); - - expect(spy).toHaveBeenCalledWith(resolvedBuild); - }); - - it('should bypass instrumentation when instrumentation API is active', async () => { - vi.spyOn(ServerGlobals, 'isInstrumentationApiUsed').mockReturnValue(true); - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true); - vi.spyOn(Util, 'isDataRequest').mockReturnValue(true); - const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const handler = proxy.createRequestHandler(); - - // Handler is always wrapped; the instrumentation API check happens per-request - expect(handler).not.toBe(originalHandler); - - const req = createRequest('https://test.com/data', 'GET'); - await handler(req); - - // Should delegate to original handler without creating spans - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - expect(startSpanSpy).not.toHaveBeenCalled(); - }); - - it('should skip span creation when OTEL data-loader span creation is disabled', async () => { - vi.spyOn(ServerGlobals, 'isInstrumentationApiUsed').mockReturnValue(false); - vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(false); - vi.spyOn(Util, 'isDataRequest').mockReturnValue(true); - const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); - - const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule); - const handler = proxy.createRequestHandler(); - - const req = createRequest('https://test.com/data', 'GET'); - await handler(req); - - // Should delegate to original handler without creating spans - expect(originalHandler).toHaveBeenCalledWith(req, undefined); - expect(startSpanSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/react-router/test/server/integration/reactRouterServer.test.ts b/packages/react-router/test/server/integration/reactRouterServer.test.ts index fd9dc2995119..6767e3571472 100644 --- a/packages/react-router/test/server/integration/reactRouterServer.test.ts +++ b/packages/react-router/test/server/integration/reactRouterServer.test.ts @@ -1,54 +1,21 @@ import { HTTP_ROUTE } from '@sentry/conventions/attributes'; import type { Client, Event, EventType, StreamedSpanJSON } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter'; -import { - instrumentReactRouterServer, - reactRouterServerIntegration, -} from '../../../src/server/integration/reactRouterServer'; +import { reactRouterServerIntegration } from '../../../src/server/integration/reactRouterServer'; import * as serverBuild from '../../../src/server/serverBuild'; -import * as serverGlobals from '../../../src/server/serverGlobals'; - -vi.mock('../../../src/server/instrumentation/reactRouter', () => { - return { - ReactRouterInstrumentation: vi.fn(), - }; -}); - -const mockNodeVersion = { major: 20, minor: 18, patch: 0 }; - -vi.mock('@sentry/node', () => { - return { - generateInstrumentOnce: vi.fn((_name: string, callback: () => any) => { - return Object.assign(callback, { id: 'test' }); - }), - get NODE_VERSION() { - return mockNodeVersion; - }, - }; -}); describe('reactRouterServerIntegration', () => { let registerServerBuildGlobalSpy: ReturnType; - let enableOtelDataLoaderSpanCreationSpy: ReturnType; beforeEach(() => { vi.clearAllMocks(); registerServerBuildGlobalSpy = vi.spyOn(serverBuild, 'registerServerBuildGlobal'); - enableOtelDataLoaderSpanCreationSpy = vi.spyOn(serverGlobals, 'enableOtelDataLoaderSpanCreation'); }); afterEach(() => { vi.restoreAllMocks(); }); - it('sets up ReactRouterInstrumentation on setupOnce', () => { - const integration = reactRouterServerIntegration(); - integration.setupOnce!(); - - expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1); - }); - it('registers the server build global callback on setupOnce', () => { const integration = reactRouterServerIntegration(); integration.setupOnce!(); @@ -56,62 +23,6 @@ describe('reactRouterServerIntegration', () => { expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); }); - it('does not register the server build global from the OTEL instrumentation setup', () => { - // Guards against re-coupling: the Vite-plugin capture registration must not depend on the - // OTEL patch being installed, so it survives once the OTEL instrumentation is removed. - instrumentReactRouterServer(); - - expect(registerServerBuildGlobalSpy).not.toHaveBeenCalled(); - }); - - it('enables OTEL data-loader span creation on Node 20.18', () => { - mockNodeVersion.major = 20; - mockNodeVersion.minor = 18; - - const integration = reactRouterServerIntegration(); - integration.setupOnce!(); - - expect(enableOtelDataLoaderSpanCreationSpy).toHaveBeenCalledTimes(1); - expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1); - expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); - }); - - it('enables OTEL data-loader span creation on Node 22.11', () => { - mockNodeVersion.major = 22; - mockNodeVersion.minor = 11; - - const integration = reactRouterServerIntegration(); - integration.setupOnce!(); - - expect(enableOtelDataLoaderSpanCreationSpy).toHaveBeenCalledTimes(1); - expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1); - expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); - }); - - it('does not enable OTEL data-loader span creation on Node 20.19', () => { - mockNodeVersion.major = 20; - mockNodeVersion.minor = 19; - - const integration = reactRouterServerIntegration(); - integration.setupOnce!(); - - expect(enableOtelDataLoaderSpanCreationSpy).not.toHaveBeenCalled(); - expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1); - expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); - }); - - it('does not enable OTEL data-loader span creation on Node 22.12', () => { - mockNodeVersion.major = 22; - mockNodeVersion.minor = 12; - - const integration = reactRouterServerIntegration(); - integration.setupOnce!(); - - expect(enableOtelDataLoaderSpanCreationSpy).not.toHaveBeenCalled(); - expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1); - expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); - }); - describe('processEvent', () => { const client = {} as Client; const hint = {}; @@ -124,7 +35,6 @@ describe('reactRouterServerIntegration', () => { contexts: { trace: { data: { [HTTP_ROUTE]: '/users/:id' }, - origin: 'auto.http.otel.http', }, }, } as unknown as Event; @@ -134,33 +44,14 @@ describe('reactRouterServerIntegration', () => { expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('/users/:id'); }); - it('deletes bogus "*" route when origin is instrumentation_api', () => { - const integration = reactRouterServerIntegration(); - const event = { - type: 'transaction' as EventType, - transaction: 'GET *', - contexts: { - trace: { - data: { [HTTP_ROUTE]: '*' }, - origin: 'auto.http.otel.instrumentation_api', - }, - }, - } as unknown as Event; - - integration.processEvent!(event, hint, client); - - expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined(); - }); - - it('deletes bogus "*" route when legacy origin and transaction name was renamed', () => { + it('deletes the bogus "*" route', () => { const integration = reactRouterServerIntegration(); const event = { type: 'transaction' as EventType, - transaction: 'GET /api/users', + transaction: 'GET /ssr', contexts: { trace: { data: { [HTTP_ROUTE]: '*' }, - origin: 'auto.http.otel.http', }, }, } as unknown as Event; @@ -169,24 +60,6 @@ describe('reactRouterServerIntegration', () => { expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined(); }); - - it('keeps "*" when legacy origin and transaction name still ends with " *"', () => { - const integration = reactRouterServerIntegration(); - const event = { - type: 'transaction' as EventType, - transaction: 'GET *', - contexts: { - trace: { - data: { [HTTP_ROUTE]: '*' }, - origin: 'auto.http.otel.http', - }, - }, - } as unknown as Event; - - integration.processEvent!(event, hint, client); - - expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('*'); - }); }); describe('processSegmentSpan', () => { @@ -196,7 +69,7 @@ describe('reactRouterServerIntegration', () => { const integration = reactRouterServerIntegration(); const span = { name: 'GET /users/:id', - attributes: { [HTTP_ROUTE]: '/users/:id', 'sentry.origin': 'auto.http.otel.http' }, + attributes: { [HTTP_ROUTE]: '/users/:id' }, } as unknown as StreamedSpanJSON; integration.processSegmentSpan!(span, client); @@ -204,40 +77,16 @@ describe('reactRouterServerIntegration', () => { expect(span.attributes?.[HTTP_ROUTE]).toBe('/users/:id'); }); - it('deletes bogus "*" route when origin is instrumentation_api', () => { + it('deletes the bogus "*" route', () => { const integration = reactRouterServerIntegration(); const span = { - name: 'GET *', - attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.instrumentation_api' }, + name: 'GET /ssr', + attributes: { [HTTP_ROUTE]: '*' }, } as unknown as StreamedSpanJSON; integration.processSegmentSpan!(span, client); expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined(); }); - - it('deletes bogus "*" route when legacy origin and span name was renamed', () => { - const integration = reactRouterServerIntegration(); - const span = { - name: 'GET /api/users', - attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' }, - } as unknown as StreamedSpanJSON; - - integration.processSegmentSpan!(span, client); - - expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined(); - }); - - it('keeps "*" when legacy origin and span name still ends with " *"', () => { - const integration = reactRouterServerIntegration(); - const span = { - name: 'GET *', - attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' }, - } as unknown as StreamedSpanJSON; - - integration.processSegmentSpan!(span, client); - - expect(span.attributes?.[HTTP_ROUTE]).toBe('*'); - }); }); }); From 8f98a8bfe87a85f57e70616a4029b5ec1a1a395e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 23 Jul 2026 15:07:32 +0200 Subject: [PATCH 2/2] ref(react-router): Drop dead bogus-route cleanup and obsolete node-20-18 e2e app Local e2e verification showed the `http.route: '*'` placeholder no longer occurs after the OTel proxy removal - the underlying HTTP instrumentation resolves the real route (e.g. `/performance/ssr`). Remove the now-dead `processEvent`/ `processSegmentSpan` cleanup and repurpose the e2e assertion to guard that a real `http.route` is set on loader-less routes. Delete the `react-router-7-framework-node-20-18` e2e app: it existed only to test the removed OTel data-loader path on old Node, and pinned react-router 7.13 (below the 7.15 floor). Its remaining coverage is a subset of `react-router-7-framework`. Ref #22290 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../performance/performance.server.test.ts | 8 +- .../.gitignore | 32 --- .../app/app.css | 6 - .../app/entry.client.tsx | 23 -- .../app/entry.server.tsx | 18 -- .../app/root.tsx | 67 ----- .../app/routes.ts | 21 -- .../app/routes/errors/client-action.tsx | 18 -- .../app/routes/errors/client-loader.tsx | 16 -- .../app/routes/errors/client-param.tsx | 17 -- .../app/routes/errors/client.tsx | 15 -- .../app/routes/errors/server-action.tsx | 18 -- .../app/routes/errors/server-loader.tsx | 16 -- .../app/routes/home.tsx | 9 - .../app/routes/performance/dynamic-param.tsx | 17 -- .../app/routes/performance/index.tsx | 14 -- .../app/routes/performance/server-action.tsx | 24 -- .../app/routes/performance/server-loader.tsx | 16 -- .../app/routes/performance/ssr.tsx | 7 - .../app/routes/performance/static.tsx | 3 - .../instrument.mjs | 8 - .../package.json | 64 ----- .../playwright.config.mjs | 8 - .../public/favicon.ico | Bin 15086 -> 0 bytes .../react-router.config.ts | 6 - .../start-event-proxy.mjs | 6 - .../tests/constants.ts | 1 - .../tests/errors/errors.client.test.ts | 140 ----------- .../tests/errors/errors.server.test.ts | 100 -------- .../performance/navigation.client.test.ts | 126 ---------- .../tests/performance/pageload.client.test.ts | 148 ------------ .../performance/performance.server.test.ts | 228 ------------------ .../performance/trace-propagation.test.ts | 47 ---- .../tsconfig.json | 20 -- .../vite.config.ts | 6 - .../server/integration/reactRouterServer.ts | 25 -- .../integration/reactRouterServer.test.ts | 69 ------ scripts/report-ci-failures.mjs | 2 +- 38 files changed, 5 insertions(+), 1364 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/public/favicon.ico delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts index 9bedfa35e212..3c0b36d6d663 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts @@ -168,9 +168,9 @@ test.describe('server - instrumentation API performance', () => { expect(httpServerTransactions).toEqual(['GET /performance']); }); - test('strips the bogus "*" http.route on routes without a loader/action', async ({ page }) => { - // The `@sentry/node` HTTP root span matches React Router's catch-all handler with `http.route: '*'`. - // On routes without a loader/action nothing overwrites it, so the integration must strip it before send. + test('resolves a real http.route on routes without a loader/action', async ({ page }) => { + // Regression guard for the server OTel removal: routes without a loader/action must still get a + // proper `http.route` (not the catch-all `*` placeholder) from the underlying HTTP instrumentation. const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { return transactionEvent.transaction === 'GET /performance/ssr'; }); @@ -180,6 +180,6 @@ test.describe('server - instrumentation API performance', () => { const transaction = await txPromise; expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.data?.['http.route']).toBeUndefined(); + expect(transaction.contexts?.trace?.data?.['http.route']).toBe('/performance/ssr'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore deleted file mode 100644 index ebb991370034..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -/test-results/ -/playwright-report/ -/playwright/.cache/ - -!*.d.ts - -# react router -.react-router diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css deleted file mode 100644 index b31c3a9d0ddf..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css +++ /dev/null @@ -1,6 +0,0 @@ -html, -body { - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx deleted file mode 100644 index 005268b40ad0..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as Sentry from '@sentry/react-router'; -import { StrictMode, startTransition } from 'react'; -import { hydrateRoot } from 'react-dom/client'; -import { HydratedRouter } from 'react-router/dom'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - // todo: get this from env - dsn: 'https://username@domain/123', - tunnel: `http://localhost:3031/`, // proxy server - integrations: [Sentry.reactRouterTracingIntegration()], - tracesSampleRate: 1.0, - tracePropagationTargets: [/^\//], -}); - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx deleted file mode 100644 index 738cd1515a4d..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createReadableStreamFromReadable } from '@react-router/node'; -import * as Sentry from '@sentry/react-router'; -import { renderToPipeableStream } from 'react-dom/server'; -import { ServerRouter } from 'react-router'; -import { type HandleErrorFunction } from 'react-router'; - -const ABORT_DELAY = 5_000; - -const handleRequest = Sentry.createSentryHandleRequest({ - streamTimeout: ABORT_DELAY, - ServerRouter, - renderToPipeableStream, - createReadableStreamFromReadable, -}); - -export default handleRequest; - -export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx deleted file mode 100644 index bc1b8f1236c0..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router'; -import type { Route } from './+types/root'; -import stylesheet from './app.css?url'; - -export const links: Route.LinksFunction = () => [ - { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, - { - rel: 'preconnect', - href: 'https://fonts.gstatic.com', - crossOrigin: 'anonymous', - }, - { - rel: 'stylesheet', - href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', - }, - { rel: 'stylesheet', href: stylesheet }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = 'Oops!'; - let details = 'An unexpected error occurred.'; - let stack: string | undefined; - - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? '404' : 'Error'; - details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details; - } else if (error && error instanceof Error) { - if (import.meta.env.DEV) { - details = error.message; - stack = error.stack; - } - } - - return ( -
-

{message}

-

{details}

- {stack && ( -
-          {stack}
-        
- )} -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts deleted file mode 100644 index b412893def52..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes'; - -export default [ - index('routes/home.tsx'), - ...prefix('errors', [ - route('client', 'routes/errors/client.tsx'), - route('client/:client-param', 'routes/errors/client-param.tsx'), - route('client-loader', 'routes/errors/client-loader.tsx'), - route('server-loader', 'routes/errors/server-loader.tsx'), - route('client-action', 'routes/errors/client-action.tsx'), - route('server-action', 'routes/errors/server-action.tsx'), - ]), - ...prefix('performance', [ - index('routes/performance/index.tsx'), - route('ssr', 'routes/performance/ssr.tsx'), - route('with/:param', 'routes/performance/dynamic-param.tsx'), - route('static', 'routes/performance/static.tsx'), - route('server-loader', 'routes/performance/server-loader.tsx'), - route('server-action', 'routes/performance/server-action.tsx'), - ]), -] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx deleted file mode 100644 index d3b2d08eef2e..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Form } from 'react-router'; - -export function clientAction() { - throw new Error('Madonna mia! Che casino nella Client Action!'); -} - -export default function ClientActionErrorPage() { - return ( -
-

Client Error Action Page

-
- -
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx deleted file mode 100644 index 72d9e62a99dc..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from './+types/server-loader'; - -export function clientLoader() { - throw new Error('¡Madre mía del client loader!'); - return { data: 'sad' }; -} - -export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) { - const { data } = loaderData; - return ( -
-

Client Loader Error Page

-
{data}
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx deleted file mode 100644 index a2e423391f03..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Route } from './+types/client-param'; - -export default function ClientErrorParamPage({ params }: Route.ComponentProps) { - return ( -
-

Client Error Param Page

- -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx deleted file mode 100644 index 190074a5ef09..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function ClientErrorPage() { - return ( -
-

Client Error Page

- -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx deleted file mode 100644 index 863c320f3557..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Form } from 'react-router'; - -export function action() { - throw new Error('Madonna mia! Che casino nella Server Action!'); -} - -export default function ServerActionErrorPage() { - return ( -
-

Server Error Action Page

-
- -
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx deleted file mode 100644 index cb777686d540..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from './+types/server-loader'; - -export function loader() { - throw new Error('¡Madre mía del server!'); - return { data: 'sad' }; -} - -export default function ServerLoaderErrorPage({ loaderData }: Route.ComponentProps) { - const { data } = loaderData; - return ( -
-

Server Error Page

-
{data}
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx deleted file mode 100644 index 4498e7a0d017..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { Route } from './+types/home'; - -export function meta({}: Route.MetaArgs) { - return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }]; -} - -export default function Home() { - return
home
; -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx deleted file mode 100644 index 1ac02775f2ff..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Route } from './+types/dynamic-param'; - -export async function loader() { - await new Promise(resolve => setTimeout(resolve, 500)); - return { data: 'burritos' }; -} - -export default function DynamicParamPage({ params }: Route.ComponentProps) { - const { param } = params; - - return ( -
-

Dynamic Parameter Page

-

The parameter value is: {param}

-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx deleted file mode 100644 index e5383306625a..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Link } from 'react-router'; - -export default function PerformancePage() { - return ( -
-

Performance Page

- -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx deleted file mode 100644 index 462fc6fbf54c..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Form } from 'react-router'; -import type { Route } from './+types/server-action'; - -export async function action({ request }: Route.ActionArgs) { - let formData = await request.formData(); - let name = formData.get('name'); - await new Promise(resolve => setTimeout(resolve, 1000)); - return { - greeting: `Hola ${name}`, - }; -} - -export default function Project({ actionData }: Route.ComponentProps) { - return ( -
-

Server action page

-
- - -
- {actionData ?

{actionData.greeting}

: null} -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx deleted file mode 100644 index e5c222ff4c05..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from './+types/server-loader'; - -export async function loader() { - await new Promise(resolve => setTimeout(resolve, 500)); - return { data: 'burritos' }; -} - -export default function ServerLoaderPage({ loaderData }: Route.ComponentProps) { - const { data } = loaderData; - return ( -
-

Server Loader Page

-
{data}
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx deleted file mode 100644 index 253e964ff15d..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function SsrPage() { - return ( -
-

SSR Page

-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx deleted file mode 100644 index 3dea24381fdc..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function StaticPage() { - return

Static Page

; -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs deleted file mode 100644 index 48e4b7b61ff3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import * as Sentry from '@sentry/react-router'; - -Sentry.init({ - dsn: 'https://username@domain/123', - environment: 'qa', // dynamic sampling bias to keep transactions - tracesSampleRate: 1.0, - tunnel: `http://localhost:3031/`, // proxy server, -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json deleted file mode 100644 index 65f4a96b0165..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "react-router-7-framework-node-20-18", - "version": "0.1.0", - "type": "module", - "private": true, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router": "7.13.0", - "@react-router/node": "7.13.0", - "@react-router/serve": "7.13.0", - "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "isbot": "^5.1.17" - }, - "devDependencies": { - "@types/react": "18.3.1", - "@types/react-dom": "18.3.1", - "@types/node": "^20", - "@react-router/dev": "7.13.0", - "@playwright/test": "~1.56.0", - "@sentry-internal/test-utils": "link:../../../test-utils", - "typescript": "^5.6.3", - "vite": "^5.4.11" - }, - "scripts": { - "build": "react-router build", - "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev", - "start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js", - "proxy": "node start-event-proxy.mjs", - "typecheck": "react-router typegen && tsc", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test:ts && pnpm test:playwright", - "test:ts": "pnpm typecheck", - "test:playwright": "playwright test" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "volta": { - "extends": "../../package.json", - "node": "20.18.2" - }, - "pnpm": { - "overrides": { - "p-map": "^4.0.0" - } - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs deleted file mode 100644 index 3ed5721107a7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig({ - startCommand: `PORT=3030 pnpm start`, - port: 3030, -}); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/public/favicon.ico deleted file mode 100644 index 5dbdfcddcb14182535f6d32d1c900681321b1aa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts deleted file mode 100644 index bb1f96469dd2..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Config } from '@react-router/dev/config'; - -export default { - ssr: true, - prerender: ['/performance/static'], -} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs deleted file mode 100644 index c430b9c3e710..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'react-router-7-framework-node-20-18', -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts deleted file mode 100644 index 2c61859bb6ef..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const APP_NAME = 'react-router-7-framework-node-20-18'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts deleted file mode 100644 index c1a7de46f1b6..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('client-side errors', () => { - const errorMessage = '¡Madre mía!'; - test('captures error thrown on click', async ({ page }) => { - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto(`/errors/client`); - await page.locator('#throw-on-click').click(); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: errorMessage, - mechanism: { - handled: false, - }, - }, - ], - }, - transaction: '/errors/client', - request: { - url: expect.stringContaining('errors/client'), - headers: expect.any(Object), - }, - level: 'error', - platform: 'javascript', - environment: 'qa', - sdk: { - integrations: expect.any(Array), - name: 'sentry.javascript.react-router', - version: expect.any(String), - }, - tags: { runtime: 'browser' }, - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }, - breadcrumbs: [ - { - category: 'ui.click', - message: 'body > div > button#throw-on-click', - }, - ], - }); - }); - - test('captures error thrown on click from a parameterized route', async ({ page }) => { - const errorMessage = '¡Madre mía de churros!'; - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto('/errors/client/churros'); - await page.locator('#throw-on-click').click(); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: '¡Madre mía de churros!', - mechanism: { - handled: false, - }, - }, - ], - }, - // todo: should be '/errors/client/:client-param' - transaction: '/errors/client/churros', - }); - }); - - test('captures error thrown in a clientLoader', async ({ page }) => { - const errorMessage = '¡Madre mía del client loader!'; - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto('/errors/client-loader'); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: errorMessage, - mechanism: { - handled: false, - type: 'auto.function.react_router.on_error', - }, - }, - ], - }, - transaction: '/errors/client-loader', - }); - }); - - test('captures error thrown in a clientAction', async ({ page }) => { - const errorMessage = 'Madonna mia! Che casino nella Client Action!'; - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto('/errors/client-action'); - await page.locator('#submit').click(); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: errorMessage, - mechanism: { - handled: false, - type: 'auto.function.react_router.on_error', - }, - }, - ], - }, - transaction: '/errors/client-action', - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts deleted file mode 100644 index 2759bfecb67e..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('server-side errors', () => { - test('captures error thrown in server loader', async ({ page }) => { - const errorMessage = '¡Madre mía del server!'; - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto(`/errors/server-loader`); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: errorMessage, - mechanism: { - handled: false, - type: 'react-router', - }, - }, - ], - }, - // todo: should be 'GET /errors/server-loader' - transaction: 'GET *', - request: { - url: expect.stringContaining('errors/server-loader'), - headers: expect.any(Object), - }, - level: 'error', - platform: 'node', - environment: 'qa', - sdk: { - integrations: expect.any(Array), - name: 'sentry.javascript.react-router', - version: expect.any(String), - }, - tags: { runtime: 'node' }, - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }, - }); - }); - - test('captures error thrown in server action', async ({ page }) => { - const errorMessage = 'Madonna mia! Che casino nella Server Action!'; - const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === errorMessage; - }); - - await page.goto(`/errors/server-action`); - await page.locator('#submit').click(); - - const error = await errorPromise; - - expect(error).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: errorMessage, - mechanism: { - handled: false, - type: 'react-router', - }, - }, - ], - }, - // todo: should be 'POST /errors/server-action' - transaction: 'POST *', - request: { - url: expect.stringContaining('errors/server-action'), - headers: expect.any(Object), - }, - level: 'error', - platform: 'node', - environment: 'qa', - sdk: { - integrations: expect.any(Array), - name: 'sentry.javascript.react-router', - version: expect.any(String), - }, - tags: { runtime: 'node' }, - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }, - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts deleted file mode 100644 index 3432b95ddae3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('client - navigation performance', () => { - test('should create navigation transaction', async ({ page }) => { - const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); - }); - - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/performance`); // pageload - await pageloadTxPromise; - await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation - - const transaction = await navigationPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.source': 'route', - 'url.template': '/performance/ssr', - 'url.path': '/performance/ssr', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/ssr', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/ssr'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, - }); - }); - - test('should update navigation transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); - }); - - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/performance`); // pageload - await pageloadTxPromise; - await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts deleted file mode 100644 index f996989ccbf5..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('client - pageload performance', () => { - test('should send pageload transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/performance`); - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.source': 'route', - 'url.template': '/performance', - // react-router-serve 301-redirects the bare index route to a trailing slash - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, - }); - }); - - test('should update pageload transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'pageload' - ); - }); - - await page.goto(`/performance/with/sentry`); - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, - }); - }); - - test('should send pageload transaction for prerendered pages', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/static' && transactionEvent.contexts?.trace?.op === 'pageload' - ); - }); - - await page.goto(`/performance/static`); - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - transaction: '/performance/static', - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.source': 'route', - 'url.template': '/performance/static', - // react-router-serve 301-redirects prerendered routes to a trailing slash - 'url.path': '/performance/static/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts deleted file mode 100644 index e0ca27a19e10..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('server - performance', () => { - test('should send server transaction on pageload', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance'; - }); - - await page.goto(`/performance`); - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, - }); - }); - - test('should send server transaction on parameterized route', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/with/:param'; - }); - - await page.goto(`/performance/with/some-param`); - - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance/with/some-param'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, - }); - }); - - test('should automatically instrument server loader', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/server-loader.data'; - }); - - await page.goto('/performance'); // initial ssr pageloads do not contain .data requests - await page.getByRole('link', { name: 'Server Loader' }).click(); // this will actually trigger a .data request - - const transaction = await txPromise; - - expect(transaction).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - op: 'http.server', - origin: 'auto.http.react_router.server', - parent_span_id: expect.any(String), - status: 'ok', - data: expect.objectContaining({ - 'http.method': 'GET', - 'http.response.status_code': 200, - 'http.status_code': 200, - 'http.status_text': 'OK', - 'http.target': '/performance/server-loader.data', - 'http.url': 'http://localhost:3030/performance/server-loader.data', - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.server', - 'sentry.source': 'url', - url: 'http://localhost:3030/performance/server-loader.data', - }), - }, - }), - transaction: 'GET /performance/server-loader.data', - type: 'transaction', - transaction_info: { source: 'url' }, - platform: 'node', - }), - ); - - // ensure we do not have a stray, bogus route attribute - expect(transaction.contexts?.trace?.data?.['http.route']).not.toBeDefined(); - - expect(transaction.spans).toContainEqual({ - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'function.react_router.loader', - 'sentry.origin': 'auto.http.react_router.server', - }, - description: 'Executing Server Loader', - op: 'function.react_router.loader', - origin: 'auto.http.react_router.server', - parent_span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - }); - }); - - test('should automatically instrument server action', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'POST /performance/server-action.data'; - }); - - await page.goto(`/performance/server-action`); - await page.getByRole('button', { name: 'Submit' }).click(); // this will trigger a .data request - - const transaction = await txPromise; - - expect(transaction).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - op: 'http.server', - origin: 'auto.http.react_router.server', - parent_span_id: expect.any(String), - status: 'ok', - data: expect.objectContaining({ - 'http.method': 'POST', - 'http.response.status_code': 200, - 'http.status_code': 200, - 'http.status_text': 'OK', - 'http.target': '/performance/server-action.data', - 'http.url': 'http://localhost:3030/performance/server-action.data', - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.server', - 'sentry.source': 'url', - url: 'http://localhost:3030/performance/server-action.data', - }), - }, - }), - transaction: 'POST /performance/server-action.data', - type: 'transaction', - transaction_info: { source: 'url' }, - platform: 'node', - }), - ); - // ensure we do not have a stray, bogus route attribute - expect(transaction.contexts?.trace?.data?.['http.route']).not.toBeDefined(); - - expect(transaction.spans).toContainEqual({ - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'function.react_router.action', - 'sentry.origin': 'auto.http.react_router.server', - }, - description: 'Executing Server Action', - op: 'function.react_router.action', - origin: 'auto.http.react_router.server', - parent_span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - }); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts deleted file mode 100644 index e9b2c9409154..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('Trace propagation', () => { - test('should inject metatags in ssr pageload', async ({ page }) => { - await page.goto(`/`); - const sentryTraceContent = await page.getAttribute('meta[name="sentry-trace"]', 'content'); - expect(sentryTraceContent).toBeDefined(); - expect(sentryTraceContent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[01]$/); - const baggageContent = await page.getAttribute('meta[name="baggage"]', 'content'); - expect(baggageContent).toBeDefined(); - expect(baggageContent).toContain('sentry-environment=qa'); - expect(baggageContent).toContain('sentry-public_key='); - expect(baggageContent).toContain('sentry-trace_id='); - expect(baggageContent).toContain('sentry-transaction='); - expect(baggageContent).toContain('sentry-sampled='); - }); - - test('should have trace connection', async ({ page }) => { - const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET *'; - }); - - const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/'; - }); - - await page.goto(`/`); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; - - expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id); - - const requestHandlerSpan = serverTx.spans?.find(span => span.op === 'request_handler.express'); - - expect(requestHandlerSpan).toBeDefined(); - expect(clientTx.contexts?.trace?.parent_span_id).toBe(requestHandlerSpan?.span_id); - }); - - test('should not have trace connection for prerendered pages', async ({ page }) => { - await page.goto('/performance/static'); - - const sentryTraceElement = await page.$('meta[name="sentry-trace"]'); - expect(sentryTraceElement).toBeNull(); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json deleted file mode 100644 index a16df276e8bc..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["node", "vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", - - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true - }, - "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"] -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts deleted file mode 100644 index 68ba30d69397..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { reactRouter } from '@react-router/dev/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [reactRouter()], -}); diff --git a/packages/react-router/src/server/integration/reactRouterServer.ts b/packages/react-router/src/server/integration/reactRouterServer.ts index a2fee6176e50..8f43009ca3d9 100644 --- a/packages/react-router/src/server/integration/reactRouterServer.ts +++ b/packages/react-router/src/server/integration/reactRouterServer.ts @@ -1,4 +1,3 @@ -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; import { defineIntegration } from '@sentry/core'; import { registerServerBuildGlobal } from '../serverBuild'; @@ -14,29 +13,5 @@ export const reactRouterServerIntegration = defineIntegration(() => { // Register global for Vite plugin ServerBuild capture (used for middleware name resolution). registerServerBuildGlobal(); }, - processEvent(event) { - // The `@sentry/node` HTTP root span matches React Router's catch-all server handler, so it - // carries a bogus `http.route` of `*`. The instrumentation API sets a proper route on requests - // that hit a loader/action/middleware, but requests without one (e.g. SSR-only routes) keep the - // placeholder - strip it here so it doesn't leak into the transaction. - if ( - event.type === 'transaction' && - event.contexts?.trace?.data && - event.contexts.trace.data[HTTP_ROUTE] === '*' - ) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete event.contexts.trace.data[HTTP_ROUTE]; - } - - return event; - }, - processSegmentSpan(span) { - // See `processEvent`: strip the bogus `*` route from the `@sentry/node` HTTP root span. - const attributes = span.attributes; - if (attributes?.[HTTP_ROUTE] === '*') { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete attributes[HTTP_ROUTE]; - } - }, }; }); diff --git a/packages/react-router/test/server/integration/reactRouterServer.test.ts b/packages/react-router/test/server/integration/reactRouterServer.test.ts index 6767e3571472..932ee8452f23 100644 --- a/packages/react-router/test/server/integration/reactRouterServer.test.ts +++ b/packages/react-router/test/server/integration/reactRouterServer.test.ts @@ -1,5 +1,3 @@ -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; -import type { Client, Event, EventType, StreamedSpanJSON } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { reactRouterServerIntegration } from '../../../src/server/integration/reactRouterServer'; import * as serverBuild from '../../../src/server/serverBuild'; @@ -22,71 +20,4 @@ describe('reactRouterServerIntegration', () => { expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1); }); - - describe('processEvent', () => { - const client = {} as Client; - const hint = {}; - - it('preserves http.route when it is not "*"', () => { - const integration = reactRouterServerIntegration(); - const event = { - type: 'transaction' as EventType, - transaction: 'GET /users/:id', - contexts: { - trace: { - data: { [HTTP_ROUTE]: '/users/:id' }, - }, - }, - } as unknown as Event; - - integration.processEvent!(event, hint, client); - - expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('/users/:id'); - }); - - it('deletes the bogus "*" route', () => { - const integration = reactRouterServerIntegration(); - const event = { - type: 'transaction' as EventType, - transaction: 'GET /ssr', - contexts: { - trace: { - data: { [HTTP_ROUTE]: '*' }, - }, - }, - } as unknown as Event; - - integration.processEvent!(event, hint, client); - - expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined(); - }); - }); - - describe('processSegmentSpan', () => { - const client = {} as Client; - - it('preserves http.route when it is not "*"', () => { - const integration = reactRouterServerIntegration(); - const span = { - name: 'GET /users/:id', - attributes: { [HTTP_ROUTE]: '/users/:id' }, - } as unknown as StreamedSpanJSON; - - integration.processSegmentSpan!(span, client); - - expect(span.attributes?.[HTTP_ROUTE]).toBe('/users/:id'); - }); - - it('deletes the bogus "*" route', () => { - const integration = reactRouterServerIntegration(); - const span = { - name: 'GET /ssr', - attributes: { [HTTP_ROUTE]: '*' }, - } as unknown as StreamedSpanJSON; - - integration.processSegmentSpan!(span, client); - - expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined(); - }); - }); }); diff --git a/scripts/report-ci-failures.mjs b/scripts/report-ci-failures.mjs index d464ffa11985..e87a38b7a824 100644 --- a/scripts/report-ci-failures.mjs +++ b/scripts/report-ci-failures.mjs @@ -29,7 +29,7 @@ import { readFileSync } from 'node:fs'; * "aws-serverless-layer (Node 22) Test" -> "aws-serverless-layer Test" * "Playwright bundle_tracing_replay Tests" -> "Playwright Tests" * "Playwright esm (1/4) Tests" -> "Playwright Tests" - * "E2E react-router-7-framework-node-20-18 Test" -> "E2E react-router-7-framework Test" + * "E2E react-router-7-framework-spa-node-20-18 Test" -> "E2E react-router-7-framework-spa Test" */ function normalizeJobName(name) { return name