11import {
2- type SpanAttributes ,
2+ type StartSpanOptions ,
3+ addBreadcrumb ,
34 captureException ,
45 debug ,
56 flushIfServerless ,
6- SEMANTIC_ATTRIBUTE_SENTRY_OP ,
77 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
88 SPAN_STATUS_ERROR ,
9- SPAN_STATUS_OK ,
109 startSpan ,
1110} from '@sentry/core' ;
12- import type { Database } from 'db0' ;
11+ import type { Database , PreparedStatement } from 'db0' ;
1312// eslint-disable-next-line import/no-extraneous-dependencies
1413import { defineNitroPlugin , useDatabase } from 'nitropack/runtime' ;
1514
15+ type PreparedStatementType = 'get' | 'run' | 'all' | 'raw' ;
16+
17+ /**
18+ * Keeps track of prepared statements that have been patched.
19+ */
20+ const patchedStatement = new WeakSet < PreparedStatement > ( ) ;
21+
1622/**
1723 * Creates a Nitro plugin that instruments the database calls.
1824 */
@@ -27,52 +33,151 @@ export default defineNitroPlugin(() => {
2733} ) ;
2834
2935function instrumentDatabase ( db : Database ) : void {
36+ db . prepare = new Proxy ( db . prepare , {
37+ apply ( target , thisArg , args : Parameters < typeof db . prepare > ) {
38+ const [ query ] = args ;
39+
40+ return instrumentPreparedStatement ( target . apply ( thisArg , args ) , query , db . dialect ) ;
41+ } ,
42+ } ) ;
43+
44+ // Sadly the `.sql` template tag doesn't call `db.prepare` internally and it calls the connector's `.prepare` directly
45+ // So we have to patch it manually, and would mean we would have less info in the spans.
46+ // https://github.com/unjs/db0/blob/main/src/database.ts#L64
3047 db . sql = new Proxy ( db . sql , {
3148 apply ( target , thisArg , args : Parameters < typeof db . sql > ) {
32- const query = args [ 0 ] ?. [ 0 ] ;
33- const attributes = getSpanAttributes ( db , query ) ;
34-
35- return startSpan (
36- {
37- name : query || 'db.query' ,
38- attributes,
39- } ,
40- async span => {
41- try {
42- const result = await target . apply ( thisArg , args ) ;
43- span . setStatus ( { code : SPAN_STATUS_OK } ) ;
44-
45- return result ;
46- } catch ( error ) {
47- span . setStatus ( { code : SPAN_STATUS_ERROR , message : 'internal_error' } ) ;
48- captureException ( error , {
49- mechanism : {
50- handled : false ,
51- type : attributes [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] ,
52- } ,
53- } ) ;
54-
55- // Re-throw the error to be handled by the caller
56- throw error ;
57- } finally {
58- await flushIfServerless ( ) ;
59- }
60- } ,
61- ) ;
49+ const query = args [ 0 ] ?. [ 0 ] ?? '' ;
50+ const opts = createStartSpanOptions ( query , db . dialect ) ;
51+
52+ return startSpan ( opts , async span => {
53+ try {
54+ const result = await target . apply ( thisArg , args ) ;
55+
56+ return result ;
57+ } catch ( error ) {
58+ span . setStatus ( { code : SPAN_STATUS_ERROR , message : 'internal_error' } ) ;
59+ captureException ( error , {
60+ mechanism : {
61+ handled : false ,
62+ type : opts . attributes ?. [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] ,
63+ } ,
64+ } ) ;
65+
66+ // Re-throw the error to be handled by the caller
67+ throw error ;
68+ } finally {
69+ await flushIfServerless ( ) ;
70+ }
71+ } ) ;
72+ } ,
73+ } ) ;
74+
75+ db . exec = new Proxy ( db . exec , {
76+ apply ( target , thisArg , args : Parameters < typeof db . exec > ) {
77+ return startSpan ( createStartSpanOptions ( args [ 0 ] , db . dialect , 'run' ) , async ( ) => {
78+ const result = await target . apply ( thisArg , args ) ;
79+
80+ createBreadcrumb ( args [ 0 ] , 'run' ) ;
81+
82+ return result ;
83+ } ) ;
6284 } ,
6385 } ) ;
6486}
6587
66- function getSpanAttributes ( db : Database , query ?: string ) : SpanAttributes {
67- const attributes : SpanAttributes = {
68- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.db.nuxt' ,
69- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'db' ,
70- 'db.system' : db . dialect ,
71- } ;
88+ /**
89+ * Instruments a DB prepared statement with Sentry.
90+ *
91+ * This is meant to be used as a top-level call, under the hood it calls `instrumentPreparedStatementQueries`
92+ * to patch the query methods. The reason for this abstraction is to ensure that the `bind` method is also patched.
93+ */
94+ function instrumentPreparedStatement ( statement : PreparedStatement , query : string , dialect : string ) : PreparedStatement {
95+ // statement.bind() returns a new instance of D1PreparedStatement, so we have to patch it as well.
96+ // eslint-disable-next-line @typescript-eslint/unbound-method
97+ statement . bind = new Proxy ( statement . bind , {
98+ apply ( target , thisArg , args : Parameters < typeof statement . bind > ) {
99+ return instrumentPreparedStatementQueries ( target . apply ( thisArg , args ) , query , dialect ) ;
100+ } ,
101+ } ) ;
72102
73- if ( query ) {
74- attributes [ 'db.query' ] = query ;
103+ return instrumentPreparedStatementQueries ( statement , query , dialect ) ;
104+ }
105+
106+ /**
107+ * Patches the query methods of a DB prepared statement with Sentry.
108+ */
109+ function instrumentPreparedStatementQueries (
110+ statement : PreparedStatement ,
111+ query : string ,
112+ dialect : string ,
113+ ) : PreparedStatement {
114+ if ( patchedStatement . has ( statement ) ) {
115+ return statement ;
75116 }
76117
77- return attributes ;
118+ // eslint-disable-next-line @typescript-eslint/unbound-method
119+ statement . get = new Proxy ( statement . get , {
120+ apply ( target , thisArg , args : Parameters < typeof statement . get > ) {
121+ return startSpan ( createStartSpanOptions ( query , dialect , 'get' ) , async ( ) => {
122+ const result = await target . apply ( thisArg , args ) ;
123+ createBreadcrumb ( query , 'get' ) ;
124+
125+ return result ;
126+ } ) ;
127+ } ,
128+ } ) ;
129+
130+ // eslint-disable-next-line @typescript-eslint/unbound-method
131+ statement . run = new Proxy ( statement . run , {
132+ apply ( target , thisArg , args : Parameters < typeof statement . run > ) {
133+ return startSpan ( createStartSpanOptions ( query , dialect , 'run' ) , async ( ) => {
134+ const result = await target . apply ( thisArg , args ) ;
135+ createBreadcrumb ( query , 'run' ) ;
136+
137+ return result ;
138+ } ) ;
139+ } ,
140+ } ) ;
141+
142+ // eslint-disable-next-line @typescript-eslint/unbound-method
143+ statement . all = new Proxy ( statement . all , {
144+ apply ( target , thisArg , args : Parameters < typeof statement . all > ) {
145+ return startSpan ( createStartSpanOptions ( query , dialect , 'all' ) , async ( ) => {
146+ const result = await target . apply ( thisArg , args ) ;
147+ // Since all has no regular shape, we can assume if it returns an array, it's a success.
148+ createBreadcrumb ( query , 'all' ) ;
149+
150+ return result ;
151+ } ) ;
152+ } ,
153+ } ) ;
154+
155+ patchedStatement . add ( statement ) ;
156+
157+ return statement ;
158+ }
159+
160+ function createBreadcrumb ( query : string , type : PreparedStatementType ) : void {
161+ addBreadcrumb ( {
162+ category : 'query' ,
163+ message : query ,
164+ data : {
165+ 'db.query_type' : type ,
166+ } ,
167+ } ) ;
168+ }
169+
170+ /**
171+ * Creates a start span options object.
172+ */
173+ function createStartSpanOptions ( query : string , dialect : string , type ?: PreparedStatementType ) : StartSpanOptions {
174+ return {
175+ op : 'db.query' ,
176+ name : query ,
177+ attributes : {
178+ 'db.system' : dialect ,
179+ 'db.query_type' : type ,
180+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.db.nuxt' ,
181+ } ,
182+ } ;
78183}
0 commit comments