diff --git a/packages/node/src/integrations/tracing/postgres/index.ts b/packages/node/src/integrations/tracing/postgres/index.ts deleted file mode 100644 index 362effccc554..000000000000 --- a/packages/node/src/integrations/tracing/postgres/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PgInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -interface PostgresIntegrationOptions { - ignoreConnectSpans?: boolean; -} - -const INTEGRATION_NAME = 'Postgres' as const; - -export const instrumentPostgres = generateInstrumentOnce( - INTEGRATION_NAME, - PgInstrumentation, - (options?: PostgresIntegrationOptions) => ({ - ignoreConnectSpans: options?.ignoreConnectSpans ?? false, - }), -); diff --git a/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts deleted file mode 100644 index 15a8689c4971..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - */ - -// Postgresql specific attributes not covered by semantic conventions -export enum AttributeNames { - PG_PLAN = 'db.postgresql.plan', - IDLE_TIMEOUT_MILLIS = 'db.postgresql.idle.timeout.millis', - MAX_CLIENT = 'db.postgresql.max.client', -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts b/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts deleted file mode 100644 index cffc9100d4f3..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - */ - -// Contains span names produced by instrumentation -export enum SpanNames { - QUERY_PREFIX = 'pg.query', - CONNECT = 'pg.connect', - POOL_CONNECT = 'pg-pool.connect', -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts deleted file mode 100644 index 9aae196ed98d..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` and `pg-pool` packages inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - * - Dropped the OTel metrics (no MeterProvider is wired up), the `SemconvStability` - * dual-emission, and config the SDK never passes (request/response hooks, - * enhancedDatabaseReporting, addSqlCommenterCommentToQueries) - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Span } from '@sentry/core'; -import { getActiveSpan, SDK_VERSION, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { SENTRY_KIND } from '@sentry/conventions/attributes'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { SpanNames } from './enums/SpanNames'; -import type { - PgClientConnect, - PgClientExtended, - PgPoolCallback, - PgPoolExtended, - PostgresCallback, -} from './internal-types'; -import type { PgInstrumentationConfig } from './types'; -import * as utils from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-pg'; - -function extractModuleExports(module: any): any { - return module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS -} - -/** - * Binds `callback` to `parentSpan`, so that the span is active again whenever the - * (deferred) callback runs. This mirrors the upstream `context.bind(context.active(), callback)`: - * `pg` invokes callbacks outside of the original async scope (e.g. for pooled connections), so we - * re-establish the trace context to keep spans created inside the callback correctly parented. - */ -function bindCallbackToSpan any>(parentSpan: Span, callback: T): T { - return function (this: unknown, ...args: any[]): unknown { - return withActiveSpan(parentSpan, () => callback.apply(this, args)); - } as T; -} - -export class PgInstrumentation extends InstrumentationBase { - public constructor(config: PgInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition[] { - const SUPPORTED_PG_VERSIONS = ['>=8.0.3 <9']; - const SUPPORTED_PG_POOL_VERSIONS = ['>=2.0.0 <4']; - - const modulePgNativeClient = new InstrumentationNodeModuleFile( - 'pg/lib/native/client.js', - SUPPORTED_PG_VERSIONS, - this._patchPgClient.bind(this), - this._unpatchPgClient.bind(this), - ); - - const modulePgClient = new InstrumentationNodeModuleFile( - 'pg/lib/client.js', - SUPPORTED_PG_VERSIONS, - this._patchPgClient.bind(this), - this._unpatchPgClient.bind(this), - ); - - const modulePG = new InstrumentationNodeModuleDefinition( - 'pg', - SUPPORTED_PG_VERSIONS, - (module: any) => { - const moduleExports = extractModuleExports(module); - - this._patchPgClient(moduleExports.Client); - return module; - }, - (module: any) => { - const moduleExports = extractModuleExports(module); - - this._unpatchPgClient(moduleExports.Client); - return module; - }, - [modulePgClient, modulePgNativeClient], - ); - - const modulePGPool = new InstrumentationNodeModuleDefinition( - 'pg-pool', - SUPPORTED_PG_POOL_VERSIONS, - (module: any) => { - const moduleExports = extractModuleExports(module); - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - this._wrap(moduleExports.prototype, 'connect', this._getPoolConnectPatch()); - return moduleExports; - }, - (module: any) => { - const moduleExports = extractModuleExports(module); - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - }, - ); - - return [modulePG, modulePGPool]; - } - - private _patchPgClient(module: any): any { - if (!module) { - return; - } - - const moduleExports = extractModuleExports(module); - - if (isWrapped(moduleExports.prototype.query)) { - this._unwrap(moduleExports.prototype, 'query'); - } - - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - - this._wrap(moduleExports.prototype, 'query', this._getClientQueryPatch()); - - this._wrap(moduleExports.prototype, 'connect', this._getClientConnectPatch()); - - return module; - } - - private _unpatchPgClient(module: any): any { - const moduleExports = extractModuleExports(module); - - if (isWrapped(moduleExports.prototype.query)) { - this._unwrap(moduleExports.prototype, 'query'); - } - - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - - return module; - } - - private _getClientConnectPatch() { - const plugin = this; - return (original: PgClientConnect) => { - return function connect(this: PgClientExtended, callback?: (...args: unknown[]) => void) { - if (utils.shouldSkipInstrumentation() || plugin.getConfig().ignoreConnectSpans) { - return original.call(this, callback); - } - - const span = startInactiveSpan({ - name: SpanNames.CONNECT, - attributes: { - [SENTRY_KIND]: 'client', - ...utils.getSemanticAttributesFromConnection(this), - }, - }); - - let cb = callback; - if (cb) { - const parentSpan = getActiveSpan(); - cb = utils.patchClientConnectCallback(span, cb); - if (parentSpan) { - cb = bindCallbackToSpan(parentSpan, cb); - } - } - - const connectResult: unknown = withActiveSpan(span, () => { - return original.call(this, cb); - }); - - return handleConnectResult(span, connectResult); - }; - }; - } - - private _getClientQueryPatch() { - return (original: (...args: any[]) => any) => { - this._diag.debug('Patching pg.Client.prototype.query'); - return function query(this: PgClientExtended, ...args: unknown[]) { - if (utils.shouldSkipInstrumentation()) { - return original.apply(this, args as never); - } - - // client.query(text, cb?), client.query(text, values, cb?), and - // client.query(configObj, cb?) are all valid signatures. We construct - // a queryConfig obj from all (valid) signatures to build the span in a - // unified way. We verify that we at least have query text, and code - // defensively when dealing with `queryConfig` after that (to handle all - // the other invalid cases, like a non-array for values being provided). - // The type casts here reflect only what we've actually validated. - const arg0 = args[0]; - const firstArgIsString = typeof arg0 === 'string'; - const firstArgIsQueryObjectWithText = utils.isObjectWithTextString(arg0); - - const queryConfig = firstArgIsString - ? { - text: arg0, - values: Array.isArray(args[1]) ? args[1] : undefined, - } - : firstArgIsQueryObjectWithText - ? { - ...(arg0 as any), - name: arg0.name, - text: arg0.text, - values: (arg0 as any).values ?? (Array.isArray(args[1]) ? args[1] : undefined), - } - : undefined; - - const span = utils.handleConfigQuery.call(this, queryConfig); - - // Bind callback (if any) to parent span (if any) - if (args.length > 0) { - const parentSpan = getActiveSpan(); - if (typeof args[args.length - 1] === 'function') { - // Patch ParameterQuery callback - args[args.length - 1] = utils.patchCallback(span, args[args.length - 1] as PostgresCallback); - - // If a parent span exists, bind the callback - if (parentSpan) { - args[args.length - 1] = bindCallbackToSpan(parentSpan, args[args.length - 1] as PostgresCallback); - } - } else if (typeof queryConfig?.callback === 'function') { - // Patch ConfigQuery callback - let callback = utils.patchCallback(span, queryConfig.callback as PostgresCallback); - - // If a parent span existed, bind the callback - if (parentSpan) { - callback = bindCallbackToSpan(parentSpan, callback); - } - - (args[0] as { callback?: PostgresCallback }).callback = callback; - } - } - - let result: unknown; - try { - result = original.apply(this, args as never); - } catch (e: unknown) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(e) }); - span.end(); - throw e; - } - - // Bind promise to parent span and end the span - if (result instanceof Promise) { - return result - .then((result: unknown) => { - span.end(); - return result; - }) - .catch((error: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(error) }); - span.end(); - return Promise.reject(error); - }); - } - - // else returns void - return result; // void - }; - }; - } - - private _getPoolConnectPatch() { - const plugin = this; - return (originalConnect: (...args: any[]) => any) => { - return function connect(this: PgPoolExtended, callback?: PgPoolCallback) { - if (utils.shouldSkipInstrumentation() || plugin.getConfig().ignoreConnectSpans) { - return originalConnect.call(this, callback); - } - - const span = startInactiveSpan({ - name: SpanNames.POOL_CONNECT, - attributes: { - [SENTRY_KIND]: 'client', - ...utils.getSemanticAttributesFromPoolConnection(this.options), - }, - }); - - let cb = callback; - if (cb) { - const parentSpan = getActiveSpan(); - cb = utils.patchCallbackPGPool(span, cb); - // If a parent span exists, bind the callback - if (parentSpan) { - cb = bindCallbackToSpan(parentSpan, cb); - } - } - - const connectResult: unknown = withActiveSpan(span, () => { - return originalConnect.call(this, cb); - }); - - return handleConnectResult(span, connectResult); - }; - }; - } -} - -function handleConnectResult(span: Span, connectResult: unknown): unknown { - if (!(connectResult instanceof Promise)) { - return connectResult; - } - - const connectResultPromise = connectResult as Promise; - // The caller's continuation after `await client.connect()` keeps its trace context via the - // SDK's async context propagation, so we don't need to re-bind the returned promise. - return connectResultPromise - .then(result => { - span.end(); - return result; - }) - .catch((error: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(error) }); - span.end(); - return Promise.reject(error); - }); -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts deleted file mode 100644 index b62df8c77234..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` and `pg-pool` packages inlined as simplified interfaces - */ - -import type { PgClient } from './pg-types'; -import type { PgPool } from './pg-pool-types'; - -export type PostgresCallback = (err: Error, res: object) => unknown; - -// NB: this type describes the shape of a parsed, normalized form of the -// connection information that's stored inside each pg.Client instance. It's -// _not_ the same as the ConnectionConfig type exported from `@types/pg`. That -// type defines how data must be _passed in_ when creating a new `pg.Client`, -// which doesn't necessarily match the normalized internal form. E.g., a user -// can call `new Client({ connectionString: '...' }), but `connectionString` -// will never show up in the type below, because only the extracted host, port, -// etc. are recorded in this normalized config. The keys listed below are also -// incomplete, which is fine because the type is internal and these keys are the -// only ones our code is reading. See https://github.com/brianc/node-postgres/blob/fde5ec586e49258dfc4a2fcd861fcdecb4794fc3/lib/client.js#L25 -export interface PgParsedConnectionParams { - database?: string; - host?: string; - namespace?: string; - port?: number; - user?: string; -} - -export interface PgClientExtended extends PgClient { - connectionParameters: PgParsedConnectionParams; -} - -export type PgPoolCallback = (err: Error, client: any, done: (release?: any) => void) => void; - -export interface PgPoolOptionsParams { - allowExitOnIdle: boolean; - connectionString?: string; // connection string if provided directly - database: string; - host: string; - idleTimeoutMillis: number; // the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due to idle time - max: number; - maxClient: number; // maximum size of the pool - maxLifetimeSeconds: number; - maxUses: number; - namespace: string; - port: number; - user: string; -} - -export interface PgPoolExtended extends PgPool { - options: PgPoolOptionsParams; -} - -export type PgClientConnect = (callback?: Function) => Promise | void; diff --git a/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts deleted file mode 100644 index 4bf1ea6a112a..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Simplified type definitions inlined from the `pg-pool` package. - * PoolClient and PoolConfig are replaced with structural equivalents. - */ - -import type { PgPoolClient } from './pg-types'; - -export interface PgPool { - readonly totalCount: number; - readonly idleCount: number; - readonly waitingCount: number; - readonly expiredCount: number; - - readonly ending: boolean; - readonly ended: boolean; - - options: any; - - connect(): Promise; - connect( - callback: (err: Error | undefined, client: PgPoolClient | undefined, done: (release?: any) => void) => void, - ): void; - - end(): Promise; - end(callback: () => void): void; - - query(...args: any[]): any; - - on(event: 'release' | 'error', listener: (err: Error, client: PgPoolClient) => void): this; - on(event: 'connect' | 'acquire' | 'remove', listener: (client: PgPoolClient) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - - [key: string]: any; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts deleted file mode 100644 index 181449405947..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Simplified type definitions inlined from the `pg` package. - * Deep type trees (Connection, Submittable, QueryConfig generics, FieldDef, - * NoticeMessage, ClientConfig) are replaced with structural equivalents. - */ - -export interface FieldDef { - name: string; - tableID: number; - columnID: number; - dataTypeID: number; - dataTypeSize: number; - dataTypeModifier: number; - format: string; -} - -export interface QueryResultBase { - command: string; - rowCount: number | null; - oid: number; - fields: FieldDef[]; -} - -export interface QueryResult extends QueryResultBase { - rows: R[]; -} - -export interface QueryArrayResult extends QueryResultBase { - rows: R[]; -} - -export interface PgClientBase { - connect(): Promise; - connect(callback: (err: Error) => void): void; - - query(...args: any[]): any; - - copyFrom(queryText: string): any; - copyTo(queryText: string): any; - - pauseDrain(): void; - resumeDrain(): void; - - escapeIdentifier(str: string): string; - escapeLiteral(str: string): string; - - on(event: 'drain', listener: () => void): this; - on(event: 'error', listener: (err: Error) => void): this; - on(event: 'notice', listener: (notice: any) => void): this; - on(event: 'notification', listener: (message: any) => void): this; - on(event: 'end', listener: () => void): this; - on(event: string, listener: (...args: any[]) => void): this; - - [key: string]: any; -} - -export interface PgClient extends PgClientBase { - user?: string | undefined; - database?: string | undefined; - port: number; - host: string; - password?: string | undefined; - ssl: boolean; - readonly connection: any; - - end(): Promise; - end(callback: (err: Error) => void): void; -} - -export interface PgPoolClient extends PgClientBase { - release(err?: Error | boolean): void; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts b/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts deleted file mode 100644 index ae3306d29c7a..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Trimmed to the (old) semantic conventions the SDK actually emits - */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by this package. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -/** - * Deprecated, use `server.address`, `server.port` attributes instead. - * - * @example "Server=(localdb)\\v11.0;Integrated Security=true;" - * - * @deprecated Replaced by `server.address` and `server.port`. - */ -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; - -/** - * Enum value "postgresql" for attribute `db.system`. - */ -export const DB_SYSTEM_VALUE_POSTGRESQL = 'postgresql' as const; diff --git a/packages/node/src/integrations/tracing/postgres/vendored/types.ts b/packages/node/src/integrations/tracing/postgres/vendored/types.ts deleted file mode 100644 index 82487d8acb53..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Trimmed to the config the SDK actually passes - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export interface PgInstrumentationConfig extends InstrumentationConfig { - /** - * If true, `pg.connect` and `pg-pool.connect` spans will not be created. - * Query spans are still recorded. - * - * @default false - */ - ignoreConnectSpans?: boolean; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/utils.ts b/packages/node/src/integrations/tracing/postgres/vendored/utils.ts deleted file mode 100644 index 2af01ae35c9d..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/utils.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` package inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { Span, SpanAttributes } from '@sentry/core'; -import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; -import { AttributeNames } from './enums/AttributeNames'; -import { SpanNames } from './enums/SpanNames'; -import type { - PgClientExtended, - PgParsedConnectionParams, - PgPoolCallback, - PgPoolExtended, - PgPoolOptionsParams, - PostgresCallback, -} from './internal-types'; -import type { PgClient } from './pg-types'; -import { ATTR_DB_CONNECTION_STRING, DB_SYSTEM_VALUE_POSTGRESQL } from './semconv'; -import { - DB_NAME, - DB_STATEMENT, - DB_SYSTEM, - DB_USER, - NET_PEER_NAME, - NET_PEER_PORT, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; - -/* oxlint-disable typescript/no-deprecated */ - -export const ORIGIN = 'auto.db.otel.postgres'; - -/** - * Helper function to get a low cardinality span name from whatever info we have - * about the query. - * - * This is tricky, because we don't have most of the information (table name, - * operation name, etc) the spec recommends using to build a low-cardinality - * value w/o parsing. So, we use db.name and assume that, if the query's a named - * prepared statement, those `name` values will be low cardinality. If we don't - * have a named prepared statement, we try to parse an operation (despite the - * spec's warnings). - * - * @params dbName The name of the db against which this query is being issued, - * which could be missing if no db name was given at the time that the - * connection was established. - * @params queryConfig Information we have about the query being issued, typed - * to reflect only the validation we've actually done on the args to - * `client.query()`. This will be undefined if `client.query()` was called - * with invalid arguments. - */ -export function getQuerySpanName(dbName: string | undefined, queryConfig?: { text: string; name?: unknown }): string { - // NB: when the query config is invalid, we omit the dbName too, so that - // someone (or some tool) reading the span name doesn't misinterpret the - // dbName as being a prepared statement or sql commit name. - if (!queryConfig) return SpanNames.QUERY_PREFIX; - - // Either the name of a prepared statement; or an attempted parse - // of the SQL command, normalized to uppercase; or unknown. - const command = - typeof queryConfig.name === 'string' && queryConfig.name - ? queryConfig.name - : parseNormalizedOperationName(queryConfig.text); - - return `${SpanNames.QUERY_PREFIX}:${command}${dbName ? ` ${dbName}` : ''}`; -} - -export function parseNormalizedOperationName(queryText: string): string { - // Trim the query text to handle leading/trailing whitespace - const trimmedQuery = queryText.trim(); - const indexOfFirstSpace = trimmedQuery.indexOf(' '); - let sqlCommand = indexOfFirstSpace === -1 ? trimmedQuery : trimmedQuery.slice(0, indexOfFirstSpace); - sqlCommand = sqlCommand.toUpperCase(); - - // Handle query text being "COMMIT;", which has an extra semicolon before the space. - return sqlCommand.endsWith(';') ? sqlCommand.slice(0, -1) : sqlCommand; -} - -export function parseAndMaskConnectionString(connectionString: string): string { - try { - // Parse the connection string - const url = new URL(connectionString); - - // Remove all auth information (username and password) - url.username = ''; - url.password = ''; - - return url.toString(); - } catch { - // If parsing fails, return a generic connection string - return 'postgresql://localhost:5432/'; - } -} - -export function getConnectionString(params: PgParsedConnectionParams | PgPoolOptionsParams): string { - if ('connectionString' in params && params.connectionString) { - return parseAndMaskConnectionString(params.connectionString); - } - const host = params.host || 'localhost'; - const port = params.port || 5432; - const database = params.database || ''; - return `postgresql://${host}:${port}/${database}`; -} - -function getPort(port: number | undefined): number | undefined { - // Port may be NaN as parseInt() is used on the value, passing null will result in NaN being parsed. - // https://github.com/brianc/node-postgres/blob/2a8efbee09a284be12748ed3962bc9b816965e36/packages/pg/lib/connection-parameters.js#L66 - if (Number.isInteger(port)) { - return port; - } - - // Unable to find the default used in pg code, so falling back to 'undefined'. - return undefined; -} - -export function getSemanticAttributesFromConnection(params: PgParsedConnectionParams): SpanAttributes { - return { - [DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL, - [DB_NAME]: params.database, - [ATTR_DB_CONNECTION_STRING]: getConnectionString(params), - [DB_USER]: params.user, - [NET_PEER_NAME]: params.host, // required - [NET_PEER_PORT]: getPort(params.port), - }; -} - -export function getSemanticAttributesFromPoolConnection(params: PgPoolOptionsParams): SpanAttributes { - let url: URL | undefined; - try { - url = params.connectionString ? new URL(params.connectionString) : undefined; - } catch { - url = undefined; - } - - return { - [AttributeNames.IDLE_TIMEOUT_MILLIS]: params.idleTimeoutMillis, - [AttributeNames.MAX_CLIENT]: params.maxClient, - [DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL, - [DB_NAME]: url?.pathname.slice(1) ?? params.database, - [ATTR_DB_CONNECTION_STRING]: getConnectionString(params), - [NET_PEER_NAME]: url?.hostname ?? params.host, - [NET_PEER_PORT]: Number(url?.port) || getPort(params.port), - [DB_USER]: url?.username ?? params.user, - }; -} - -/** - * The SDK always requires a parent span (it sets `requireParentSpan: true`), so - * we only instrument when there is an active span to parent the new span under. - */ -export function shouldSkipInstrumentation(): boolean { - return getActiveSpan() === undefined; -} - -// Create an (inactive) span from our normalized queryConfig object, -// or return a basic span if no queryConfig was given/could be created. -export function handleConfigQuery( - this: PgClientExtended, - queryConfig?: { text: string; values?: unknown; name?: unknown }, -): Span { - // Create child span. - const { connectionParameters } = this; - const dbName = connectionParameters.database; - - const spanName = getQuerySpanName(dbName, queryConfig); - const span = startInactiveSpan({ - name: spanName, - attributes: { - [SENTRY_KIND]: 'client', - ...getSemanticAttributesFromConnection(connectionParameters), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }, - }); - - if (!queryConfig) { - return span; - } - - // Set attributes - if (queryConfig.text) { - span.setAttribute(DB_STATEMENT, queryConfig.text); - } - - // Set plan name attribute, if present - if (typeof queryConfig.name === 'string') { - span.setAttribute(AttributeNames.PG_PLAN, queryConfig.name); - } - - return span; -} - -export function patchCallback(span: Span, cb: PostgresCallback): PostgresCallback { - return function patchedCallback(this: PgClientExtended, err: Error, res: object) { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.call(this, err, res); - }; -} - -export function patchCallbackPGPool(span: Span, cb: PgPoolCallback): PgPoolCallback { - return function patchedCallback(this: PgPoolExtended, err: Error, res: object, done: any) { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.call(this, err, res, done); - }; -} - -export function patchClientConnectCallback(span: Span, cb: (...args: unknown[]) => void): (...args: unknown[]) => void { - return function patchedClientConnectCallback(this: PgClient, ...args: unknown[]) { - const err = args[0]; - if (err instanceof Error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.apply(this, args); - }; -} - -/** - * Attempt to get a message string from a thrown value, while being quite - * defensive, to recognize the fact that, in JS, any kind of value (even - * primitives) can be thrown. - */ -export function getErrorMessage(e: unknown): string | undefined { - return typeof e === 'object' && e !== null && 'message' in e - ? String((e as { message?: unknown }).message) - : undefined; -} - -export function isObjectWithTextString(it: unknown): it is ObjectWithText { - return typeof it === 'object' && typeof (it as null | { text?: unknown })?.text === 'string'; -} - -export type ObjectWithText = { - text: string; - [k: string]: unknown; -};