@@ -9,9 +9,11 @@ import { consoleLoggingIntegration, httpIntegration, init } from '@sentry/nextjs
99type DrizzleQueryError = Error & {
1010 query : string ;
1111 params : unknown [ ] ;
12- cause ?: { code ?: string ; message ?: string } ;
12+ cause ?: { code ?: string ; message ?: string ; name ?: string ; constructor ?: { name ?: string } } ;
1313} ;
1414
15+ const GENERIC_ERROR_TYPE_NAMES = new Set ( [ 'Error' , 'error' ] ) ;
16+
1517function isDrizzleQueryError ( error : unknown ) : error is DrizzleQueryError {
1618 return (
1719 error instanceof Error &&
@@ -21,6 +23,36 @@ function isDrizzleQueryError(error: unknown): error is DrizzleQueryError {
2123 ) ;
2224}
2325
26+ function causeTypeName ( cause : NonNullable < DrizzleQueryError [ 'cause' ] > ) : string {
27+ if ( typeof cause . code === 'string' && / ^ [ A - Z 0 - 9 ] { 5 } $ / . test ( cause . code ) ) {
28+ return 'PostgresError' ;
29+ }
30+
31+ if (
32+ typeof cause . name === 'string' &&
33+ cause . name . length > 0 &&
34+ ! GENERIC_ERROR_TYPE_NAMES . has ( cause . name )
35+ ) {
36+ return cause . name ;
37+ }
38+
39+ const ctorName = cause . constructor ?. name ;
40+ if (
41+ typeof ctorName === 'string' &&
42+ ctorName . length > 0 &&
43+ ctorName !== 'Object' &&
44+ ! GENERIC_ERROR_TYPE_NAMES . has ( ctorName )
45+ ) {
46+ return ctorName ;
47+ }
48+
49+ return 'DatabaseError' ;
50+ }
51+
52+ function isDrizzleWrapperException ( value : { value ?: string } ) : boolean {
53+ return typeof value . value === 'string' && value . value . startsWith ( 'Failed query:' ) ;
54+ }
55+
2456const TRPC_4XX_CODES = new Set ( [
2557 'BAD_REQUEST' ,
2658 'UNAUTHORIZED' ,
@@ -46,48 +78,73 @@ function isTRPC4xxError(error: unknown): boolean {
4678 ) ;
4779}
4880
49- if ( process . env . NODE_ENV !== 'development' ) {
50- init ( {
51- dsn : process . env . NEXT_PUBLIC_SENTRY_DSN ,
81+ init ( {
82+ dsn : process . env . NEXT_PUBLIC_SENTRY_DSN ,
5283
53- // Tracing is fully disabled.
54- tracesSampleRate : 0 ,
84+ // Tracing is fully disabled.
85+ tracesSampleRate : 0 ,
5586
56- // Setting this option to true will print useful information to the console while you're setting up Sentry.
57- debug : false ,
58- normalizeDepth : 5 ,
87+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
88+ debug : false ,
89+ normalizeDepth : 5 ,
5990
60- // Skip Sentry's OTEL setup because we are using Vercel's OTEL with SentrySpanProcessor
61- skipOpenTelemetrySetup : true ,
91+ // Skip Sentry's OTEL setup because we are using Vercel's OTEL with SentrySpanProcessor
92+ skipOpenTelemetrySetup : true ,
6293
63- integrations : [
64- // Keep Sentry's httpIntegration for correct request isolation, but do not
65- // emit spans here because tracing spans are produced by Vercel's OTel.
66- httpIntegration ( { spans : false } ) ,
67- // send console.log, console.error, and console.warn calls as logs to Sentry
68- consoleLoggingIntegration ( { levels : [ 'log' , 'error' , 'warn' ] } ) ,
69- ] ,
94+ integrations : [
95+ // Keep Sentry's httpIntegration for correct request isolation, but do not
96+ // emit spans here because tracing spans are produced by Vercel's OTel.
97+ httpIntegration ( { spans : false } ) ,
98+ // send console.log, console.error, and console.warn calls as logs to Sentry
99+ consoleLoggingIntegration ( { levels : [ 'log' , 'error' , 'warn' ] } ) ,
100+ ] ,
70101
71- beforeSend ( event , hint ) {
72- const error = hint . originalException ;
73- if ( isTRPC4xxError ( error ) ) {
74- return null ;
75- }
102+ beforeSend ( event , hint ) {
103+ const error = hint . originalException ;
104+ if ( isTRPC4xxError ( error ) ) {
105+ return null ;
106+ }
76107
77- // Drizzle Queries are wrapped and that prevents Sentry from properly grouping them
78- if ( isDrizzleQueryError ( error ) ) {
79- const pgCode = error . cause ?. code ;
80- event . fingerprint = [
81- 'drizzle-query-error' ,
82- pgCode ?? 'generic' ,
83- error . cause ?. message ?? 'generic' ,
84- ] ;
85- event . tags = {
86- ...event . tags ,
87- 'db.error_code' : pgCode ,
88- } ;
108+ // Drizzle wraps query errors with a `Failed query: <unique SQL>` message,
109+ // which breaks Sentry grouping and hides the real root cause (e.g. a
110+ // "statement timeout" on `error.cause`). Rewrite the primary exception so
111+ // the reported error reflects the underlying cause, and move the failed
112+ // query into a context so it stays visible on the issue without polluting
113+ // the title or fingerprint.
114+ if ( isDrizzleQueryError ( error ) ) {
115+ const cause = error . cause ;
116+ const pgCode = cause ?. code ;
117+ event . fingerprint = [ 'drizzle-query-error' , pgCode ?? 'generic' , cause ?. message ?? 'generic' ] ;
118+ event . tags = {
119+ ...event . tags ,
120+ 'db.error_code' : pgCode ,
121+ } ;
122+ event . contexts = {
123+ ...event . contexts ,
124+ drizzle_query : {
125+ query : error . query ,
126+ wrapper_message : error . message ,
127+ } ,
128+ } ;
129+
130+ if ( cause ) {
131+ // Prefer the Drizzle wrapper so we keep the stack that points through
132+ // our code, then drop serialized cause entries because they duplicate
133+ // the rewritten primary exception.
134+ const values = event . exception ?. values ;
135+ if ( values && values . length > 0 ) {
136+ const primaryException =
137+ values . find ( isDrizzleWrapperException ) ?? values [ values . length - 1 ] ;
138+ primaryException . type = causeTypeName ( cause ) ;
139+ primaryException . value = cause . message ?? 'unknown database error' ;
140+ event . exception = {
141+ ...event . exception ,
142+ values : [ primaryException ] ,
143+ } ;
144+ }
89145 }
90- return event ;
91- } ,
92- } ) ;
93- }
146+ }
147+
148+ return event ;
149+ } ,
150+ } ) ;
0 commit comments