Skip to content

Commit 3cb64d4

Browse files
committed
feat: add bun sqlite auto instrumentation
1 parent a788685 commit 3cb64d4

File tree

4 files changed

+565
-0
lines changed

4 files changed

+565
-0
lines changed

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,5 @@ export type { BunOptions } from './types';
154154
export { BunClient } from './client';
155155
export { getDefaultIntegrations, init } from './sdk';
156156
export { bunServerIntegration } from './integrations/bunserver';
157+
export { bunSqliteIntegration } from './integrations/bunsqlite';
157158
export { makeFetchTransport } from './transports';
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import type { IntegrationFn } from '@sentry/core';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, captureException, defineIntegration, startSpan } from '@sentry/core';
3+
4+
const INTEGRATION_NAME = 'BunSqlite';
5+
6+
const _bunSqliteIntegration = (() => {
7+
return {
8+
name: INTEGRATION_NAME,
9+
setupOnce() {
10+
instrumentBunSqlite();
11+
},
12+
};
13+
}) satisfies IntegrationFn;
14+
15+
/**
16+
* Instruments `bun:sqlite` to automatically create spans and capture errors.
17+
*
18+
* Enabled by default in the Bun SDK.
19+
*
20+
* ```js
21+
* Sentry.init({
22+
* integrations: [
23+
* Sentry.bunSqliteIntegration(),
24+
* ],
25+
* })
26+
* ```
27+
*/
28+
export const bunSqliteIntegration = defineIntegration(_bunSqliteIntegration);
29+
30+
let hasPatchedBunSqlite = false;
31+
32+
export function _resetBunSqliteInstrumentation(): void {
33+
hasPatchedBunSqlite = false;
34+
}
35+
36+
/**
37+
* Instruments bun:sqlite by patching the Database class.
38+
*/
39+
function instrumentBunSqlite(): void {
40+
if (hasPatchedBunSqlite) {
41+
return;
42+
}
43+
44+
try {
45+
const sqliteModule = require('bun:sqlite');
46+
47+
if (!sqliteModule || !sqliteModule.Database) {
48+
return;
49+
}
50+
51+
const OriginalDatabase = sqliteModule.Database;
52+
53+
const DatabaseProxy = new Proxy(OriginalDatabase, {
54+
construct(target, args) {
55+
const instance = new target(...args);
56+
if (args[0]) {
57+
Object.defineProperty(instance, '_sentryDbName', {
58+
value: args[0],
59+
writable: false,
60+
enumerable: false,
61+
configurable: false,
62+
});
63+
}
64+
return instance;
65+
},
66+
});
67+
68+
for (const prop in OriginalDatabase) {
69+
if (OriginalDatabase.hasOwnProperty(prop)) {
70+
DatabaseProxy[prop] = OriginalDatabase[prop];
71+
}
72+
}
73+
74+
sqliteModule.Database = DatabaseProxy;
75+
76+
OriginalDatabase.prototype.constructor = DatabaseProxy;
77+
78+
const proto = OriginalDatabase.prototype;
79+
const methodsToInstrument = ['query', 'prepare', 'run', 'exec', 'transaction'];
80+
81+
const inParentSpanMap = new WeakMap<any, boolean>();
82+
const dbNameMap = new WeakMap<any, string>();
83+
84+
methodsToInstrument.forEach(method => {
85+
if (proto[method]) {
86+
const originalMethod = proto[method];
87+
88+
if (originalMethod._sentryInstrumented) {
89+
return;
90+
}
91+
92+
proto[method] = function (this: any, ...args: any[]) {
93+
let dbName = this._sentryDbName || dbNameMap.get(this);
94+
95+
if (!dbName && this.filename) {
96+
dbName = this.filename;
97+
dbNameMap.set(this, dbName);
98+
}
99+
100+
const sql = method !== 'transaction' && args[0] && typeof args[0] === 'string' ? args[0] : undefined;
101+
102+
if (inParentSpanMap.get(this) && method === 'prepare') {
103+
const result = originalMethod.apply(this, args);
104+
if (result) {
105+
return instrumentStatement(result, sql, dbName);
106+
}
107+
return result;
108+
}
109+
110+
return startSpan(
111+
{
112+
name: sql || 'db.sql.' + method,
113+
op: `db.sql.${method}`,
114+
attributes: {
115+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.bun.sqlite',
116+
'db.system': 'sqlite',
117+
'db.operation': method,
118+
...(sql && { 'db.statement': sql }),
119+
...(dbName && { 'db.name': dbName }),
120+
},
121+
},
122+
span => {
123+
try {
124+
const wasInParentSpan = inParentSpanMap.get(this) || false;
125+
if (method === 'query') {
126+
inParentSpanMap.set(this, true);
127+
}
128+
129+
const result = originalMethod.apply(this, args);
130+
131+
if (wasInParentSpan) {
132+
inParentSpanMap.set(this, wasInParentSpan);
133+
} else {
134+
inParentSpanMap.delete(this);
135+
}
136+
137+
if (method === 'prepare' && result) {
138+
return instrumentStatement(result, sql, dbName);
139+
}
140+
141+
return result;
142+
} catch (error) {
143+
span.setStatus({ code: 2, message: 'internal_error' });
144+
captureException(error, {
145+
mechanism: {
146+
type: 'bun.sqlite',
147+
handled: false,
148+
data: {
149+
function: method,
150+
},
151+
},
152+
});
153+
throw error;
154+
}
155+
},
156+
);
157+
};
158+
159+
// Mark the instrumented method
160+
proto[method]._sentryInstrumented = true;
161+
}
162+
});
163+
164+
hasPatchedBunSqlite = true;
165+
} catch (error) {
166+
// Silently fail if bun:sqlite is not available
167+
}
168+
}
169+
170+
/**
171+
* Instruments a Statement instance.
172+
*/
173+
function instrumentStatement(statement: any, sql?: string, dbName?: string): any {
174+
const methodsToInstrument = ['run', 'get', 'all', 'values'];
175+
176+
methodsToInstrument.forEach(method => {
177+
if (typeof statement[method] === 'function') {
178+
statement[method] = new Proxy(statement[method], {
179+
apply(target, thisArg, args) {
180+
return startSpan(
181+
{
182+
name: `db.statement.${method}`,
183+
op: `db.sql.statement.${method}`,
184+
attributes: {
185+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.bun.sqlite',
186+
'db.system': 'sqlite',
187+
'db.operation': method,
188+
...(sql && { 'db.statement': sql }),
189+
...(dbName && { 'db.name': dbName }),
190+
},
191+
},
192+
span => {
193+
try {
194+
return target.apply(thisArg, args);
195+
} catch (error) {
196+
span.setStatus({ code: 2, message: 'internal_error' });
197+
captureException(error, {
198+
mechanism: {
199+
type: 'bun.sqlite.statement',
200+
handled: false,
201+
data: {
202+
function: method,
203+
},
204+
},
205+
});
206+
throw error;
207+
}
208+
},
209+
);
210+
},
211+
});
212+
}
213+
});
214+
215+
return statement;
216+
}

packages/bun/src/sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
onUnhandledRejectionIntegration,
2323
} from '@sentry/node';
2424
import { bunServerIntegration } from './integrations/bunserver';
25+
import { bunSqliteIntegration } from './integrations/bunsqlite';
2526
import { makeFetchTransport } from './transports';
2627
import type { BunOptions } from './types';
2728

@@ -49,6 +50,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
4950
modulesIntegration(),
5051
// Bun Specific
5152
bunServerIntegration(),
53+
bunSqliteIntegration(),
5254
...(hasSpansEnabled(_options) ? getAutoPerformanceIntegrations() : []),
5355
];
5456
}

0 commit comments

Comments
 (0)