From adad0af7a36c1f2ac0c2ade78d870f3d1fc751e8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 11 Feb 2026 18:56:11 +0100 Subject: [PATCH 1/3] feat(node): Add `ignoreConnectSpans` option to `postgresIntegration` --- .../postgres/scenario-ignoreConnect.js | 46 ++++++++++++++ .../suites/tracing/postgres/test.ts | 48 +++++++++++++++ .../node/src/integrations/tracing/postgres.ts | 11 +++- .../integrations/tracing/postgres.test.ts | 60 +++++++++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgres/scenario-ignoreConnect.js create mode 100644 packages/node/test/integrations/tracing/postgres.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-ignoreConnect.js b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-ignoreConnect.js new file mode 100644 index 000000000000..1de77ec5b89c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-ignoreConnect.js @@ -0,0 +1,46 @@ +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, + integrations: [Sentry.postgresIntegration({ ignoreConnectSpans: true })], + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const { Client } = require('pg'); + +const client = new Client({ port: 5494, user: 'test', password: 'test', database: 'tests' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await client.connect(); + + await client + .query( + '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"));', + ) + .catch(() => { + // if this is not a fresh database, the table might already exist + }); + + await client.query('INSERT INTO "User" ("email", "name") VALUES ($1, $2)', ['tim', 'tim@domain.com']); + await client.query('SELECT * FROM "User"'); + } finally { + await client.end(); + } + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index b0d0649a1ac9..d57621827f6d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -57,6 +57,54 @@ describe('postgres auto instrumentation', () => { .completed(); }); + test.only("doesn't emit connect spans if ignoreConnectSpans is true", { timeout: 90_000 }, async () => { + await createRunner(__dirname, 'scenario-ignoreConnect.js') + .withDockerCompose({ + workingDirectory: [__dirname], + readyMatches: ['port 5432'], + setupCommand: 'yarn', + }) + .expect({ + transaction: txn => { + const spanNames = txn.spans?.map(span => span.description); + expect(spanNames?.find(name => name?.includes('connect'))).toBeUndefined(); + expect(txn).toMatchObject({ + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'SELECT * FROM "User"', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'SELECT * FROM "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + ]), + }); + }, + }) + .start() + .completed(); + }); + test('should auto-instrument `pg-native` package', { timeout: 90_000 }, async () => { const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', diff --git a/packages/node/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts index d3b3c0cc0edf..5520a75f9a9b 100644 --- a/packages/node/src/integrations/tracing/postgres.ts +++ b/packages/node/src/integrations/tracing/postgres.ts @@ -3,24 +3,29 @@ import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; +interface PostgresIntegrationOptions { + ignoreConnectSpans?: boolean; +} + const INTEGRATION_NAME = 'Postgres'; export const instrumentPostgres = generateInstrumentOnce( INTEGRATION_NAME, - () => + (options?: PostgresIntegrationOptions) => new PgInstrumentation({ requireParentSpan: true, requestHook(span) { addOriginToSpan(span, 'auto.db.otel.postgres'); }, + ignoreConnectSpans: options?.ignoreConnectSpans ?? false, }), ); -const _postgresIntegration = (() => { +const _postgresIntegration = ((options?: PostgresIntegrationOptions) => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentPostgres(); + instrumentPostgres(options); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/test/integrations/tracing/postgres.test.ts b/packages/node/test/integrations/tracing/postgres.test.ts new file mode 100644 index 000000000000..41f42c5c4e7e --- /dev/null +++ b/packages/node/test/integrations/tracing/postgres.test.ts @@ -0,0 +1,60 @@ +import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; +import { INSTRUMENTED } from '@sentry/node-core'; +import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; +import { instrumentPostgres, postgresIntegration } from '../../../src/integrations/tracing/postgres'; + +vi.mock('@opentelemetry/instrumentation-pg'); + +describe('postgres integration', () => { + beforeEach(() => { + vi.clearAllMocks(); + delete INSTRUMENTED.Postgres; + + (PgInstrumentation as unknown as MockInstance).mockImplementation(() => ({ + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: () => ({}), + enable: () => undefined, + })); + }); + + it('has a name and setupOnce method', () => { + const integration = postgresIntegration(); + expect(integration.name).toBe('Postgres'); + expect(typeof integration.setupOnce).toBe('function'); + }); + + it('passes ignoreConnectSpans: true to PgInstrumentation when set on integration', () => { + postgresIntegration({ ignoreConnectSpans: true }).setupOnce!(); + + expect(PgInstrumentation).toHaveBeenCalledTimes(1); + expect(PgInstrumentation).toHaveBeenCalledWith({ + requireParentSpan: true, + requestHook: expect.any(Function), + ignoreConnectSpans: true, + }); + }); + + it('passes ignoreConnectSpans: false to PgInstrumentation by default', () => { + postgresIntegration().setupOnce!(); + + expect(PgInstrumentation).toHaveBeenCalledTimes(1); + expect(PgInstrumentation).toHaveBeenCalledWith({ + requireParentSpan: true, + requestHook: expect.any(Function), + ignoreConnectSpans: false, + }); + }); + + it('instrumentPostgres receives ignoreConnectSpans option', () => { + instrumentPostgres({ ignoreConnectSpans: true }); + + expect(PgInstrumentation).toHaveBeenCalledTimes(1); + expect(PgInstrumentation).toHaveBeenCalledWith({ + requireParentSpan: true, + requestHook: expect.any(Function), + ignoreConnectSpans: true, + }); + }); +}); From 755fd069d7f7327aa260bce863f922a23a514fe2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 11 Feb 2026 19:08:03 +0100 Subject: [PATCH 2/3] fix obscure generateInstrumentOnce bug --- .../suites/tracing/postgres/test.ts | 2 +- .../node/src/integrations/tracing/postgres.ts | 16 ++++----- .../integrations/tracing/postgres.test.ts | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index d57621827f6d..1fd03e92d0e2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -57,7 +57,7 @@ describe('postgres auto instrumentation', () => { .completed(); }); - test.only("doesn't emit connect spans if ignoreConnectSpans is true", { timeout: 90_000 }, async () => { + test("doesn't emit connect spans if ignoreConnectSpans is true", { timeout: 90_000 }, async () => { await createRunner(__dirname, 'scenario-ignoreConnect.js') .withDockerCompose({ workingDirectory: [__dirname], diff --git a/packages/node/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts index 5520a75f9a9b..7aeb6c671b63 100644 --- a/packages/node/src/integrations/tracing/postgres.ts +++ b/packages/node/src/integrations/tracing/postgres.ts @@ -11,14 +11,14 @@ const INTEGRATION_NAME = 'Postgres'; export const instrumentPostgres = generateInstrumentOnce( INTEGRATION_NAME, - (options?: PostgresIntegrationOptions) => - new PgInstrumentation({ - requireParentSpan: true, - requestHook(span) { - addOriginToSpan(span, 'auto.db.otel.postgres'); - }, - ignoreConnectSpans: options?.ignoreConnectSpans ?? false, - }), + PgInstrumentation, + (options?: PostgresIntegrationOptions) => ({ + requireParentSpan: true, + requestHook(span) { + addOriginToSpan(span, 'auto.db.otel.postgres'); + }, + ignoreConnectSpans: options?.ignoreConnectSpans ?? false, + }), ); const _postgresIntegration = ((options?: PostgresIntegrationOptions) => { diff --git a/packages/node/test/integrations/tracing/postgres.test.ts b/packages/node/test/integrations/tracing/postgres.test.ts index 41f42c5c4e7e..39c4714b4b54 100644 --- a/packages/node/test/integrations/tracing/postgres.test.ts +++ b/packages/node/test/integrations/tracing/postgres.test.ts @@ -57,4 +57,37 @@ describe('postgres integration', () => { ignoreConnectSpans: true, }); }); + + it('second call to instrumentPostgres passes full config to setConfig, not raw user options', () => { + const mockSetConfig = vi.fn(); + (PgInstrumentation as unknown as MockInstance).mockImplementation((config: unknown) => ({ + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: mockSetConfig, + enable: () => undefined, + })); + + instrumentPostgres({ ignoreConnectSpans: true }); + expect(PgInstrumentation).toHaveBeenCalledWith( + expect.objectContaining({ + requireParentSpan: true, + ignoreConnectSpans: true, + requestHook: expect.any(Function), + }), + ); + + mockSetConfig.mockClear(); + instrumentPostgres({ ignoreConnectSpans: false }); + + expect(PgInstrumentation).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledTimes(1); + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + requireParentSpan: true, + ignoreConnectSpans: false, + requestHook: expect.any(Function), + }), + ); + }); }); From 56a235d7f645b3fa8e688751564a1b9d959c59f6 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Feb 2026 12:44:28 +0100 Subject: [PATCH 3/3] lint --- packages/node/test/integrations/tracing/postgres.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/test/integrations/tracing/postgres.test.ts b/packages/node/test/integrations/tracing/postgres.test.ts index 39c4714b4b54..aeb72e75c498 100644 --- a/packages/node/test/integrations/tracing/postgres.test.ts +++ b/packages/node/test/integrations/tracing/postgres.test.ts @@ -60,7 +60,7 @@ describe('postgres integration', () => { it('second call to instrumentPostgres passes full config to setConfig, not raw user options', () => { const mockSetConfig = vi.fn(); - (PgInstrumentation as unknown as MockInstance).mockImplementation((config: unknown) => ({ + (PgInstrumentation as unknown as MockInstance).mockImplementation(() => ({ setTracerProvider: () => undefined, setMeterProvider: () => undefined, getConfig: () => ({}),