From a2a3f25bd65917f4eb53ac5eab1bbab11b50c0f4 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:06:12 +0000 Subject: [PATCH 1/3] fix(sentry): group Drizzle errors by underlying cause, not SQL text Drizzle wraps query errors with a 'Failed query: ' message. beforeSend set a useful fingerprint, but the event's primary exception (used for the issue title) still carried the unique SQL string, so grouping regressed and the root cause (e.g. 'statement timeout') wasn't visible in the title. Rewrite the root exception value (last entry in event.exception.values, per Sentry's oldest-to-newest ordering) so its type/message reflect error.cause, and move the failed query + params into the drizzle_query context so they stay visible on the issue without polluting the title or fingerprint. Non-Drizzle errors and Drizzle errors without a cause are unaffected. --- apps/web/sentry.server.config.ts | 45 +++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index 3e9a600088..c937ea1efa 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -9,7 +9,7 @@ import { consoleLoggingIntegration, httpIntegration, init } from '@sentry/nextjs type DrizzleQueryError = Error & { query: string; params: unknown[]; - cause?: { code?: string; message?: string }; + cause?: { code?: string; message?: string; name?: string; constructor?: { name?: string } }; }; function isDrizzleQueryError(error: unknown): error is DrizzleQueryError { @@ -21,6 +21,15 @@ function isDrizzleQueryError(error: unknown): error is DrizzleQueryError { ); } +function causeTypeName(cause: NonNullable): string { + if (typeof cause.name === 'string' && cause.name.length > 0) return cause.name; + const ctorName = cause.constructor?.name; + if (typeof ctorName === 'string' && ctorName.length > 0 && ctorName !== 'Object') { + return ctorName; + } + return 'DatabaseError'; +} + const TRPC_4XX_CODES = new Set([ 'BAD_REQUEST', 'UNAUTHORIZED', @@ -74,18 +83,46 @@ if (process.env.NODE_ENV !== 'development') { return null; } - // Drizzle Queries are wrapped and that prevents Sentry from properly grouping them + // Drizzle wraps query errors with a `Failed query: ` message, + // which breaks Sentry grouping and hides the real root cause (e.g. a + // "statement timeout" on `error.cause`). Rewrite the primary exception so + // the reported error reflects the underlying cause, and move the failed + // query into a context so it stays visible on the issue without polluting + // the title or fingerprint. if (isDrizzleQueryError(error)) { - const pgCode = error.cause?.code; + const cause = error.cause; + const pgCode = cause?.code; event.fingerprint = [ 'drizzle-query-error', pgCode ?? 'generic', - error.cause?.message ?? 'generic', + cause?.message ?? 'generic', ]; event.tags = { ...event.tags, 'db.error_code': pgCode, }; + event.contexts = { + ...event.contexts, + drizzle_query: { + query: error.query, + params: error.params, + wrapper_message: error.message, + }, + }; + + if (cause) { + // `event.exception.values` is ordered oldest-to-newest; the last + // entry is the root exception Sentry uses for the title and + // grouping. Rewrite it so the title/type reflect the cause while + // keeping the wrapper's stack trace (which points to our code, + // unlike the pg-internal one). + const values = event.exception?.values; + if (values && values.length > 0) { + const root = values[values.length - 1]; + root.type = causeTypeName(cause); + root.value = cause.message ?? 'unknown database error'; + } + } } return event; }, From d2d958ff4980f8d793de555c18531b43dfbe2f01 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 30 Apr 2026 14:38:22 +0200 Subject: [PATCH 2/3] fix(sentry): collapse Drizzle wrapped exceptions --- apps/web/sentry.server.config.ts | 155 ++++++++++++++++++------------- 1 file changed, 88 insertions(+), 67 deletions(-) diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index c937ea1efa..a4ae528c6d 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -12,6 +12,8 @@ type DrizzleQueryError = Error & { cause?: { code?: string; message?: string; name?: string; constructor?: { name?: string } }; }; +const GENERIC_ERROR_TYPE_NAMES = new Set(['Error', 'error']); + function isDrizzleQueryError(error: unknown): error is DrizzleQueryError { return ( error instanceof Error && @@ -22,14 +24,35 @@ function isDrizzleQueryError(error: unknown): error is DrizzleQueryError { } function causeTypeName(cause: NonNullable): string { - if (typeof cause.name === 'string' && cause.name.length > 0) return cause.name; + if (typeof cause.code === 'string' && /^[A-Z0-9]{5}$/.test(cause.code)) { + return 'PostgresError'; + } + + if ( + typeof cause.name === 'string' && + cause.name.length > 0 && + !GENERIC_ERROR_TYPE_NAMES.has(cause.name) + ) { + return cause.name; + } + const ctorName = cause.constructor?.name; - if (typeof ctorName === 'string' && ctorName.length > 0 && ctorName !== 'Object') { + if ( + typeof ctorName === 'string' && + ctorName.length > 0 && + ctorName !== 'Object' && + !GENERIC_ERROR_TYPE_NAMES.has(ctorName) + ) { return ctorName; } + return 'DatabaseError'; } +function isDrizzleWrapperException(value: { value?: string }): boolean { + return typeof value.value === 'string' && value.value.startsWith('Failed query:'); +} + const TRPC_4XX_CODES = new Set([ 'BAD_REQUEST', 'UNAUTHORIZED', @@ -55,76 +78,74 @@ function isTRPC4xxError(error: unknown): boolean { ); } -if (process.env.NODE_ENV !== 'development') { - init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, +init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - // Tracing is fully disabled. - tracesSampleRate: 0, + // Tracing is fully disabled. + tracesSampleRate: 0, - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - normalizeDepth: 5, + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + normalizeDepth: 5, - // Skip Sentry's OTEL setup because we are using Vercel's OTEL with SentrySpanProcessor - skipOpenTelemetrySetup: true, + // Skip Sentry's OTEL setup because we are using Vercel's OTEL with SentrySpanProcessor + skipOpenTelemetrySetup: true, - integrations: [ - // Keep Sentry's httpIntegration for correct request isolation, but do not - // emit spans here because tracing spans are produced by Vercel's OTel. - httpIntegration({ spans: false }), - // send console.log, console.error, and console.warn calls as logs to Sentry - consoleLoggingIntegration({ levels: ['log', 'error', 'warn'] }), - ], + integrations: [ + // Keep Sentry's httpIntegration for correct request isolation, but do not + // emit spans here because tracing spans are produced by Vercel's OTel. + httpIntegration({ spans: false }), + // send console.log, console.error, and console.warn calls as logs to Sentry + consoleLoggingIntegration({ levels: ['log', 'error', 'warn'] }), + ], - beforeSend(event, hint) { - const error = hint.originalException; - if (isTRPC4xxError(error)) { - return null; - } + beforeSend(event, hint) { + const error = hint.originalException; + if (isTRPC4xxError(error)) { + return null; + } - // Drizzle wraps query errors with a `Failed query: ` message, - // which breaks Sentry grouping and hides the real root cause (e.g. a - // "statement timeout" on `error.cause`). Rewrite the primary exception so - // the reported error reflects the underlying cause, and move the failed - // query into a context so it stays visible on the issue without polluting - // the title or fingerprint. - if (isDrizzleQueryError(error)) { - const cause = error.cause; - const pgCode = cause?.code; - event.fingerprint = [ - 'drizzle-query-error', - pgCode ?? 'generic', - cause?.message ?? 'generic', - ]; - event.tags = { - ...event.tags, - 'db.error_code': pgCode, - }; - event.contexts = { - ...event.contexts, - drizzle_query: { - query: error.query, - params: error.params, - wrapper_message: error.message, - }, - }; - - if (cause) { - // `event.exception.values` is ordered oldest-to-newest; the last - // entry is the root exception Sentry uses for the title and - // grouping. Rewrite it so the title/type reflect the cause while - // keeping the wrapper's stack trace (which points to our code, - // unlike the pg-internal one). - const values = event.exception?.values; - if (values && values.length > 0) { - const root = values[values.length - 1]; - root.type = causeTypeName(cause); - root.value = cause.message ?? 'unknown database error'; - } + // Drizzle wraps query errors with a `Failed query: ` message, + // which breaks Sentry grouping and hides the real root cause (e.g. a + // "statement timeout" on `error.cause`). Rewrite the primary exception so + // the reported error reflects the underlying cause, and move the failed + // query into a context so it stays visible on the issue without polluting + // the title or fingerprint. + if (isDrizzleQueryError(error)) { + const cause = error.cause; + const pgCode = cause?.code; + event.fingerprint = ['drizzle-query-error', pgCode ?? 'generic', cause?.message ?? 'generic']; + event.tags = { + ...event.tags, + 'db.error_code': pgCode, + }; + event.contexts = { + ...event.contexts, + drizzle_query: { + query: error.query, + params: error.params, + wrapper_message: error.message, + }, + }; + + if (cause) { + // Prefer the Drizzle wrapper so we keep the stack that points through + // our code, then drop serialized cause entries because they duplicate + // the rewritten primary exception. + const values = event.exception?.values; + if (values && values.length > 0) { + const primaryException = + values.find(isDrizzleWrapperException) ?? values[values.length - 1]; + primaryException.type = causeTypeName(cause); + primaryException.value = cause.message ?? 'unknown database error'; + event.exception = { + ...event.exception, + values: [primaryException], + }; } } - return event; - }, - }); -} + } + + return event; + }, +}); From bb1aff012bd97d890c16fb9cadb261bcf9c6a1fe Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 30 Apr 2026 15:00:50 +0200 Subject: [PATCH 3/3] fix(sentry): omit Drizzle query params --- apps/web/sentry.server.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index a4ae528c6d..c78fbfb138 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -123,7 +123,6 @@ init({ ...event.contexts, drizzle_query: { query: error.query, - params: error.params, wrapper_message: error.message, }, };