Skip to content

Commit f94b203

Browse files
authored
feat(nuxt): Instrument Database (#17899)
This pull request introduces automatic instrumentation for database queries in Nuxt applications in server side handlers using Sentry. #### Implementation Details - Instruments database `.sql`, `.prepare` and `.exec` calls. - Adds breadcrumbs and spans following cloudflare's D1 implementation. This relies on the work done in #17858 and #17886
1 parent 2e652f3 commit f94b203

File tree

17 files changed

+1605
-3
lines changed

17 files changed

+1605
-3
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ export default defineNuxtConfig({
1212
},
1313
},
1414
nitro: {
15+
experimental: {
16+
database: true,
17+
},
18+
database: {
19+
default: {
20+
connector: 'sqlite',
21+
options: { name: 'db' },
22+
},
23+
users: {
24+
connector: 'sqlite',
25+
options: { name: 'users_db' },
26+
},
27+
analytics: {
28+
connector: 'sqlite',
29+
options: { name: 'analytics_db' },
30+
},
31+
},
1532
storage: {
1633
'test-storage': {
1734
driver: 'memory',

dev-packages/e2e-tests/test-applications/nuxt-3/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
]
3434
},
3535
"volta": {
36-
"extends": "../../package.json"
36+
"extends": "../../package.json",
37+
"node": "22.20.0"
3738
}
3839
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { defineEventHandler, getQuery, useDatabase } from '#imports';
2+
3+
export default defineEventHandler(async event => {
4+
const query = getQuery(event);
5+
const method = query.method as string;
6+
7+
switch (method) {
8+
case 'default-db': {
9+
// Test default database instance
10+
const db = useDatabase();
11+
await db.exec('CREATE TABLE IF NOT EXISTS default_table (id INTEGER PRIMARY KEY, data TEXT)');
12+
await db.exec(`INSERT OR REPLACE INTO default_table (id, data) VALUES (1, 'default data')`);
13+
const stmt = db.prepare('SELECT * FROM default_table WHERE id = ?');
14+
const result = await stmt.get(1);
15+
return { success: true, database: 'default', result };
16+
}
17+
18+
case 'users-db': {
19+
// Test named database instance 'users'
20+
const usersDb = useDatabase('users');
21+
await usersDb.exec(
22+
'CREATE TABLE IF NOT EXISTS user_profiles (id INTEGER PRIMARY KEY, username TEXT, email TEXT)',
23+
);
24+
await usersDb.exec(
25+
`INSERT OR REPLACE INTO user_profiles (id, username, email) VALUES (1, 'john_doe', '[email protected]')`,
26+
);
27+
const stmt = usersDb.prepare('SELECT * FROM user_profiles WHERE id = ?');
28+
const result = await stmt.get(1);
29+
return { success: true, database: 'users', result };
30+
}
31+
32+
case 'analytics-db': {
33+
// Test named database instance 'analytics'
34+
const analyticsDb = useDatabase('analytics');
35+
await analyticsDb.exec(
36+
'CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, event_name TEXT, count INTEGER)',
37+
);
38+
await analyticsDb.exec(`INSERT OR REPLACE INTO events (id, event_name, count) VALUES (1, 'page_view', 100)`);
39+
const stmt = analyticsDb.prepare('SELECT * FROM events WHERE id = ?');
40+
const result = await stmt.get(1);
41+
return { success: true, database: 'analytics', result };
42+
}
43+
44+
case 'multiple-dbs': {
45+
// Test operations across multiple databases in a single request
46+
const defaultDb = useDatabase();
47+
const usersDb = useDatabase('users');
48+
const analyticsDb = useDatabase('analytics');
49+
50+
// Create tables and insert data in all databases
51+
await defaultDb.exec('CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY, token TEXT)');
52+
await defaultDb.exec(`INSERT OR REPLACE INTO sessions (id, token) VALUES (1, 'session-token-123')`);
53+
54+
await usersDb.exec('CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, account_name TEXT)');
55+
await usersDb.exec(`INSERT OR REPLACE INTO accounts (id, account_name) VALUES (1, 'Premium Account')`);
56+
57+
await analyticsDb.exec(
58+
'CREATE TABLE IF NOT EXISTS metrics (id INTEGER PRIMARY KEY, metric_name TEXT, value REAL)',
59+
);
60+
await analyticsDb.exec(
61+
`INSERT OR REPLACE INTO metrics (id, metric_name, value) VALUES (1, 'conversion_rate', 0.25)`,
62+
);
63+
64+
// Query from all databases
65+
const sessionResult = await defaultDb.prepare('SELECT * FROM sessions WHERE id = ?').get(1);
66+
const accountResult = await usersDb.prepare('SELECT * FROM accounts WHERE id = ?').get(1);
67+
const metricResult = await analyticsDb.prepare('SELECT * FROM metrics WHERE id = ?').get(1);
68+
69+
return {
70+
success: true,
71+
results: {
72+
default: sessionResult,
73+
users: accountResult,
74+
analytics: metricResult,
75+
},
76+
};
77+
}
78+
79+
case 'sql-template-multi': {
80+
// Test SQL template tag across multiple databases
81+
const defaultDb = useDatabase();
82+
const usersDb = useDatabase('users');
83+
84+
await defaultDb.exec('CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY, message TEXT)');
85+
await usersDb.exec('CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY, action TEXT)');
86+
87+
const defaultResult = await defaultDb.sql`INSERT INTO logs (message) VALUES (${'test message'})`;
88+
const usersResult = await usersDb.sql`INSERT INTO audit_logs (action) VALUES (${'user_login'})`;
89+
90+
return {
91+
success: true,
92+
results: {
93+
default: defaultResult,
94+
users: usersResult,
95+
},
96+
};
97+
}
98+
99+
default:
100+
return { error: 'Unknown method' };
101+
}
102+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { defineEventHandler, getQuery, useDatabase } from '#imports';
2+
3+
export default defineEventHandler(async event => {
4+
const db = useDatabase();
5+
const query = getQuery(event);
6+
const method = query.method as string;
7+
8+
switch (method) {
9+
case 'prepare-get': {
10+
await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
11+
await db.exec(`INSERT OR REPLACE INTO users (id, name, email) VALUES (1, 'Test User', '[email protected]')`);
12+
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
13+
const result = await stmt.get(1);
14+
return { success: true, result };
15+
}
16+
17+
case 'prepare-all': {
18+
await db.exec('CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT, price REAL)');
19+
await db.exec(`INSERT OR REPLACE INTO products (id, name, price) VALUES
20+
(1, 'Product A', 10.99),
21+
(2, 'Product B', 20.50),
22+
(3, 'Product C', 15.25)`);
23+
const stmt = db.prepare('SELECT * FROM products WHERE price > ?');
24+
const results = await stmt.all(10);
25+
return { success: true, count: results.length, results };
26+
}
27+
28+
case 'prepare-run': {
29+
await db.exec('CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, customer TEXT, amount REAL)');
30+
const stmt = db.prepare('INSERT INTO orders (customer, amount) VALUES (?, ?)');
31+
const result = await stmt.run('John Doe', 99.99);
32+
return { success: true, result };
33+
}
34+
35+
case 'prepare-bind': {
36+
await db.exec('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, category TEXT, value INTEGER)');
37+
await db.exec(`INSERT OR REPLACE INTO items (id, category, value) VALUES
38+
(1, 'electronics', 100),
39+
(2, 'books', 50),
40+
(3, 'electronics', 200)`);
41+
const stmt = db.prepare('SELECT * FROM items WHERE category = ?');
42+
const boundStmt = stmt.bind('electronics');
43+
const results = await boundStmt.all();
44+
return { success: true, count: results.length, results };
45+
}
46+
47+
case 'sql': {
48+
await db.exec('CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, content TEXT, created_at TEXT)');
49+
const timestamp = new Date().toISOString();
50+
const results = await db.sql`INSERT INTO messages (content, created_at) VALUES (${'Hello World'}, ${timestamp})`;
51+
return { success: true, results };
52+
}
53+
54+
case 'exec': {
55+
await db.exec('DROP TABLE IF EXISTS logs');
56+
await db.exec('CREATE TABLE logs (id INTEGER PRIMARY KEY, message TEXT, level TEXT)');
57+
const result = await db.exec(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`);
58+
return { success: true, result };
59+
}
60+
61+
case 'error': {
62+
const stmt = db.prepare('SELECT * FROM nonexistent_table WHERE invalid_column = ?');
63+
await stmt.get(1);
64+
return { success: false, message: 'Should have thrown an error' };
65+
}
66+
67+
default:
68+
return { error: 'Unknown method' };
69+
}
70+
});
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test.describe('multiple database instances', () => {
5+
test('instruments default database instance', async ({ request }) => {
6+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
7+
return transactionEvent.transaction === 'GET /api/db-multi-test';
8+
});
9+
10+
await request.get('/api/db-multi-test?method=default-db');
11+
12+
const transaction = await transactionPromise;
13+
14+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
15+
16+
expect(dbSpans).toBeDefined();
17+
expect(dbSpans!.length).toBeGreaterThan(0);
18+
19+
// Check that we have the SELECT span
20+
const selectSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM default_table'));
21+
expect(selectSpan).toBeDefined();
22+
expect(selectSpan?.op).toBe('db.query');
23+
expect(selectSpan?.data?.['db.system.name']).toBe('sqlite');
24+
expect(selectSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
25+
});
26+
27+
test('instruments named database instance (users)', async ({ request }) => {
28+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
29+
return transactionEvent.transaction === 'GET /api/db-multi-test';
30+
});
31+
32+
await request.get('/api/db-multi-test?method=users-db');
33+
34+
const transaction = await transactionPromise;
35+
36+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
37+
38+
expect(dbSpans).toBeDefined();
39+
expect(dbSpans!.length).toBeGreaterThan(0);
40+
41+
// Check that we have the SELECT span from users database
42+
const selectSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM user_profiles'));
43+
expect(selectSpan).toBeDefined();
44+
expect(selectSpan?.op).toBe('db.query');
45+
expect(selectSpan?.data?.['db.system.name']).toBe('sqlite');
46+
expect(selectSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
47+
});
48+
49+
test('instruments named database instance (analytics)', async ({ request }) => {
50+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
51+
return transactionEvent.transaction === 'GET /api/db-multi-test';
52+
});
53+
54+
await request.get('/api/db-multi-test?method=analytics-db');
55+
56+
const transaction = await transactionPromise;
57+
58+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
59+
60+
expect(dbSpans).toBeDefined();
61+
expect(dbSpans!.length).toBeGreaterThan(0);
62+
63+
// Check that we have the SELECT span from analytics database
64+
const selectSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM events'));
65+
expect(selectSpan).toBeDefined();
66+
expect(selectSpan?.op).toBe('db.query');
67+
expect(selectSpan?.data?.['db.system.name']).toBe('sqlite');
68+
expect(selectSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
69+
});
70+
71+
test('instruments multiple database instances in single request', async ({ request }) => {
72+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
73+
return transactionEvent.transaction === 'GET /api/db-multi-test';
74+
});
75+
76+
await request.get('/api/db-multi-test?method=multiple-dbs');
77+
78+
const transaction = await transactionPromise;
79+
80+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
81+
82+
expect(dbSpans).toBeDefined();
83+
expect(dbSpans!.length).toBeGreaterThan(0);
84+
85+
// Check that we have spans from all three databases
86+
const sessionSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM sessions'));
87+
const accountSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM accounts'));
88+
const metricSpan = dbSpans?.find(span => span.description?.includes('SELECT * FROM metrics'));
89+
90+
expect(sessionSpan).toBeDefined();
91+
expect(sessionSpan?.op).toBe('db.query');
92+
expect(sessionSpan?.data?.['db.system.name']).toBe('sqlite');
93+
94+
expect(accountSpan).toBeDefined();
95+
expect(accountSpan?.op).toBe('db.query');
96+
expect(accountSpan?.data?.['db.system.name']).toBe('sqlite');
97+
98+
expect(metricSpan).toBeDefined();
99+
expect(metricSpan?.op).toBe('db.query');
100+
expect(metricSpan?.data?.['db.system.name']).toBe('sqlite');
101+
102+
// All should have the same origin
103+
expect(sessionSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
104+
expect(accountSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
105+
expect(metricSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
106+
});
107+
108+
test('instruments SQL template tag across multiple databases', async ({ request }) => {
109+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
110+
return transactionEvent.transaction === 'GET /api/db-multi-test';
111+
});
112+
113+
await request.get('/api/db-multi-test?method=sql-template-multi');
114+
115+
const transaction = await transactionPromise;
116+
117+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
118+
119+
expect(dbSpans).toBeDefined();
120+
expect(dbSpans!.length).toBeGreaterThan(0);
121+
122+
// Check that we have INSERT spans from both databases
123+
const logsInsertSpan = dbSpans?.find(span => span.description?.includes('INSERT INTO logs'));
124+
const auditLogsInsertSpan = dbSpans?.find(span => span.description?.includes('INSERT INTO audit_logs'));
125+
126+
expect(logsInsertSpan).toBeDefined();
127+
expect(logsInsertSpan?.op).toBe('db.query');
128+
expect(logsInsertSpan?.data?.['db.system.name']).toBe('sqlite');
129+
expect(logsInsertSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
130+
131+
expect(auditLogsInsertSpan).toBeDefined();
132+
expect(auditLogsInsertSpan?.op).toBe('db.query');
133+
expect(auditLogsInsertSpan?.data?.['db.system.name']).toBe('sqlite');
134+
expect(auditLogsInsertSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt');
135+
});
136+
137+
test('creates correct span count for multiple database operations', async ({ request }) => {
138+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
139+
return transactionEvent.transaction === 'GET /api/db-multi-test';
140+
});
141+
142+
await request.get('/api/db-multi-test?method=multiple-dbs');
143+
144+
const transaction = await transactionPromise;
145+
146+
const dbSpans = transaction.spans?.filter(span => span.op === 'db.query');
147+
148+
// We should have multiple spans:
149+
// - 3 CREATE TABLE (exec) spans
150+
// - 3 INSERT (exec) spans
151+
// - 3 SELECT (prepare + get) spans
152+
// Total should be at least 9 spans
153+
expect(dbSpans).toBeDefined();
154+
expect(dbSpans!.length).toBeGreaterThanOrEqual(9);
155+
});
156+
});

0 commit comments

Comments
 (0)