Skip to content

React Router v7 Vite frontend performance monitoring not receiving events #16086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
3 tasks done
NGimbal opened this issue Apr 16, 2025 · 3 comments
Open
3 tasks done

Comments

@NGimbal
Copy link

NGimbal commented Apr 16, 2025

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/react-router

SDK Version

9.12.0

Framework Version

react-router v7 previously remix, using vite, based on epic-stack

Link to Sentry event

No response

Reproduction Example/SDK Setup

The best place to look for reproduction is the epic-stack. I've followed their integration in my app. Releases, backend performance monitoring, and error capturing all seem to work, but frontend performance monitoring is not working in Sentry despite seeing events sent by the frontend.

Issue first reported here: #14519 (comment)

Steps to Reproduce

Followed epic-stack example for sentry / react-router v7 / vite integration

// monitoring.client.tsx
import * as Sentry from '@sentry/react-router'

export function init() {
	Sentry.init({
		dsn: ENV.SENTRY_DSN,
		environment: ENV.SENTRY_ENV,
		beforeSend(event) {
			if (event.request?.url) {
				const url = new URL(event.request.url)
				if (
					url.protocol === 'chrome-extension:' ||
					url.protocol === 'moz-extension:'
				) {
					// This error is from a browser extension, ignore it
					return null
				}
			}
			return event
		},
		integrations: [
			Sentry.replayIntegration(),
			Sentry.browserProfilingIntegration(),
		],

		// Set tracesSampleRate to 1.0 to capture 100%
		// of transactions for performance monitoring.
		// We recommend adjusting this value in production
		tracesSampleRate: 1.0,

		// Capture Replay for 10% of all sessions,
		// plus for 100% of sessions with an error
		replaysSessionSampleRate: 0.1,
		replaysOnErrorSampleRate: 1.0,
	})
}
//react-router.config.ts
import { type Config } from '@react-router/dev/config'
import { sentryOnBuildEnd } from '@sentry/react-router'

const MODE = process.env.NODE_ENV

export default {
	// Defaults to true. Set to false to enable SPA for all routes.
	ssr: true,
	future: {
		unstable_optimizeDeps: true,
	},
	buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
		if (MODE === 'production' && process.env.SENTRY_AUTH_TOKEN) {
			await sentryOnBuildEnd({
				viteConfig,
				reactRouterConfig,
				buildManifest,
			})
		}
	},
} satisfies Config
// monitoring.ts
import prismaInstrumentation from '@prisma/instrumentation'
import { nodeProfilingIntegration } from '@sentry/profiling-node'
import * as Sentry from '@sentry/react-router'

// prisma's exports are wrong...
// https://github.com/prisma/prisma/issues/23410
const { PrismaInstrumentation } = prismaInstrumentation

export function init() {
	Sentry.init({
		dsn: process.env.SENTRY_DSN,
		environment: process.env.SENTRY_ENV,
		denyUrls: [
			/\/resources\/healthcheck/,
			// TODO: be smarter about the public assets...
			/\/build\//,
			/\/favicons\//,
			/\/img\//,
			/\/fonts\//,
			/\/favicon.ico/,
			/\/site\.webmanifest/,
		],
		integrations: [
			Sentry.prismaIntegration({
				prismaInstrumentation: new PrismaInstrumentation(),
			}),
			Sentry.httpIntegration(),
			nodeProfilingIntegration(),
		],
		tracesSampler(samplingContext) {
			// ignore healthcheck transactions by other services (consul, etc.)
			if (samplingContext.request?.url?.includes('/resources/healthcheck')) {
				return 0
			}
			return 1
		},
		beforeSendTransaction(event) {
			// ignore all healthcheck related transactions
			//  note that name of header here is case-sensitive
			if (event.request?.headers?.['x-healthcheck'] === 'true') {
				return null
			}

			return event
		},
	})
}
// index.ts
import * as Sentry from '@sentry/react-router'

// ...
const SENTRY_ENABLED = IS_PROD && process.env.SENTRY_DSN

if (SENTRY_ENABLED) {
	import('./utils/monitoring.js').then(({ init }) => init()).catch(console.log)
}

// this is actually different than the most recent epic-stack version which doesn't use helmet
app.use(
	helmet({
		xPoweredBy: false,
		referrerPolicy: { policy: 'same-origin' },
		crossOriginEmbedderPolicy: false,
		contentSecurityPolicy: {
			directives: {
				'connect-src': [
					MODE === 'development' ? 'ws:' : null,
					process.env.SENTRY_DSN ? '*.sentry.io' : null,
					"'self'",
				].filter(Boolean),
				// ...
			},
		},
	}),
)

// ...

closeWithGrace(async ({ err }) => {
	await new Promise((resolve, reject) => {
		server.close((e) => (e ? reject(e) : resolve('ok')))
	})
	if (err) {
		console.error(chalk.red(err))
		console.error(chalk.red(err.stack))
		if (SENTRY_ENABLED) {
			Sentry.captureException(err)
			await Sentry.flush(500)
		}
	}
})
// vite.config.ts
import { createRequire } from 'node:module'
import path from 'node:path'
import { reactRouter } from '@react-router/dev/vite'
import {
	sentryReactRouter,
	type SentryReactRouterBuildOptions,
} from '@sentry/react-router'
import { defineConfig } from 'vite'
import { envOnlyMacros } from 'vite-env-only'
import { viteStaticCopy } from 'vite-plugin-static-copy'

const require = createRequire(import.meta.url)
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'))

const MODE = process.env.NODE_ENV

const sentryConfig: SentryReactRouterBuildOptions = {
	authToken: process.env.SENTRY_AUTH_TOKEN,
	org: 'xxx',
	project: 'xxx',
	unstable_sentryVitePluginOptions: {
		release: {
			name: process.env.COMMIT_SHA,
			setCommits: {
				auto: true,
			},
		},
		sourcemaps: {
			filesToDeleteAfterUpload: ['./build/**/*.map', '.server-build/**/*.map'],
		},
	},
}

export default defineConfig((config) => ({
	build: {
		cssMinify: MODE === 'production',

		rollupOptions: {
			external: [/node:.*/, 'fsevents'],
		},

		assetsInlineLimit: (source: string) => {
			if (
				source.endsWith('sprite.svg') ||
				source.endsWith('favicon.svg') ||
				source.endsWith('apple-touch-icon.png')
			) {
				return false
			}
		},

		sourcemap: true,
	},
	optimizeDeps: {
		exclude: ['execa', 'npm-run-path', 'unicorn-magic'],
	},
	server: {
		watch: {
			ignored: ['**/playwright-report/**'],
		},
	},
	sentryConfig,
	plugins: [
		envOnlyMacros(),
		// it would be really nice to have this enabled in tests, but we'll have to
		// wait until https://github.com/remix-run/remix/issues/9871 is fixed
		MODE === 'test' ? null : reactRouter(),
		MODE === 'production' && process.env.SENTRY_AUTH_TOKEN
			? sentryReactRouter(sentryConfig, config)
			: null,
		viteStaticCopy({
			targets: [
				{
					src: path.join(pdfjsDistPath, 'build'),
					dest: 'pdfjs-dist',
				},
			],
		}),
	],
	test: {
		include: ['./app/**/*.test.{ts,tsx}'],
		setupFiles: ['./tests/setup/setup-test-env.ts'],
		globalSetup: ['./tests/setup/global-setup.ts'],
		restoreMocks: true,
		coverage: {
			include: ['app/**/*.{ts,tsx}'],
			exclude: ['app/components/**/*.{ts,tsx}', 'app/emails/**/*.{ts,tsx}'],
			all: true,
		},
	},
	ssr: {
		noExternal: [/@atlaskit\/pragmatic-drag-and-drop.*/, '@silk-hq/components'],
	},
}))
//entry.server.ts (abbreviated)
import * as Sentry from '@sentry/node'

export default async function handleRequest(...args: DocRequestArgs) {
	// ...

	if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
		responseHeaders.append('Document-Policy', 'js-profiling')
	}

        // ...
}

// ...

export function handleError(
	error: unknown,
	{ request }: LoaderFunctionArgs | ActionFunctionArgs,
): void {
	// Skip capturing if the request is aborted as Remix docs suggest
	// Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
	if (request.signal.aborted) {
		return
	}
	if (error instanceof Error) {
		console.error(chalk.red(error.stack))
		void Sentry.captureException(error)
	} else {
		console.error(error)
		Sentry.captureException(error)
	}
}

Expected Result

I should see frontend performance monitoring info.

Actual Result

Image

Events sent from frontend:

// event sent from frontend
fetch("https://xxx/envelope/?sentry_version=7&sentry_key=xxx&sentry_client=sentry.javascript.react-router%2F9.5.0", {
  "headers": {
    "accept": "*/*",
    "accept-language": "en-US,en;q=0.9",
    "content-type": "text/plain;charset=UTF-8",
    "priority": "u=1, i",
    "sec-ch-ua": "\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"macOS\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "cross-site",
    "Referer": "xxx",
    "Referrer-Policy": "strict-origin"
  },
  "body": "{\"sent_at\":\"2025-04-12T15:41:38.217Z\",\"sdk\":{\"name\":\"sentry.javascript.react-router\",\"version\":\"9.5.0\"}}\n{\"type\":\"session\"}\n{\"sid\":\"xxx\",\"init\":true,\"started\":\"2025-04-12T15:41:38.217Z\",\"timestamp\":\"2025-04-12T15:41:38.217Z\",\"status\":\"ok\",\"errors\":0,\"attrs\":{\"release\":\"xxx\",\"environment\":\"production\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\"}}",
  "method": "POST"
});
@chargome
Copy link
Member

Hey @NGimbal thanks for moving this here. Where do you call the init() from your monitoring.client.tsx? The init call should happen in entry.client.tsx

@NGimbal
Copy link
Author

NGimbal commented Apr 16, 2025

@chargome thanks for the reply - here's my entry.client.tsx I am calling init there it looks like:

// entry.client.tsx
import { startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'

if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
	void import('./utils/monitoring.client.tsx').then(({ init }) => init())
}

startTransition(() => {
	hydrateRoot(document, <HydratedRouter />)
})

@getsantry getsantry bot moved this to Waiting for: Product Owner in GitHub Issues with 👀 3 Apr 16, 2025
Copy link
Member

Generally looks alright I would say, can you add debug: true to your client init call and check the logs in your browser?

@getsantry getsantry bot moved this to Waiting for: Community in GitHub Issues with 👀 3 Apr 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Waiting for: Community
Development

No branches or pull requests

3 participants