Skip to content

Commit 35bf27d

Browse files
onurtemizkanclaude
andcommitted
feat(node): Add ESM support for postgres.js instrumentation
This change refactors the postgres.js instrumentation to support both CommonJS and ESM environments. Key changes: - Uses `replaceExports` for ESM default export patching - Wraps sql instances via Proxy to intercept query creation - Adds support for `sql.unsafe()` and `sql.file()` methods - Adds support for `sql.begin()` and `sql.reserve()` transaction methods - Uses Symbol-based connection context storage instead of scope context - Adds `requestHook` configuration option for custom span modification - Improves span naming to use `db.{operation}` format - Comprehensive test coverage for CJS, ESM, URL initialization, and requestHook 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a3875c5 commit 35bf27d

File tree

15 files changed

+1354
-194
lines changed

15 files changed

+1354
-194
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [
10+
Sentry.postgresJsIntegration({
11+
requestHook: (span, sanitizedSqlQuery, connectionContext) => {
12+
// Add custom attributes to demonstrate requestHook functionality
13+
span.setAttribute('custom.requestHook', 'called');
14+
15+
// Set context information as extras for test validation
16+
Sentry.setExtra('requestHookCalled', {
17+
sanitizedQuery: sanitizedSqlQuery,
18+
database: connectionContext?.database,
19+
host: connectionContext?.host,
20+
port: connectionContext?.port,
21+
});
22+
},
23+
}),
24+
],
25+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [
10+
Sentry.postgresJsIntegration({
11+
requestHook: (span, sanitizedSqlQuery, connectionContext) => {
12+
// Add custom attributes to demonstrate requestHook functionality
13+
span.setAttribute('custom.requestHook', 'called');
14+
15+
// Set context information as extras for test validation
16+
Sentry.setExtra('requestHookCalled', {
17+
sanitizedQuery: sanitizedSqlQuery,
18+
database: connectionContext?.database,
19+
host: connectionContext?.host,
20+
port: connectionContext?.port,
21+
});
22+
},
23+
}),
24+
],
25+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
// Stop the process from exiting before the transaction is sent
12+
setInterval(() => {}, 1000);
13+
14+
const postgres = require('postgres');
15+
16+
const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
17+
18+
async function run() {
19+
await Sentry.startSpan(
20+
{
21+
name: 'Test Transaction',
22+
op: 'transaction',
23+
},
24+
async () => {
25+
try {
26+
await sql`
27+
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"));
28+
`;
29+
30+
await sql`
31+
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
32+
`;
33+
34+
await sql`
35+
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
36+
`;
37+
38+
await sql`
39+
DROP TABLE "User";
40+
`;
41+
} finally {
42+
await sql.end();
43+
}
44+
},
45+
);
46+
}
47+
48+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
49+
run();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as Sentry from '@sentry/node';
2+
import postgres from 'postgres';
3+
4+
// Stop the process from exiting before the transaction is sent
5+
setInterval(() => {}, 1000);
6+
7+
const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
8+
9+
async function run() {
10+
await Sentry.startSpan(
11+
{
12+
name: 'Test Transaction',
13+
op: 'transaction',
14+
},
15+
async () => {
16+
try {
17+
await sql`
18+
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"));
19+
`;
20+
21+
await sql`
22+
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
23+
`;
24+
25+
await sql`
26+
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
27+
`;
28+
29+
await sql`
30+
DROP TABLE "User";
31+
`;
32+
} finally {
33+
await sql.end();
34+
}
35+
},
36+
);
37+
}
38+
39+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
40+
run();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
// Import postgres AFTER Sentry.init() so instrumentation is set up
12+
const postgres = require('postgres');
13+
14+
// Stop the process from exiting before the transaction is sent
15+
setInterval(() => {}, 1000);
16+
17+
// Test with plain object options
18+
const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
19+
20+
async function run() {
21+
await Sentry.startSpan(
22+
{
23+
name: 'Test Transaction',
24+
op: 'transaction',
25+
},
26+
async () => {
27+
try {
28+
// Test sql.unsafe() - this was not being instrumented before the fix
29+
await sql.unsafe('CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))');
30+
31+
await sql.unsafe('INSERT INTO "User" ("email") VALUES ($1)', ['test@example.com']);
32+
33+
await sql.unsafe('SELECT * FROM "User" WHERE "email" = $1', ['test@example.com']);
34+
35+
await sql.unsafe('DROP TABLE "User"');
36+
37+
// This will be captured as an error as the table no longer exists
38+
await sql.unsafe('SELECT * FROM "User"');
39+
} finally {
40+
await sql.end();
41+
}
42+
},
43+
);
44+
}
45+
46+
run();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as Sentry from '@sentry/node';
2+
import postgres from 'postgres';
3+
4+
// Stop the process from exiting before the transaction is sent
5+
setInterval(() => {}, 1000);
6+
7+
// Test with plain object options
8+
const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
9+
10+
async function run() {
11+
await Sentry.startSpan(
12+
{
13+
name: 'Test Transaction',
14+
op: 'transaction',
15+
},
16+
async () => {
17+
try {
18+
// Test sql.unsafe() - this was not being instrumented before the fix
19+
await sql.unsafe('CREATE TABLE "User" ("id" SERIAL NOT NULL, "email" TEXT NOT NULL, PRIMARY KEY ("id"))');
20+
21+
await sql.unsafe('INSERT INTO "User" ("email") VALUES ($1)', ['test@example.com']);
22+
23+
await sql.unsafe('SELECT * FROM "User" WHERE "email" = $1', ['test@example.com']);
24+
25+
await sql.unsafe('DROP TABLE "User"');
26+
27+
// This will be captured as an error as the table no longer exists
28+
await sql.unsafe('SELECT * FROM "User"');
29+
} finally {
30+
await sql.end();
31+
}
32+
},
33+
);
34+
}
35+
36+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
37+
run();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
// Import postgres AFTER Sentry.init() so instrumentation is set up
12+
const postgres = require('postgres');
13+
14+
// Stop the process from exiting before the transaction is sent
15+
setInterval(() => {}, 1000);
16+
17+
// Test URL-based initialization - this is the common pattern that was causing the regression
18+
const sql = postgres('postgres://test:test@localhost:5444/test_db');
19+
20+
async function run() {
21+
await Sentry.startSpan(
22+
{
23+
name: 'Test Transaction',
24+
op: 'transaction',
25+
},
26+
async () => {
27+
try {
28+
await sql`
29+
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"));
30+
`;
31+
32+
await sql`
33+
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
34+
`;
35+
36+
await sql`
37+
UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com';
38+
`;
39+
40+
await sql`
41+
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
42+
`;
43+
44+
await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => {
45+
await Promise.all(rows);
46+
});
47+
48+
await sql`
49+
DROP TABLE "User";
50+
`;
51+
52+
// This will be captured as an error as the table no longer exists
53+
await sql`
54+
SELECT * FROM "User" WHERE "email" = 'foo@baz.com';
55+
`;
56+
} finally {
57+
await sql.end();
58+
}
59+
},
60+
);
61+
}
62+
63+
run();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as Sentry from '@sentry/node';
2+
import postgres from 'postgres';
3+
4+
// Stop the process from exiting before the transaction is sent
5+
setInterval(() => {}, 1000);
6+
7+
// Test URL-based initialization - this is the common pattern that was causing the regression
8+
const sql = postgres('postgres://test:test@localhost:5444/test_db');
9+
10+
async function run() {
11+
await Sentry.startSpan(
12+
{
13+
name: 'Test Transaction',
14+
op: 'transaction',
15+
},
16+
async () => {
17+
try {
18+
await sql`
19+
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"));
20+
`;
21+
22+
await sql`
23+
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
24+
`;
25+
26+
await sql`
27+
UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com';
28+
`;
29+
30+
await sql`
31+
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
32+
`;
33+
34+
// Test parameterized queries
35+
await sql`
36+
SELECT * FROM "User" WHERE "email" = ${'bar@baz.com'} AND "name" = ${'Foo'};
37+
`;
38+
39+
// Test DELETE operation
40+
await sql`
41+
DELETE FROM "User" WHERE "email" = 'bar@baz.com';
42+
`;
43+
44+
// Test INSERT with RETURNING
45+
await sql`
46+
INSERT INTO "User" ("email", "name") VALUES ('test@example.com', 'Test User') RETURNING *;
47+
`;
48+
49+
// Test cursor-based queries
50+
await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => {
51+
await Promise.all(rows);
52+
});
53+
54+
// Test multiple rows at once
55+
await sql`
56+
SELECT * FROM "User" LIMIT 10;
57+
`;
58+
59+
await sql`
60+
DROP TABLE "User";
61+
`;
62+
63+
// This will be captured as an error as the table no longer exists
64+
await sql`
65+
SELECT * FROM "User" WHERE "email" = 'foo@baz.com';
66+
`;
67+
} finally {
68+
await sql.end();
69+
}
70+
},
71+
);
72+
}
73+
74+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
75+
run();

0 commit comments

Comments
 (0)