Open
Show file tree
Hide file tree
Changes from all commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Failed to load files.
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,4 +154,5 @@ export type { BunOptions } from './types';
export { BunClient } from './client';
export { getDefaultIntegrations, init } from './sdk';
export { bunServerIntegration } from './integrations/bunserver';
export { bunSqliteIntegration } from './integrations/bunsqlite';
export { makeFetchTransport } from './transports';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import type { IntegrationFn } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, captureException, defineIntegration, startSpan } from '@sentry/core';

const INTEGRATION_NAME = 'BunSqlite';

const _bunSqliteIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentBunSqlite();
},
};
}) satisfies IntegrationFn;

/**
* Instruments `bun:sqlite` to automatically create spans and capture errors.
*
* Enabled by default in the Bun SDK.
*
* ```js
* Sentry.init({
* integrations: [
* Sentry.bunSqliteIntegration(),
* ],
* })
* ```
*/
export const bunSqliteIntegration = defineIntegration(_bunSqliteIntegration);

let hasedBunSqlite = false;

export function _resetBunSqliteInstrumentation(): void {
hasedBunSqlite = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this functionality, it doesn't actually reset the proxy implementation, so calling this will lead to the sql module getting ed multiple times.

}

/**
* Instruments bun:sqlite by ing the Database class.
*/
function instrumentBunSqlite(): void {
if (hasedBunSqlite) {
return;
}

try {
const sqliteModule = require('bun:sqlite');

if (!sqliteModule || !sqliteModule.Database) {
return;
}

const OriginalDatabase = sqliteModule.Database;

const DatabaseProxy = new Proxy(OriginalDatabase, {
construct(target, args) {
const instance = new target(...args);
if (args[0]) {
Object.defineProperty(instance, '_sentryDbName', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this being set?

It only seems to be defined, which would mean let dbName = this._sentryDbName || dbNameMap.get(this); would always evaluate dbNameMap.get(this)

value: args[0],
writable: false,
enumerable: false,
configurable: false,
});
}
return instance;
},
});

for (const prop in OriginalDatabase) {
if (OriginalDatabase.hasOwnProperty(prop)) {
DatabaseProxy[prop] = OriginalDatabase[prop];
}
}

sqliteModule.Database = DatabaseProxy;

OriginalDatabase..constructor = DatabaseProxy;

const proto = OriginalDatabase.;
const methodsToInstrument = ['query', 'prepare', 'run', 'exec', 'transaction'];

const inParentSpanMap = new WeakMap<any, boolean>();
const dbNameMap = new WeakMap<any, string>();
Comment on lines +81 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do these maps do? Can we type them more stronger? Why are they WeakMap?


methodsToInstrument.forEach(method => {
if (proto[method]) {
const originalMethod = proto[method];

if (originalMethod._sentryInstrumented) {
return;
}

proto[method] = function (this: any, ...args: any[]) {
let dbName = this._sentryDbName || dbNameMap.get(this);

if (!dbName && this.filename) {
dbName = this.filename;
dbNameMap.set(this, dbName);
}
Comment on lines +93 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this logic?


const sql = method !== 'transaction' && args[0] && typeof args[0] === 'string' ? args[0] : undefined;

if (inParentSpanMap.get(this) && method === 'prepare') {
const result = originalMethod.apply(this, args);
if (result) {
return instrumentStatement(result, sql, dbName);
}
return result;
}

return startSpan(
{
name: sql || 'db.sql.' + method,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what cases is sql not defined? We should always try to make the span name the db statement.

op: `db.sql.${method}`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.bun.sqlite',
'db.system': 'sqlite',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'db.system': 'sqlite',
'db.system.name': 'sqlite',

https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/#db-system

'db.operation': method,
...(sql && { 'db.statement': sql }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...(sql && { 'db.statement': sql }),
...(sql && { 'db.query.text': sql }),

https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/#db-statement

...(dbName && { 'db.name': dbName }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...(dbName && { 'db.name': dbName }),
...(dbName && { 'db.namespace': dbName }),

https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/#db-name

},
},
span => {
try {
const wasInParentSpan = inParentSpanMap.get(this) || false;
if (method === 'query') {
inParentSpanMap.set(this, true);
}

const result = originalMethod.apply(this, args);

if (wasInParentSpan) {
inParentSpanMap.set(this, wasInParentSpan);
} else {
inParentSpanMap.delete(this);
}

if (method === 'prepare' && result) {
return instrumentStatement(result, sql, dbName);
}

return result;
} catch (error) {
span.setStatus({ code: 2, message: 'internal_error' });
captureException(error, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to capture an error here, we can just let the error bubble up.

mechanism: {
type: 'bun.sqlite',
handled: false,
data: {
function: method,
},
},
});
throw error;
}
},
);
};

// Mark the instrumented method
proto[method]._sentryInstrumented = true;
}
});

hasedBunSqlite = true;
} catch (error) {
// Silently fail if bun:sqlite is not available
}
}

/**
* Instruments a Statement instance.
*/
function instrumentStatement(statement: any, sql?: string, dbName?: string): any {
const methodsToInstrument = ['run', 'get', 'all', 'values'];

methodsToInstrument.forEach(method => {
if (typeof statement[method] === 'function') {
statement[method] = new Proxy(statement[method], {
apply(target, thisArg, args) {
return startSpan(
{
name: `db.statement.${method}`,
op: `db.sql.statement.${method}`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.bun.sqlite',
'db.system': 'sqlite',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'db.system': 'sqlite',
'db.system.name': 'sqlite',

https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/#db-system

'db.operation': method,
...(sql && { 'db.statement': sql }),
...(dbName && { 'db.name': dbName }),
},
},
span => {
try {
return target.apply(thisArg, args);
} catch (error) {
span.setStatus({ code: 2, message: 'internal_error' });
captureException(error, {
mechanism: {
type: 'bun.sqlite.statement',
handled: false,
data: {
function: method,
},
},
});
throw error;
}
},
);
},
});
}
});

return statement;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ import {
onUnhandledRejectionIntegration,
} from '@sentry/node';
import { bunServerIntegration } from './integrations/bunserver';
import { bunSqliteIntegration } from './integrations/bunsqlite';
import { makeFetchTransport } from './transports';
import type { BunOptions } from './types';

Expand DownExpand Up@@ -49,6 +50,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
modulesIntegration(),
// Bun Specific
bunServerIntegration(),
bunSqliteIntegration(),
...(hasSpansEnabled(_options) ? getAutoPerformanceIntegrations() : []),
];
}
Expand Down
Loading