Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion app/routes/resources/healthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
77 changes: 77 additions & 0 deletions app/utils/sentry-event-filters.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
72 changes: 72 additions & 0 deletions app/utils/sentry-event-filters.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> | null
} | null
tags?: Record<string, unknown> | null
contexts?: {
trace?: {
data?: Record<string, unknown> | 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),
)
}
2 changes: 1 addition & 1 deletion docs/skills/epic-deployment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/epic-routing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
22 changes: 19 additions & 3 deletions server/utils/monitoring.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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(),
Expand All @@ -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
}

Expand Down
Loading