Skip to content

Commit 4fbfe16

Browse files
committed
fix: instrument all execution methods
1 parent 041a882 commit 4fbfe16

File tree

2 files changed

+149
-44
lines changed

2 files changed

+149
-44
lines changed
Lines changed: 148 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import {
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
1413
import { 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

2935
function 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
}

packages/nuxt/src/vite/databaseConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Nuxt } from 'nuxt/schema';
66
* Sets up the database instrumentation.
77
*/
88
export function addDatabaseInstrumentation(nuxt: Nuxt): void {
9-
if (!nuxt.options.nitro?.database) {
9+
if (!nuxt.options.nitro?.experimental?.database && !nuxt.options.nitro?.database) {
1010
consoleSandbox(() => {
1111
// eslint-disable-next-line no-console
1212
console.log('[Sentry] No database configuration found. Skipping database instrumentation.');

0 commit comments

Comments
 (0)