Skip to content

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

Closed
@NGimbal

Description

@NGimbal

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"
});

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status

    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions