From b7aac3b1b65d0cf8618eada85ecf7cbcf4d627f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 20:02:28 +0000 Subject: [PATCH 1/2] fix: probe DB health with SELECT 1 instead of User.count Fly healthchecks were generating Slow DB Query Sentry insights from prisma.user.count() on the tiny demo VM without indicating app issues. Co-authored-by: Kent C. Dodds --- app/routes/resources/healthcheck.tsx | 4 +++- docs/skills/epic-deployment/SKILL.md | 2 +- docs/skills/epic-routing/SKILL.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/routes/resources/healthcheck.tsx b/app/routes/resources/healthcheck.tsx index e38230ec6..c7871d34a 100644 --- a/app/routes/resources/healthcheck.tsx +++ b/app/routes/resources/healthcheck.tsx @@ -9,8 +9,10 @@ export async function loader({ request }: Route.LoaderArgs) { try { // if we can connect to the database and make a simple query // and make a HEAD request to ourselves, then we're good. + // Prefer SELECT 1 over User.count(): count() was producing Slow DB Query + // Sentry insights on the tiny Fly demo VM without indicating app issues. await Promise.all([ - prisma.user.count(), + prisma.$queryRaw`SELECT 1`, fetch(`${new URL(request.url).protocol}${host}`, { method: 'HEAD', headers: { 'X-Healthcheck': 'true' }, diff --git a/docs/skills/epic-deployment/SKILL.md b/docs/skills/epic-deployment/SKILL.md index 107745043..164f411a3 100644 --- a/docs/skills/epic-deployment/SKILL.md +++ b/docs/skills/epic-deployment/SKILL.md @@ -122,7 +122,7 @@ export async function loader({ request }: Route.LoaderArgs) { try { await Promise.all([ - prisma.user.count(), // Verify DB + prisma.$queryRaw`SELECT 1`, // Verify DB fetch(`${new URL(request.url).protocol}${host}`, { method: 'HEAD', headers: { 'X-Healthcheck': 'true' }, diff --git a/docs/skills/epic-routing/SKILL.md b/docs/skills/epic-routing/SKILL.md index e989d408e..6596c5205 100644 --- a/docs/skills/epic-routing/SKILL.md +++ b/docs/skills/epic-routing/SKILL.md @@ -207,7 +207,7 @@ export async function loader({ request }: Route.LoaderArgs) { try { await Promise.all([ - prisma.user.count(), // Check DB + prisma.$queryRaw`SELECT 1`, // Check DB fetch(`${new URL(request.url).protocol}${host}`, { method: 'HEAD', headers: { 'X-Healthcheck': 'true' }, From 3ac8771002dbb9bf5b9563657a23f4914d5b99c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 20:02:28 +0000 Subject: [PATCH 2/2] fix: drop expected Sentry noise from bots and healthchecks Filter React Router missing loader/action/method errors and orphaned healthcheck Prisma transactions so the public demo issue stream stays actionable. Co-authored-by: Kent C. Dodds --- app/entry.server.tsx | 10 ++++ app/utils/sentry-event-filters.test.ts | 77 ++++++++++++++++++++++++++ app/utils/sentry-event-filters.ts | 72 ++++++++++++++++++++++++ server/utils/monitoring.ts | 22 +++++++- 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 app/utils/sentry-event-filters.test.ts create mode 100644 app/utils/sentry-event-filters.ts diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 99fdd4b88..1e99a19ae 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -15,6 +15,7 @@ import { import { getEnv, init } from './utils/env.server.ts' import { getInstanceInfo } from './utils/litefs.server.ts' import { NonceProvider } from './utils/nonce-provider.ts' +import { isExpectedReactRouterErrorMessage } from './utils/sentry-event-filters.ts' import { makeTimings } from './utils/timing.server.ts' export const streamTimeout = 5000 @@ -132,6 +133,15 @@ export function handleError( return } + // Expected React Router responses to unsupported methods / missing handlers + // (common from bots and scanners on the public demo). Don't alert. + if ( + error instanceof Error && + isExpectedReactRouterErrorMessage(error.message) + ) { + return + } + if (error instanceof Error) { console.error(styleText('red', String(error.stack))) } else { diff --git a/app/utils/sentry-event-filters.test.ts b/app/utils/sentry-event-filters.test.ts new file mode 100644 index 000000000..63a73c823 --- /dev/null +++ b/app/utils/sentry-event-filters.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from 'vitest' +import { + isExpectedReactRouterErrorMessage, + isHealthcheckTransaction, + shouldDropErrorEvent, +} from './sentry-event-filters.ts' + +test('detects missing action / loader / invalid method router errors', () => { + expect( + isExpectedReactRouterErrorMessage( + 'You made a POST request to "/" but did not provide an `action` for route "root", so there is no way to handle the request.', + ), + ).toBe(true) + expect( + isExpectedReactRouterErrorMessage( + 'You made a GET request to "/resources/theme-switch" but did not provide a `loader` for route "routes/resources/theme-switch", so there is no way to handle the request.', + ), + ).toBe(true) + expect( + isExpectedReactRouterErrorMessage('Invalid request method "OPTIONS"'), + ).toBe(true) + expect(isExpectedReactRouterErrorMessage('Database connection failed')).toBe( + false, + ) +}) + +test('shouldDropErrorEvent reads exception values', () => { + expect( + shouldDropErrorEvent({ + exception: { + values: [ + { + value: + 'You made a POST request to "/" but did not provide an `action` for route "root", so there is no way to handle the request.', + }, + ], + }, + }), + ).toBe(true) + expect( + shouldDropErrorEvent({ + exception: { values: [{ value: 'Unexpected server failure' }] }, + }), + ).toBe(false) +}) + +test('isHealthcheckTransaction matches url, header, and orphaned prisma dsc', () => { + expect( + isHealthcheckTransaction({ + request: { url: 'http://127.0.0.1:8080/resources/healthcheck' }, + }), + ).toBe(true) + expect( + isHealthcheckTransaction({ + request: { headers: { 'x-healthcheck': 'true' } }, + }), + ).toBe(true) + expect( + isHealthcheckTransaction({ + transaction: 'prisma:client:operation', + tags: { url: 'http://172.19.14.2:8080/resources/healthcheck' }, + contexts: { + trace: { + data: { + 'sentry.dsc.transaction': 'GET /resources/healthcheck', + }, + }, + }, + }), + ).toBe(true) + expect( + isHealthcheckTransaction({ + transaction: 'GET /users', + request: { url: 'https://example.com/users' }, + }), + ).toBe(false) +}) diff --git a/app/utils/sentry-event-filters.ts b/app/utils/sentry-event-filters.ts new file mode 100644 index 000000000..8ea4d83d3 --- /dev/null +++ b/app/utils/sentry-event-filters.ts @@ -0,0 +1,72 @@ +/** + * Pure helpers for dropping expected Sentry noise from the public Epic Stack + * demo (bots, scanners, Fly healthchecks) without hiding real application bugs. + */ + +const EXPECTED_REACT_ROUTER_ERROR_PATTERNS = [ + /did not provide an `action`/i, + /did not provide a `loader`/i, + /^Invalid request method /i, +] + +export function isExpectedReactRouterErrorMessage(message: string): boolean { + return EXPECTED_REACT_ROUTER_ERROR_PATTERNS.some((pattern) => + pattern.test(message), + ) +} + +export function getEventErrorMessages(event: { + message?: string + exception?: { values?: Array<{ value?: string | null } | null> | null } +}): string[] { + const messages: string[] = [] + for (const value of event.exception?.values ?? []) { + if (value?.value) messages.push(value.value) + } + if (event.message) messages.push(event.message) + return messages +} + +export function shouldDropErrorEvent(event: { + message?: string + exception?: { values?: Array<{ value?: string | null } | null> | null } +}): boolean { + return getEventErrorMessages(event).some(isExpectedReactRouterErrorMessage) +} + +type TransactionLike = { + transaction?: string | null + request?: { + url?: string | null + headers?: Record | null + } | null + tags?: Record | null + contexts?: { + trace?: { + data?: Record | null + } | null + } | null +} + +function includesHealthcheckPath(value: string): boolean { + return value.includes('/resources/healthcheck') +} + +export function isHealthcheckTransaction(event: TransactionLike): boolean { + if (event.request?.headers?.['x-healthcheck'] === 'true') { + return true + } + + const candidates = [ + event.request?.url, + event.transaction, + typeof event.tags?.url === 'string' ? event.tags.url : null, + typeof event.contexts?.trace?.data?.['sentry.dsc.transaction'] === 'string' + ? event.contexts.trace.data['sentry.dsc.transaction'] + : null, + ] + + return candidates.some( + (value) => typeof value === 'string' && includesHealthcheckPath(value), + ) +} diff --git a/server/utils/monitoring.ts b/server/utils/monitoring.ts index 107521a10..4b331744e 100644 --- a/server/utils/monitoring.ts +++ b/server/utils/monitoring.ts @@ -1,6 +1,10 @@ import { PrismaInstrumentation } from '@prisma/instrumentation' import { nodeProfilingIntegration } from '@sentry/profiling-node' import * as Sentry from '@sentry/react-router' +import { + isHealthcheckTransaction, + shouldDropErrorEvent, +} from '../../app/utils/sentry-event-filters.ts' export function init() { Sentry.init({ @@ -16,6 +20,12 @@ export function init() { /\/favicon.ico/, /\/site\.webmanifest/, ], + ignoreErrors: [ + // Bots/scanners hitting routes without the matching loader/action/method. + /did not provide an `action`/, + /did not provide a `loader`/, + /^Invalid request method /, + ], integrations: [ Sentry.prismaIntegration({ prismaInstrumentation: new PrismaInstrumentation(), @@ -30,10 +40,16 @@ export function init() { } return process.env.NODE_ENV === 'production' ? 1 : 0 }, + beforeSend(event) { + if (shouldDropErrorEvent(event)) { + return null + } + return event + }, beforeSendTransaction(event) { - // ignore all healthcheck related transactions - // note that name of header here is case-sensitive - if (event.request?.headers?.['x-healthcheck'] === 'true') { + // Drop Fly/consul healthchecks, including orphaned Prisma spans that + // surface as Slow DB Query insights with transaction prisma:client:operation. + if (isHealthcheckTransaction(event)) { return null }