diff --git a/sentry-javascript/19580/.react-router/types/+future.ts b/sentry-javascript/19580/.react-router/types/+future.ts new file mode 100644 index 0000000..7f4533c --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/+future.ts @@ -0,0 +1,9 @@ +// Generated by React Router + +import "react-router"; + +declare module "react-router" { + interface Future { + v8_middleware: false + } +} \ No newline at end of file diff --git a/sentry-javascript/19580/.react-router/types/+routes.ts b/sentry-javascript/19580/.react-router/types/+routes.ts new file mode 100644 index 0000000..4566862 --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/+routes.ts @@ -0,0 +1,43 @@ +// Generated by React Router + +import "react-router" + +declare module "react-router" { + interface Register { + pages: Pages + routeFiles: RouteFiles + routeModules: RouteModules + } +} + +type Pages = { + "/": { + params: {}; + }; + "/items/:id": { + params: { + "id": string; + }; + }; +}; + +type RouteFiles = { + "root.tsx": { + id: "root"; + page: "/" | "/items/:id"; + }; + "routes/home.tsx": { + id: "routes/home"; + page: "/"; + }; + "routes/items.$id.tsx": { + id: "routes/items.$id"; + page: "/items/:id"; + }; +}; + +type RouteModules = { + "root": typeof import("./app/root.tsx"); + "routes/home": typeof import("./app/routes/home.tsx"); + "routes/items.$id": typeof import("./app/routes/items.$id.tsx"); +}; \ No newline at end of file diff --git a/sentry-javascript/19580/.react-router/types/+server-build.d.ts b/sentry-javascript/19580/.react-router/types/+server-build.d.ts new file mode 100644 index 0000000..13792c1 --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/+server-build.d.ts @@ -0,0 +1,18 @@ +// Generated by React Router + +declare module "virtual:react-router/server-build" { + import { ServerBuild } from "react-router"; + export const assets: ServerBuild["assets"]; + export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; + export const basename: ServerBuild["basename"]; + export const entry: ServerBuild["entry"]; + export const future: ServerBuild["future"]; + export const isSpaMode: ServerBuild["isSpaMode"]; + export const prerender: ServerBuild["prerender"]; + export const publicPath: ServerBuild["publicPath"]; + export const routeDiscovery: ServerBuild["routeDiscovery"]; + export const routes: ServerBuild["routes"]; + export const ssr: ServerBuild["ssr"]; + export const allowedActionOrigins: ServerBuild["allowedActionOrigins"]; + export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"]; +} \ No newline at end of file diff --git a/sentry-javascript/19580/.react-router/types/app/+types/root.ts b/sentry-javascript/19580/.react-router/types/app/+types/root.ts new file mode 100644 index 0000000..5bd414e --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/app/+types/root.ts @@ -0,0 +1,59 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../root.js") + +type Info = GetInfo<{ + file: "root.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../root.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/sentry-javascript/19580/.react-router/types/app/routes/+types/home.ts b/sentry-javascript/19580/.react-router/types/app/routes/+types/home.ts new file mode 100644 index 0000000..e49d62a --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/app/routes/+types/home.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../home.js") + +type Info = GetInfo<{ + file: "routes/home.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/home"; + module: typeof import("../home.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/sentry-javascript/19580/.react-router/types/app/routes/+types/items.$id.ts b/sentry-javascript/19580/.react-router/types/app/routes/+types/items.$id.ts new file mode 100644 index 0000000..15d74b3 --- /dev/null +++ b/sentry-javascript/19580/.react-router/types/app/routes/+types/items.$id.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../items.$id.js") + +type Info = GetInfo<{ + file: "routes/items.$id.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/items.$id"; + module: typeof import("../items.$id.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/sentry-javascript/19580/README.md b/sentry-javascript/19580/README.md new file mode 100644 index 0000000..8ae19e3 --- /dev/null +++ b/sentry-javascript/19580/README.md @@ -0,0 +1,52 @@ +# Reproduction for sentry-javascript#19580 + +**Issue:** https://github.com/getsentry/sentry-javascript/issues/19580 + +## Description + +`reactRouterTracingIntegration` produces `[object Object]` as transaction name when navigating using a `` (or `navigate()`) with an object `to` prop, e.g.: + +```tsx + +``` + +The root cause is in the patched `navigate` function which uses `String(args[0])` to derive the transaction name. When called with an object, `String({...})` produces `"[object Object]"`. + +## Steps to Reproduce + +1. Set your Sentry DSN: + ```bash + export SENTRY_DSN= + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the dev server: + ```bash + npm run dev + ``` + +4. Open the app in your browser (http://localhost:5173) + +5. Click **"Link with object to={{ pathname: "/items/2", search: "..." }}"** — this navigates using an object `to` prop + +6. Check Sentry: the transaction name for this navigation will be `[object Object]` + +7. For comparison, click **"Link with string to="/items/1?redirectTo=%2F""** — this navigates using a string `to` prop and the transaction name is correct + +## Expected Behavior + +Transaction name should be the resolved pathname (e.g., `/items/2`), not `[object Object]`. + +## Actual Behavior + +Transaction name is `[object Object]` when navigating with an object `to` prop. + +## Environment + +- `@sentry/react-router`: ^9.38.0 +- React Router: v7 (framework mode) +- React: 19 diff --git a/sentry-javascript/19580/app/entry.client.tsx b/sentry-javascript/19580/app/entry.client.tsx new file mode 100644 index 0000000..f6720f9 --- /dev/null +++ b/sentry-javascript/19580/app/entry.client.tsx @@ -0,0 +1,34 @@ +import * as Sentry from "@sentry/react-router"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +if (process.env.SENTRY_DSN) { + console.log("SENTRY_DSN is set, initializing Sentry"); +} else { + console.log("SENTRY_DSN is not set, skipping Sentry initialization"); +} + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + integrations: [Sentry.reactRouterTracingIntegration()], + tracesSampleRate: 1.0, + debug: true, + beforeSendTransaction(event) { + const name = event.transaction; + console.log( + `[Sentry Transaction] name: "${name}", source: "${event.transaction_info?.source}"`, + name === "[object Object]" ? " <-- BUG (issue #19580)" : "" + ); + return event; + }, +}); + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/sentry-javascript/19580/app/entry.server.tsx b/sentry-javascript/19580/app/entry.server.tsx new file mode 100644 index 0000000..a73718b --- /dev/null +++ b/sentry-javascript/19580/app/entry.server.tsx @@ -0,0 +1,14 @@ +import * as Sentry from "@sentry/react-router"; +import { renderToPipeableStream } from "react-dom/server"; +import { ServerRouter } from "react-router"; +import { createReadableStreamFromReadable } from "@react-router/node"; + +export default Sentry.createSentryHandleRequest({ + ServerRouter, + renderToPipeableStream, + createReadableStreamFromReadable, +}); + +export const handleError = Sentry.createSentryHandleError({ + logErrors: false, +}); diff --git a/sentry-javascript/19580/app/root.tsx b/sentry-javascript/19580/app/root.tsx new file mode 100644 index 0000000..11d5972 --- /dev/null +++ b/sentry-javascript/19580/app/root.tsx @@ -0,0 +1,23 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/sentry-javascript/19580/app/routes.ts b/sentry-javascript/19580/app/routes.ts new file mode 100644 index 0000000..d55ed6c --- /dev/null +++ b/sentry-javascript/19580/app/routes.ts @@ -0,0 +1,6 @@ +import { type RouteConfig, index, route } from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("items/:id", "routes/items.$id.tsx"), +] satisfies RouteConfig; diff --git a/sentry-javascript/19580/app/routes/home.tsx b/sentry-javascript/19580/app/routes/home.tsx new file mode 100644 index 0000000..ab27756 --- /dev/null +++ b/sentry-javascript/19580/app/routes/home.tsx @@ -0,0 +1,59 @@ +import { Link, useNavigate } from "react-router"; + +export default function Home() { + const navigate = useNavigate(); + + return ( +
+

Reproduction: sentry-javascript#19580

+

+ reactRouterTracingIntegration uses String(args[0]) for + the transaction name. When navigate(to) is called with an object{" "} + to, that becomes [object Object]. +

+

+ Open the browser console and look for{" "} + [Sentry Transaction] name: "..." — when you navigate with an + object, the name should be [object Object] (the bug). +

+ +

1. Navigate with string (correct name)

+

+ + Link with string to="/items/1?redirectTo=%2F" + +

+ +

2. Navigate with object – Link (bug)

+

+ + Link with object to={{ pathname, search }} + +

+ +

3. Navigate with object – programmatic (bug; most reliable)

+

+ This calls navigate({ pathname, search }) directly, so + the router receives the object and Sentry does String(args[0]). +

+

+ +

+
+ ); +} diff --git a/sentry-javascript/19580/app/routes/items.$id.tsx b/sentry-javascript/19580/app/routes/items.$id.tsx new file mode 100644 index 0000000..1abaa60 --- /dev/null +++ b/sentry-javascript/19580/app/routes/items.$id.tsx @@ -0,0 +1,17 @@ +import { Link, useParams, useSearchParams } from "react-router"; + +export default function ItemDetail() { + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get("redirectTo") || "/"; + + return ( +
+

Item {id}

+

redirectTo: {redirectTo}

+

+ Back to home +

+
+ ); +} diff --git a/sentry-javascript/19580/instrument.server.mjs b/sentry-javascript/19580/instrument.server.mjs new file mode 100644 index 0000000..606715a --- /dev/null +++ b/sentry-javascript/19580/instrument.server.mjs @@ -0,0 +1,13 @@ +import * as Sentry from "@sentry/react-router"; + +if (process.env.SENTRY_DSN) { + console.log("SENTRY_DSN is set, initializing Sentry"); +} else { + console.log("SENTRY_DSN is not set, skipping Sentry initialization"); +} + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + tracesSampleRate: 1.0, + debug: false, +}); diff --git a/sentry-javascript/19580/package.json b/sentry-javascript/19580/package.json new file mode 100644 index 0000000..84cb2e2 --- /dev/null +++ b/sentry-javascript/19580/package.json @@ -0,0 +1,25 @@ +{ + "name": "sentry-javascript-19580-repro", + "private": true, + "type": "module", + "scripts": { + "dev": "NODE_OPTIONS='--import ./instrument.server.mjs' NODE_ENV=production react-router dev", + "build": "NODE_ENV=production react-router build", + "start": "NODE_OPTIONS='--import ./instrument.server.mjs' NODE_ENV=production react-router-serve ./build/server/index.js" + }, + "dependencies": { + "@sentry/react-router": "^9.38.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router": "^7.6.1", + "@react-router/node": "^7.6.1" + }, + "devDependencies": { + "@react-router/dev": "^7.6.1", + "@react-router/serve": "^7.6.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/sentry-javascript/19580/react-router.config.ts b/sentry-javascript/19580/react-router.config.ts new file mode 100644 index 0000000..e45e273 --- /dev/null +++ b/sentry-javascript/19580/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + ssr: true, +} satisfies Config; diff --git a/sentry-javascript/19580/tsconfig.json b/sentry-javascript/19580/tsconfig.json new file mode 100644 index 0000000..71c227c --- /dev/null +++ b/sentry-javascript/19580/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["app/**/*.ts", "app/**/*.tsx", "vite.config.ts", "react-router.config.ts"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/sentry-javascript/19580/vite.config.ts b/sentry-javascript/19580/vite.config.ts new file mode 100644 index 0000000..7ffae05 --- /dev/null +++ b/sentry-javascript/19580/vite.config.ts @@ -0,0 +1,6 @@ +import { reactRouter } from "@react-router/dev/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [reactRouter()], +});