Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.knexIntegration()],
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,41 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di
await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => {
test('should name spans after the query summary with span streaming', { timeout: 60_000 }, async () => {
await createRunner()
.expect({
span: container => {
// The `mysql2` driver spans underneath the knex spans come from a different integration and
// are named after the full statement, so they are filtered out here.
const knexSpans = container.items.filter(item => item.attributes['sentry.origin']?.value === ORIGIN);

expect(
knexSpans.map(span => ({
name: span.name,
summary: span.attributes['db.query.summary']?.value,
text: span.attributes['db.query.text']?.value,
})),
).toEqual([
{
name: 'create table `User`',
summary: 'create table `User`',
text: 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(3) not null default CURRENT_TIMESTAMP(3), `email` text not null, `name` text not null)',
},
{
name: 'insert `User`',
summary: 'insert `User`',
text: 'insert into `User` (`email`, `name`) values (?, ?)',
},
{ name: 'select `User`', summary: 'select `User`', text: 'select * from `User`' },
{ name: 'drop table `User`', summary: 'drop table `User`', text: 'drop table `User`' },
]);
},
})
.start()
.completed();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.knexIntegration()],
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,46 @@ describe('knex auto instrumentation', () => {
await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => {
test('should name spans after the query summary with span streaming', { timeout: 60_000 }, async () => {
await createRunner()
.expect({
span: container => {
// The `pg` driver spans underneath the knex spans come from a different integration and
// are named after the full statement, so they are filtered out here.
const knexSpans = container.items.filter(item => item.attributes['sentry.origin']?.value === ORIGIN);

expect(
Comment thread
Lms24 marked this conversation as resolved.
knexSpans.map(span => ({
name: span.name,
summary: span.attributes['db.query.summary']?.value,
text: span.attributes['db.query.text']?.value,
})),
).toEqual([
{
name: 'create table "User"',
summary: 'create table "User"',
text: 'create table "User" ("id" serial primary key, "createdAt" timestamptz(3) not null default CURRENT_TIMESTAMP(3), "email" text not null, "name" text not null)',
},
{
name: 'insert "User"',
summary: 'insert "User"',
text: 'insert into "User" ("email", "name") values (?, ?)',
},
{ name: 'select "User"', summary: 'select "User"', text: 'select * from "User"' },
{
name: 'select "DoesNotExist"',
summary: 'select "DoesNotExist"',
text: 'select * from "DoesNotExist"',
},
{ name: 'drop table "User"', summary: 'drop table "User"', text: 'drop table "User"' },
]);
},
})
.start()
.completed();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function expectPrismaV5Spans(transaction: TransactionEvent): void {
expect.objectContaining({
data: {
'db.statement': expect.stringContaining('INSERT INTO'),
'db.query.summary': 'INSERT "public"."User"',
'db.system': 'postgresql',
'sentry.kind': 'client',
'sentry.op': 'db',
Expand All @@ -84,6 +85,7 @@ function expectPrismaV5Spans(transaction: TransactionEvent): void {
expect.objectContaining({
data: {
'db.statement': expect.stringContaining('SELECT'),
'db.query.summary': 'SELECT "public"',
'db.system': 'postgresql',
'sentry.kind': 'client',
'sentry.op': 'db',
Expand All @@ -96,6 +98,7 @@ function expectPrismaV5Spans(transaction: TransactionEvent): void {
expect.objectContaining({
data: {
'db.statement': expect.stringContaining('DELETE'),
'db.query.summary': 'DELETE "public"."User"',
'db.system': 'postgresql',
'sentry.kind': 'client',
'sentry.op': 'db',
Expand Down Expand Up @@ -128,5 +131,54 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, ()
copyPaths: ['prisma'],
},
);

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-span-streaming.mjs',
(createRunner, test) => {
test(
'should name db query spans after the query summary with span streaming',
{ timeout: 75_000 },
async () => {
await createRunner()
// Prisma's engine startup can outlast the span buffer's flush interval, so the query spans
// are not guaranteed to be in the first span envelope.
.unordered()
.expect({
span: container => {
// v5 reports the SQL on the deprecated `db.statement` rather than `db.query.text`.
const querySpans = container.items.filter(item => item.attributes['db.statement']);

expect(
querySpans.map(span => ({
name: span.name,
summary: span.attributes['db.query.summary']?.value,
})),
).toEqual([
{ name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' },
{ name: 'SELECT "public"', summary: 'SELECT "public"' },
{ name: 'BEGIN', summary: 'BEGIN' },
{ name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' },
{ name: 'SELECT "public"', summary: 'SELECT "public"' },
{ name: 'COMMIT', summary: 'COMMIT' },
{ name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' },
]);

// The raw engine span name must never leak through.
expect(container.items.map(span => span.name)).not.toContain('prisma:engine:db_query');
},
})
.start()
.completed();
},
);
},
{
additionalDependencies: ADDITIONAL_DEPENDENCIES,
afterSetupCommand: AFTER_SETUP_COMMAND,
copyPaths: ['prisma'],
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname]
'sentry.op': 'db',
'db.query.text':
'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1',
'db.query.summary': 'SELECT "public"',
'db.system': 'postgresql',
'sentry.kind': 'client',
},
Expand All @@ -99,6 +100,7 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname]
data: {
'sentry.op': 'db',
'db.query.text': 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1',
'db.query.summary': 'DELETE "public"."User"',
'db.system': 'postgresql',
'sentry.kind': 'client',
},
Expand All @@ -119,4 +121,48 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname]
copyPaths: ['prisma'],
},
);

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-span-streaming.mjs',
(createRunner, test) => {
test('should name db query spans after the query summary with span streaming', { timeout: 75_000 }, async () => {
await createRunner()
// Prisma's engine startup can outlast the span buffer's flush interval, so the query spans
// are not guaranteed to be in the first span envelope.
.unordered()
.expect({
span: container => {
const querySpans = container.items.filter(item => item.attributes['db.query.text']);

// `SELECT "public"` is what the core query-summary helper derives from a schema-qualified,
// quoted table (it stops at the first quoted identifier).
expect(
querySpans.map(span => ({
name: span.name,
summary: span.attributes['db.query.summary']?.value,
})),
).toEqual([
{ name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' },
{ name: 'SELECT "public"', summary: 'SELECT "public"' },
{ name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' },
]);

// Neither the raw engine span name nor the full statement may end up as a span name.
expect(container.items.map(span => span.name)).not.toContain('prisma:engine:db_query');
querySpans.forEach(span => {
expect(span.name).not.toBe(span.attributes['db.query.text']?.value);
});
},
})
.start()
.completed();
});
},
{
afterSetupCommand: 'prisma generate --schema prisma/schema.prisma',
copyPaths: ['prisma'],
},
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,65 @@ describe('Prisma ORM v7 Tests', () => {
copyPaths: ['prisma', 'prisma.config.ts'],
},
);

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-span-streaming.mjs',
(createRunner, test) => {
test(
'should name db query spans after the query summary with span streaming',
{ timeout: 75_000 },
async () => {
await createRunner()
// Prisma's engine startup can outlast the span buffer's flush interval, so the query spans
// are not guaranteed to be in the first span envelope.
.unordered()
.expect({
span: container => {
// v7 runs the queries through the `pg` adapter, whose own spans are named after the full
// statement by a different integration, so they are filtered out here.
const querySpans = container.items.filter(
item =>
item.attributes['sentry.origin']?.value === 'auto.db.prisma' && item.attributes['db.query.text'],
);

// `SELECT "public"` is what the core query-summary helper derives from a schema-qualified,
// quoted table (it stops at the first quoted identifier).
expect(
querySpans.map(span => ({
name: span.name,
summary: span.attributes['db.query.summary']?.value,
})),
).toEqual([
{ name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' },
{ name: 'SELECT "public"', summary: 'SELECT "public"' },
{ name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' },
]);

// Neither the raw client span name nor the full statement may end up as a span name.
expect(container.items.map(span => span.name)).not.toContain('prisma:client:db_query');
querySpans.forEach(span => {
expect(span.name).not.toBe(span.attributes['db.query.text']?.value);
});
},
})
.start()
.completed();
},
);
},
{
additionalDependencies: {
'@prisma/adapter-pg': '7.2.0',
'@prisma/client': '7.2.0',
pg: '^8.11.0',
prisma: '7.2.0',
typescript: '^5.9.0',
},
afterSetupCommand: 'prisma generate --schema prisma/schema.prisma && tsc -p prisma/tsconfig.json',
copyPaths: ['prisma', 'prisma.config.ts'],
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ function query(connection, sql, method = 'execSql') {
});
}

function queryWithParameter(connection, sql, name, type, value) {
return new Promise((resolve, reject) => {
const request = new Request(sql, err => (err ? reject(err) : resolve()));
request.addParameter(name, type, value);
connection.execSql(request);
});
}

function callProcedure(connection) {
return new Promise((resolve, reject) => {
const request = new Request(PROCEDURE_NAME, err => (err ? reject(err) : resolve()));
Expand Down Expand Up @@ -118,6 +126,16 @@ async function run() {
`if object_id('[dbo].[${BULK_TABLE}]') is null CREATE TABLE [dbo].[${BULK_TABLE}] (c1 int, c2 varchar(30))`,
);
await bulkLoad(connection);

// Reads against real tables: the single- and multi-table shapes a query summary has to resolve.
await query(connection, `SELECT c1, c2 FROM ${PREPARED_TABLE}`);
await query(connection, `SELECT p.c1 FROM ${PREPARED_TABLE} p INNER JOIN [dbo].[${BULK_TABLE}] b ON p.c1 = b.c1`);

// An inlined literal, the same filter parameterized, and a string literal containing `from` — the
// last one is why the statement is sanitized before it is summarized.
await query(connection, `SELECT c1, c2 FROM ${PREPARED_TABLE} WHERE c1 = 42`);
await queryWithParameter(connection, `SELECT c1, c2 FROM ${PREPARED_TABLE} WHERE c1 = @c1`, 'c1', TYPES.Int, 1);
await query(connection, `SELECT c1, c2 FROM [dbo].[${BULK_TABLE}] WHERE c2 = 'hello from acme'`);
});

connection.close();
Expand Down
Loading
Loading