Skip to content

feat(node): Add postgresjs instrumentation #16665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"node-cron": "^3.0.3",
"node-schedule": "^2.1.1",
"pg": "8.16.0",
"postgres": "^3.4.7",
"proxy": "^2.1.1",
"redis-4": "npm:redis@^4.6.14",
"reflect-metadata": "0.2.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3.9'

services:
db:
image: postgres:13
restart: always
container_name: integration-tests-postgresjs
ports:
- '5444:5432'
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});

// Stop the process from exiting before the transaction is sent
setInterval(() => {}, 1000);

const postgres = require('postgres');

const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });

async function run() {
await Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
async () => {
try {
await sql`
CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));
`;

await sql`
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
`;

await sql`
UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com';

Choose a reason for hiding this comment

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

We'd perhaps want to leave the email in a constant since we are repeating it over and over.
it doesn't really matter since it's a dummy email, what I'm thinking is: what if at some point we want to run the test on a specific email? (if we ever want to do that)

It would be easier to just change the value of the constant over changing every single instance of it.

Just my thought 😁

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, updated. Thanks 👍

`;

await sql`
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
`;

await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => {
await Promise.all(rows);
});

await sql`
DROP TABLE "User";
`;

// This will be captured as an error as the table no longer exists
await sql`
SELECT * FROM "User" WHERE "email" = 'foo@baz.com';
`;
} finally {
await sql.end();
}
},
);
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
225 changes: 225 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { describe, expect, test } from 'vitest';
import { createRunner } from '../../../utils/runner';

const EXISTING_TEST_EMAIL = 'bar@baz.com';
const NON_EXISTING_TEST_EMAIL = 'foo@baz.com';

describe('postgresjs auto instrumentation', () => {
test('should auto-instrument `postgres` package', { timeout: 60_000 }, async () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'CREATE TABLE',
'db.query.text':
'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description:
'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'SELECT',
'db.query.text':
"select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description:
"select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'INSERT',
'db.query.text': `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
'sentry.origin': 'auto.db.otel.postgres',
'sentry.op': 'db',
'server.address': 'localhost',
'server.port': 5444,
}),
description: `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'UPDATE',
'db.query.text': `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description: `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'SELECT',
'db.query.text': `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description: `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'SELECT',
'db.query.text': 'SELECT * from generate_series(?,?) as x',
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description: 'SELECT * from generate_series(?,?) as x',
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
'db.operation.name': 'DROP TABLE',
'db.query.text': 'DROP TABLE "User"',
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description: 'DROP TABLE "User"',
op: 'db',
status: 'ok',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
expect.objectContaining({
data: expect.objectContaining({
'db.namespace': 'test_db',
'db.system.name': 'postgres',
// No db.operation.name here, as this is an errored span
'db.response.status_code': '42P01',
'error.type': 'PostgresError',
'db.query.text': `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
'sentry.op': 'db',
'sentry.origin': 'auto.db.otel.postgres',
'server.address': 'localhost',
'server.port': 5444,
}),
description: `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
op: 'db',
status: 'unknown_error',
origin: 'auto.db.otel.postgres',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
}),
]),
};

const EXPECTED_ERROR_EVENT = {
event_id: expect.any(String),
contexts: {
trace: {
trace_id: expect.any(String),
span_id: expect.any(String),
},
},
exception: {
values: [
{
type: 'PostgresError',
value: 'relation "User" does not exist',
stacktrace: expect.objectContaining({
frames: expect.arrayContaining([
expect.objectContaining({
function: 'handle',
module: 'postgres.cjs.src:connection',
filename: expect.any(String),
lineno: expect.any(Number),
colno: expect.any(Number),
}),
]),
}),
},
],
},
};

await createRunner(__dirname, 'scenario.js')
.withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
.expect({ transaction: EXPECTED_TRANSACTION })
.expect({ event: EXPECTED_ERROR_EVENT })
.start()
.completed();
});
});
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export {
onUnhandledRejectionIntegration,
parameterize,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
childProcessIntegration,
createSentryWinstonTransport,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
childProcessIntegration,
createSentryWinstonTransport,
Expand Down
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
hapiIntegration,
setupHapiErrorHandler,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
hapiIntegration,
setupHapiErrorHandler,
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { mysqlIntegration } from './integrations/tracing/mysql';
export { mysql2Integration } from './integrations/tracing/mysql2';
export { redisIntegration } from './integrations/tracing/redis';
export { postgresIntegration } from './integrations/tracing/postgres';
export { postgresJsIntegration } from './integrations/tracing/postgresjs';
export { prismaIntegration } from './integrations/tracing/prisma';
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa';
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { instrumentMongoose, mongooseIntegration } from './mongoose';
import { instrumentMysql, mysqlIntegration } from './mysql';
import { instrumentMysql2, mysql2Integration } from './mysql2';
import { instrumentPostgres, postgresIntegration } from './postgres';
import { instrumentPostgresJs, postgresJsIntegration } from './postgresjs';
import { prismaIntegration } from './prisma';
import { instrumentRedis, redisIntegration } from './redis';
import { instrumentTedious, tediousIntegration } from './tedious';
Expand Down Expand Up @@ -44,6 +45,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
amqplibIntegration(),
lruMemoizerIntegration(),
vercelAIIntegration(),
postgresJsIntegration(),
];
}

Expand Down Expand Up @@ -75,5 +77,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentGenericPool,
instrumentAmqplib,
instrumentVercelAi,
instrumentPostgresJs,
];
}
Loading
Loading