|
1 | | -import { |
2 | | - addBreadcrumb, |
3 | | - captureException, |
4 | | - debug, |
5 | | - flushIfServerless, |
6 | | - SEMANTIC_ATTRIBUTE_SENTRY_OP, |
7 | | - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
8 | | - type Span, |
9 | | - SPAN_STATUS_ERROR, |
10 | | - startSpan, |
11 | | - type StartSpanOptions, |
12 | | -} from '@sentry/core'; |
13 | | -import type { Database, PreparedStatement } from 'db0'; |
14 | | -import type { NitroAppPlugin } from 'nitropack'; |
15 | | -import { useDatabase } from 'nitropack/runtime'; |
16 | | -import type { DatabaseConnectionConfig as DatabaseConfig } from 'nitropack/types'; |
| 1 | +import type { NitroAppPlugin } from 'nitro/types'; |
| 2 | +import { useDatabase } from 'nitro/database'; |
17 | 3 | // @ts-expect-error - This is a virtual module |
18 | 4 | import { databaseConfig } from '#sentry/database-config.mjs'; |
19 | | -import { type DatabaseSpanData, getDatabaseSpanData } from '../utils/database-span-data'; |
20 | | - |
21 | | -type MaybeInstrumentedDatabase = Database & { |
22 | | - __sentry_instrumented__?: boolean; |
23 | | -}; |
24 | | - |
25 | | -/** |
26 | | - * Keeps track of prepared statements that have been patched. |
27 | | - */ |
28 | | -const patchedStatement = new WeakSet<PreparedStatement>(); |
| 5 | +import type { DatabaseConnectionConfig } from '../utils/database-span-data'; |
| 6 | +import { createDatabasePlugin } from '../utils/instrumentDatabase'; |
29 | 7 |
|
30 | 8 | /** |
31 | | - * The Sentry origin for the database plugin. |
32 | | - */ |
33 | | -const SENTRY_ORIGIN = 'auto.db.nuxt'; |
34 | | - |
35 | | -/** |
36 | | - * Creates a Nitro plugin that instruments the database calls. |
| 9 | + * Nitro plugin that instruments database calls for Nuxt v5+ (Nitro v3+) |
37 | 10 | */ |
38 | 11 | export default (() => { |
39 | | - try { |
40 | | - const _databaseConfig = databaseConfig as Record<string, DatabaseConfig>; |
41 | | - const databaseInstances = Object.keys(databaseConfig); |
42 | | - debug.log('[Nitro Database Plugin]: Instrumenting databases...'); |
43 | | - |
44 | | - for (const instance of databaseInstances) { |
45 | | - debug.log('[Nitro Database Plugin]: Instrumenting database instance:', instance); |
46 | | - const db = useDatabase(instance); |
47 | | - instrumentDatabase(db, _databaseConfig[instance]); |
48 | | - } |
49 | | - |
50 | | - debug.log('[Nitro Database Plugin]: Databases instrumented.'); |
51 | | - } catch (error) { |
52 | | - // During build time, we can't use the useDatabase function, so we just log an error. |
53 | | - if (error instanceof Error && /Cannot access 'instances'/.test(error.message)) { |
54 | | - debug.log('[Nitro Database Plugin]: Database instrumentation skipped during build time.'); |
55 | | - return; |
56 | | - } |
57 | | - |
58 | | - debug.error('[Nitro Database Plugin]: Failed to instrument database:', error); |
59 | | - } |
| 12 | + createDatabasePlugin(useDatabase, databaseConfig as Record<string, DatabaseConnectionConfig>); |
60 | 13 | }) satisfies NitroAppPlugin; |
61 | | - |
62 | | -/** |
63 | | - * Instruments a database instance with Sentry. |
64 | | - */ |
65 | | -function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConfig): void { |
66 | | - if (db.__sentry_instrumented__) { |
67 | | - debug.log('[Nitro Database Plugin]: Database already instrumented. Skipping...'); |
68 | | - return; |
69 | | - } |
70 | | - |
71 | | - const metadata: DatabaseSpanData = { |
72 | | - 'db.system.name': config?.connector ?? db.dialect, |
73 | | - ...getDatabaseSpanData(config), |
74 | | - }; |
75 | | - |
76 | | - db.prepare = new Proxy(db.prepare, { |
77 | | - apply(target, thisArg, args: Parameters<typeof db.prepare>) { |
78 | | - const [query] = args; |
79 | | - |
80 | | - return instrumentPreparedStatement(target.apply(thisArg, args), query, metadata); |
81 | | - }, |
82 | | - }); |
83 | | - |
84 | | - // Sadly the `.sql` template tag doesn't call `db.prepare` internally and it calls the connector's `.prepare` directly |
85 | | - // So we have to patch it manually, and would mean we would have less info in the spans. |
86 | | - // https://github.com/unjs/db0/blob/main/src/database.ts#L64 |
87 | | - db.sql = new Proxy(db.sql, { |
88 | | - apply(target, thisArg, args: Parameters<typeof db.sql>) { |
89 | | - const query = args[0]?.[0] ?? ''; |
90 | | - const opts = createStartSpanOptions(query, metadata); |
91 | | - |
92 | | - return startSpan( |
93 | | - opts, |
94 | | - handleSpanStart(() => target.apply(thisArg, args)), |
95 | | - ); |
96 | | - }, |
97 | | - }); |
98 | | - |
99 | | - db.exec = new Proxy(db.exec, { |
100 | | - apply(target, thisArg, args: Parameters<typeof db.exec>) { |
101 | | - return startSpan( |
102 | | - createStartSpanOptions(args[0], metadata), |
103 | | - handleSpanStart(() => target.apply(thisArg, args), { query: args[0] }), |
104 | | - ); |
105 | | - }, |
106 | | - }); |
107 | | - |
108 | | - db.__sentry_instrumented__ = true; |
109 | | -} |
110 | | - |
111 | | -/** |
112 | | - * Instruments a DB prepared statement with Sentry. |
113 | | - * |
114 | | - * This is meant to be used as a top-level call, under the hood it calls `instrumentPreparedStatementQueries` |
115 | | - * to patch the query methods. The reason for this abstraction is to ensure that the `bind` method is also patched. |
116 | | - */ |
117 | | -function instrumentPreparedStatement( |
118 | | - statement: PreparedStatement, |
119 | | - query: string, |
120 | | - data: DatabaseSpanData, |
121 | | -): PreparedStatement { |
122 | | - // statement.bind() returns a new instance of D1PreparedStatement, so we have to patch it as well. |
123 | | - // eslint-disable-next-line @typescript-eslint/unbound-method |
124 | | - statement.bind = new Proxy(statement.bind, { |
125 | | - apply(target, thisArg, args: Parameters<typeof statement.bind>) { |
126 | | - return instrumentPreparedStatementQueries(target.apply(thisArg, args), query, data); |
127 | | - }, |
128 | | - }); |
129 | | - |
130 | | - return instrumentPreparedStatementQueries(statement, query, data); |
131 | | -} |
132 | | - |
133 | | -/** |
134 | | - * Patches the query methods of a DB prepared statement with Sentry. |
135 | | - */ |
136 | | -function instrumentPreparedStatementQueries( |
137 | | - statement: PreparedStatement, |
138 | | - query: string, |
139 | | - data: DatabaseSpanData, |
140 | | -): PreparedStatement { |
141 | | - if (patchedStatement.has(statement)) { |
142 | | - return statement; |
143 | | - } |
144 | | - |
145 | | - // eslint-disable-next-line @typescript-eslint/unbound-method |
146 | | - statement.get = new Proxy(statement.get, { |
147 | | - apply(target, thisArg, args: Parameters<typeof statement.get>) { |
148 | | - return startSpan( |
149 | | - createStartSpanOptions(query, data), |
150 | | - handleSpanStart(() => target.apply(thisArg, args), { query }), |
151 | | - ); |
152 | | - }, |
153 | | - }); |
154 | | - |
155 | | - // eslint-disable-next-line @typescript-eslint/unbound-method |
156 | | - statement.run = new Proxy(statement.run, { |
157 | | - apply(target, thisArg, args: Parameters<typeof statement.run>) { |
158 | | - return startSpan( |
159 | | - createStartSpanOptions(query, data), |
160 | | - handleSpanStart(() => target.apply(thisArg, args), { query }), |
161 | | - ); |
162 | | - }, |
163 | | - }); |
164 | | - |
165 | | - // eslint-disable-next-line @typescript-eslint/unbound-method |
166 | | - statement.all = new Proxy(statement.all, { |
167 | | - apply(target, thisArg, args: Parameters<typeof statement.all>) { |
168 | | - return startSpan( |
169 | | - createStartSpanOptions(query, data), |
170 | | - handleSpanStart(() => target.apply(thisArg, args), { query }), |
171 | | - ); |
172 | | - }, |
173 | | - }); |
174 | | - |
175 | | - patchedStatement.add(statement); |
176 | | - |
177 | | - return statement; |
178 | | -} |
179 | | - |
180 | | -/** |
181 | | - * Creates a span start callback handler |
182 | | - */ |
183 | | -function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string }) { |
184 | | - return async (span: Span) => { |
185 | | - try { |
186 | | - const result = await fn(); |
187 | | - if (breadcrumbOpts) { |
188 | | - createBreadcrumb(breadcrumbOpts.query); |
189 | | - } |
190 | | - |
191 | | - return result; |
192 | | - } catch (error) { |
193 | | - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); |
194 | | - captureException(error, { |
195 | | - mechanism: { |
196 | | - handled: false, |
197 | | - type: SENTRY_ORIGIN, |
198 | | - }, |
199 | | - }); |
200 | | - |
201 | | - // Re-throw the error to be handled by the caller |
202 | | - throw error; |
203 | | - } finally { |
204 | | - await flushIfServerless(); |
205 | | - } |
206 | | - }; |
207 | | -} |
208 | | - |
209 | | -function createBreadcrumb(query: string): void { |
210 | | - addBreadcrumb({ |
211 | | - category: 'query', |
212 | | - message: query, |
213 | | - data: { |
214 | | - 'db.query.text': query, |
215 | | - }, |
216 | | - }); |
217 | | -} |
218 | | - |
219 | | -/** |
220 | | - * Creates a start span options object. |
221 | | - */ |
222 | | -function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpanOptions { |
223 | | - return { |
224 | | - name: query, |
225 | | - attributes: { |
226 | | - 'db.query.text': query, |
227 | | - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, |
228 | | - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query', |
229 | | - ...data, |
230 | | - }, |
231 | | - }; |
232 | | -} |
0 commit comments