diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 3f7dfb18900e..c59909da82ce 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -161,7 +161,9 @@ "**/integrations/tracing/fastify/vendored/**/*.ts" ], "rules": { - "typescript/no-explicit-any": "off" + "typescript/no-explicit-any": "off", + "no-unsafe-member-access": "off", + "no-this-alias": "off" } }, { diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts index fef931399723..63e3fd7773f5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts @@ -12,8 +12,10 @@ describe('mysql2 auto instrumentation', () => { expect.objectContaining({ description: 'SELECT 1 + 1 AS solution', op: 'db', + origin: 'auto.db.otel.mysql2', data: expect.objectContaining({ 'db.system': 'mysql', + 'db.statement': 'SELECT 1 + 1 AS solution', 'net.peer.name': 'localhost', 'net.peer.port': 3306, 'db.user': 'root', @@ -22,8 +24,10 @@ describe('mysql2 auto instrumentation', () => { expect.objectContaining({ description: 'SELECT NOW()', op: 'db', + origin: 'auto.db.otel.mysql2', data: expect.objectContaining({ 'db.system': 'mysql', + 'db.statement': 'SELECT NOW()', 'net.peer.name': 'localhost', 'net.peer.port': 3306, 'db.user': 'root', diff --git a/packages/node/src/integrations/tracing/mysql2/index.ts b/packages/node/src/integrations/tracing/mysql2/index.ts index 7210622d268f..f02c6db7b589 100644 --- a/packages/node/src/integrations/tracing/mysql2/index.ts +++ b/packages/node/src/integrations/tracing/mysql2/index.ts @@ -1,19 +1,11 @@ import { MySQL2Instrumentation } from './vendored/instrumentation'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mysql2'; -export const instrumentMysql2 = generateInstrumentOnce( - INTEGRATION_NAME, - () => - new MySQL2Instrumentation({ - responseHook(span) { - addOriginToSpan(span, 'auto.db.otel.mysql2'); - }, - }), -); +export const instrumentMysql2 = generateInstrumentOnce(INTEGRATION_NAME, () => new MySQL2Instrumentation()); const _mysql2Integration = (() => { return { diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts index e1effc2aad8b..a76654b26c48 100644 --- a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts +++ b/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts @@ -18,67 +18,51 @@ * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 * - Types from 'mysql2' inlined as simplified interfaces * - Minor TypeScript strictness adjustments for this repository's compiler settings + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs */ -/* eslint-disable */ - -import * as api from '@opentelemetry/api'; -import { SDK_VERSION } from '@sentry/core'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - isWrapped, - safeExecuteInTheMiddle, - SemconvStability, - semconvStabilityFromStr, -} from '@opentelemetry/instrumentation'; + +import { SpanKind } from '@opentelemetry/api'; +import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; +import type { SpanAttributes } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { DB_SYSTEM_VALUE_MYSQL, ATTR_DB_STATEMENT, ATTR_DB_SYSTEM } from './semconv'; -import { addSqlCommenterComment } from '../../utils/sql-common'; -import type { Connection, Query, QueryOptions, QueryError, FieldPacket, FormatFunction } from './mysql2-types'; -import { MySQL2InstrumentationConfig } from './types'; +import type { Connection, FormatFunction, Query, QueryError, QueryOptions } from './mysql2-types'; +import { ATTR_DB_STATEMENT, ATTR_DB_SYSTEM, DB_SYSTEM_VALUE_MYSQL } from './semconv'; +import type { MySQL2InstrumentationConfig } from './types'; import { getConnectionAttributes, getConnectionPrototypeToInstrument, getQueryText, getSpanName, once } from './utils'; -import { - ATTR_DB_QUERY_TEXT, - ATTR_DB_SYSTEM_NAME, - DB_SYSTEM_NAME_VALUE_MYSQL, -} from '@opentelemetry/semantic-conventions'; const PACKAGE_NAME = '@sentry/instrumentation-mysql2'; +const ORIGIN = 'auto.db.otel.mysql2'; const supportedVersions = ['>=1.4.2 <4']; -export class MySQL2Instrumentation extends InstrumentationBase { - private _netSemconvStability!: SemconvStability; - private _dbSemconvStability!: SemconvStability; +// The raw imported `mysql2` module exposes the `format` helper used to render +// parameterized queries. Typed shallowly since it is only read internally. +type MySQL2Module = { format?: FormatFunction; [key: string]: unknown }; - constructor(config: MySQL2InstrumentationConfig = {}) { +export class MySQL2Instrumentation extends InstrumentationBase { + public constructor(config: MySQL2InstrumentationConfig = {}) { super(PACKAGE_NAME, SDK_VERSION, config); - this._setSemconvStabilityFromEnv(); } - private _setSemconvStabilityFromEnv() { - this._netSemconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); - this._dbSemconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); - } - - protected init() { + protected init(): InstrumentationNodeModuleDefinition[] { let format: FormatFunction | undefined; - function setFormatFunction(moduleExports: any) { + function setFormatFunction(moduleExports: MySQL2Module): void { if (!format && moduleExports.format) { format = moduleExports.format; } } - const patch = (ConnectionPrototype: Connection) => { + const patch = (ConnectionPrototype: Connection): void => { if (isWrapped(ConnectionPrototype.query)) { this._unwrap(ConnectionPrototype, 'query'); } - this._wrap(ConnectionPrototype, 'query', this._patchQuery(format, false) as any); + this._wrap(ConnectionPrototype, 'query', this._patchQuery(format) as any); if (isWrapped(ConnectionPrototype.execute)) { this._unwrap(ConnectionPrototype, 'execute'); } - this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format, true) as any); + this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format) as any); }; - const unpatch = (ConnectionPrototype: Connection) => { + const unpatch = (ConnectionPrototype: Connection): void => { this._unwrap(ConnectionPrototype, 'query'); this._unwrap(ConnectionPrototype, 'execute'); }; @@ -86,7 +70,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { + (moduleExports: MySQL2Module) => { setFormatFunction(moduleExports); return moduleExports; }, @@ -95,7 +79,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { + (moduleExports: MySQL2Module) => { setFormatFunction(moduleExports); return moduleExports; }, @@ -120,9 +104,9 @@ export class MySQL2Instrumentation extends InstrumentationBase { - const thisPlugin = this; return function query( this: Connection, query: string | Query | QueryOptions, @@ -135,61 +119,24 @@ export class MySQL2Instrumentation extends InstrumentationBase { + const endSpan = once((err?: QueryError | null) => { if (err) { - span.setStatus({ - code: api.SpanStatusCode.ERROR, - message: err.message, - }); - } else { - if (typeof responseHook === 'function') { - safeExecuteInTheMiddle( - () => { - responseHook(span, { - queryResults: results, - }); - }, - err => { - if (err) { - thisPlugin._diag.warn('Failed executing responseHook', err); - } - }, - true, - ); - } + span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); } - span.end(); }); @@ -204,8 +151,8 @@ export class MySQL2Instrumentation extends InstrumentationBase { endSpan(err); }) - .once('result', (results: any) => { - endSpan(undefined, results); + .once('result', () => { + endSpan(); }); return streamableQuery; @@ -222,11 +169,11 @@ export class MySQL2Instrumentation extends InstrumentationBase void) { return (originalCallback: Function) => { - return function (err: QueryError | null, results?: any, _fields?: FieldPacket[]) { - endSpan(err, results); - return originalCallback(...arguments); + return function (...args: [err: QueryError | null, ...rest: unknown[]]) { + endSpan(args[0]); + return originalCallback(...args); }; }; } diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts b/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts index 1ac348ca8742..dbdcbd3dd5bd 100644 --- a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts +++ b/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts @@ -17,7 +17,6 @@ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 */ -/* eslint-disable */ export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; export const ATTR_DB_NAME = 'db.name' as const; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/types.ts b/packages/node/src/integrations/tracing/mysql2/vendored/types.ts index d36f9f634b56..4f92e0f59178 100644 --- a/packages/node/src/integrations/tracing/mysql2/vendored/types.ts +++ b/packages/node/src/integrations/tracing/mysql2/vendored/types.ts @@ -16,27 +16,9 @@ * NOTICE from the Sentry authors: * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 + * - Pruned config options that Sentry's integration never sets (responseHook, query masking, SQL commenter) */ -/* eslint-disable */ -import { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import type { Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -export interface MySQL2ResponseHookInformation { - queryResults: any; -} - -export interface MySQL2InstrumentationExecutionResponseHook { - (span: Span, responseHookInfo: MySQL2ResponseHookInformation): void; -} - -export interface MySQL2InstrumentationQueryMaskingHook { - (query: string): string; -} - -export interface MySQL2InstrumentationConfig extends InstrumentationConfig { - maskStatement?: boolean; - maskStatementHook?: MySQL2InstrumentationQueryMaskingHook; - responseHook?: MySQL2InstrumentationExecutionResponseHook; - addSqlCommenterCommentToQueries?: boolean; -} +export type MySQL2InstrumentationConfig = InstrumentationConfig; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts b/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts index 1d2d4d2c90b1..4e01e9a2e189 100644 --- a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts +++ b/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts @@ -17,10 +17,11 @@ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 * - Types from 'mysql2' inlined as simplified interfaces + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs */ -/* eslint-disable */ -import { Attributes } from '@opentelemetry/api'; +import type { SpanAttributes } from '@sentry/core'; +import type { FormatFunction } from './mysql2-types'; import { ATTR_DB_CONNECTION_STRING, ATTR_DB_NAME, @@ -28,10 +29,6 @@ import { ATTR_NET_PEER_NAME, ATTR_NET_PEER_PORT, } from './semconv'; -import type { FormatFunction } from './mysql2-types'; -import { MySQL2InstrumentationQueryMaskingHook } from './types'; -import { SemconvStability } from '@opentelemetry/instrumentation'; -import { ATTR_DB_NAMESPACE, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT } from '@opentelemetry/semantic-conventions'; interface QueryOptions { sql: string; @@ -50,42 +47,26 @@ interface Config { connectionConfig?: Config; } -export function getConnectionAttributes( - config: Config, - dbSemconvStability: SemconvStability, - netSemconvStability: SemconvStability, -): Attributes { +export function getConnectionAttributes(config: Config): SpanAttributes { const { host, port, database, user } = getConfig(config); - const attrs: Attributes = {}; - if (dbSemconvStability & SemconvStability.OLD) { - attrs[ATTR_DB_CONNECTION_STRING] = getJDBCString(host, port, database); - attrs[ATTR_DB_NAME] = database; - attrs[ATTR_DB_USER] = user; - } - if (dbSemconvStability & SemconvStability.STABLE) { - attrs[ATTR_DB_NAMESPACE] = database; - } + const attrs: SpanAttributes = { + [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, port, database), + [ATTR_DB_NAME]: database, + [ATTR_DB_USER]: user, + [ATTR_NET_PEER_NAME]: host, + }; const portNumber = parseInt(port, 10); - if (netSemconvStability & SemconvStability.OLD) { - attrs[ATTR_NET_PEER_NAME] = host; - if (!isNaN(portNumber)) { - attrs[ATTR_NET_PEER_PORT] = portNumber; - } - } - if (netSemconvStability & SemconvStability.STABLE) { - attrs[ATTR_SERVER_ADDRESS] = host; - if (!isNaN(portNumber)) { - attrs[ATTR_SERVER_PORT] = portNumber; - } + if (!isNaN(portNumber)) { + attrs[ATTR_NET_PEER_PORT] = portNumber; } return attrs; } function getConfig(config: any) { - const { host, port, database, user } = (config && config.connectionConfig) || config || {}; + const { host, port, database, user } = config?.connectionConfig || config || {}; return { host, port, database, user }; } @@ -103,32 +84,20 @@ function getJDBCString(host: string | undefined, port: number | undefined, datab return jdbcString; } -export function getQueryText( - query: string | Query | QueryOptions, - format?: FormatFunction, - values?: any[], - maskStatement = false, - maskStatementHook: MySQL2InstrumentationQueryMaskingHook = defaultMaskingHook, -): string { +export function getQueryText(query: string | Query | QueryOptions, format?: FormatFunction, values?: any[]): string { const [querySql, queryValues] = typeof query === 'string' ? [query, values] : [query.sql, hasValues(query) ? values || query.values : values]; try { - if (maskStatement) { - return maskStatementHook(querySql); - } else if (format && queryValues) { + if (format && queryValues) { return format(querySql, queryValues); } else { return querySql; } - } catch (e) { - return 'Could not determine the query due to an error in masking or formatting'; + } catch { + return 'Could not determine the query due to an error in formatting'; } } -function defaultMaskingHook(query: string): string { - return query.replace(/\b\d+\b/g, '?').replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '?'); -} - function hasValues(obj: Query | QueryOptions): obj is QueryOptions { return 'values' in obj; } diff --git a/packages/node/test/integrations/tracing/mysql2.test.ts b/packages/node/test/integrations/tracing/mysql2.test.ts new file mode 100644 index 000000000000..3fc38b4653cb --- /dev/null +++ b/packages/node/test/integrations/tracing/mysql2.test.ts @@ -0,0 +1,260 @@ +/* + * The upstream @opentelemetry/instrumentation-mysql2 suite runs against a real + * mysql2 + MySQL server. Here we exercise the patched `query`/`execute` methods + * against a fake mysql2 connection so the instrumentation logic (span name, + * attributes, origin, error status, callback vs streamable signatures, parent + * linking, patch/unpatch) can be unit tested without a database. + */ + +import { EventEmitter } from 'node:events'; +import type { SpanJSON } from '@sentry/core'; +import { getClient, spanToJSON } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as Sentry from '../../../src'; +import { MySQL2Instrumentation } from '../../../src/integrations/tracing/mysql2/vendored/instrumentation'; +import { cleanupOtel, mockSdkInit } from '../../helpers/mockSdkInit'; + +type AnyFn = (...args: any[]) => any; + +const ORIGIN = 'auto.db.otel.mysql2'; + +const CONFIG = { host: 'localhost', port: 3306, database: 'test', user: 'root' }; +const ROWS = [{ solution: 2 }]; + +interface FakeMysql2 { + Connection: any; +} + +// Builds an original `query`/`execute` implementation mimicking mysql2: a trailing +// callback is invoked with `(err, rows)`, otherwise a streamable EventEmitter Query +// is returned that emits `result`/`error`. +function fakeQueryImpl({ reject = false }: { reject?: boolean } = {}): AnyFn { + return function (this: unknown, _sql: unknown, valuesOrCallback?: unknown, callback?: unknown): unknown { + const cb = (typeof valuesOrCallback === 'function' ? valuesOrCallback : callback) as AnyFn | undefined; + const err = reject ? Object.assign(new Error('boom'), { code: 'ER_FAKE' }) : null; + + if (cb) { + queueMicrotask(() => cb(err, reject ? undefined : ROWS)); + return undefined; + } + + const query = new EventEmitter(); + queueMicrotask(() => { + if (reject) { + query.emit('error', err); + } else { + query.emit('result', ROWS); + } + }); + return query; + }; +} + +function createFakeMysql2({ reject = false }: { reject?: boolean } = {}): FakeMysql2 { + class Connection { + public config = CONFIG; + } + + (Connection.prototype as any).query = fakeQueryImpl({ reject }); + (Connection.prototype as any).execute = fakeQueryImpl({ reject }); + + return { Connection }; +} + +// Waits a macrotask so the queued microtask callback/event has fired and the span ended. +function tick(): Promise { + return new Promise(resolve => setTimeout(resolve, 0)); +} + +describe('mysql2 instrumentation', () => { + let instrumentation: MySQL2Instrumentation; + let finishedSpans: SpanJSON[]; + + beforeEach(() => { + mockSdkInit({ tracesSampleRate: 1 }); + instrumentation = new MySQL2Instrumentation(); + + finishedSpans = []; + getClient()?.on('spanEnd', span => { + finishedSpans.push(spanToJSON(span)); + }); + }); + + afterEach(() => { + instrumentation.disable(); + cleanupOtel(); + }); + + // Sets the `format` helper (top-level module) and patches the Connection prototype + // through the real `mysql2/lib/connection.js` module-file patch. + function patch(fake: FakeMysql2, { format }: { format?: AnyFn } = {}): FakeMysql2 { + const definition = instrumentation.getModuleDefinitions()[0]!; + if (format) { + definition.patch!({ format }); + } + const connectionFile = definition.files.find(file => file.name.includes('connection'))!; + connectionFile.patch(fake.Connection); + return fake; + } + + function unpatch(fake: FakeMysql2): void { + const definition = instrumentation.getModuleDefinitions()[0]!; + const connectionFile = definition.files.find(file => file.name.includes('connection'))!; + connectionFile.unpatch!(fake.Connection); + } + + function mysqlSpans(): SpanJSON[] { + return finishedSpans.filter(span => span.origin === ORIGIN); + } + + function spanByDescription(description: string): SpanJSON | undefined { + return mysqlSpans().find(span => span.description === description); + } + + describe('callback signature', () => { + it('creates a span with the expected name, attributes and origin', async () => { + const { Connection } = patch(createFakeMysql2()); + const conn = new Connection(); + + await Sentry.startSpan( + { name: 'root' }, + () => new Promise(resolve => conn.query('SELECT 1 + 1 AS solution', () => resolve())), + ); + + const span = spanByDescription('SELECT'); + expect(span).toBeDefined(); + expect(span!.origin).toBe(ORIGIN); + expect(span!.data).toMatchObject({ + 'db.system': 'mysql', + 'db.statement': 'SELECT 1 + 1 AS solution', + 'db.name': 'test', + 'db.user': 'root', + 'db.connection_string': 'jdbc:mysql://localhost:3306/test', + 'net.peer.name': 'localhost', + 'net.peer.port': 3306, + }); + // op is derived downstream from `db.system`, not set by the instrumentation + expect(span!.data['sentry.op']).toBeUndefined(); + }); + + it('forwards the original callback result', async () => { + const { Connection } = patch(createFakeMysql2()); + const conn = new Connection(); + + const rows = await new Promise((resolve, reject) => { + Sentry.startSpan({ name: 'root' }, () => { + conn.query('SELECT 1', (err: Error | null, res?: unknown) => (err ? reject(err) : resolve(res))); + }); + }); + + expect(rows).toEqual(ROWS); + }); + + it('passes values through to the SQL formatter for `db.statement`', async () => { + const format: AnyFn = (sql: string, values: unknown[]) => `${sql} -- ${JSON.stringify(values)}`; + const { Connection } = patch(createFakeMysql2(), { format }); + const conn = new Connection(); + + await Sentry.startSpan( + { name: 'root' }, + () => new Promise(resolve => conn.query('SELECT ?', ['1'], () => resolve())), + ); + + const span = spanByDescription('SELECT'); + expect(span!.data['db.statement']).toBe('SELECT ? -- ["1"]'); + }); + + it('sets error status when the callback receives an error', async () => { + const { Connection } = patch(createFakeMysql2({ reject: true })); + const conn = new Connection(); + + await new Promise(resolve => { + Sentry.startSpan({ name: 'root' }, () => { + conn.query('SELECT 1', () => resolve()); + }); + }); + + const span = spanByDescription('SELECT'); + expect(span).toBeDefined(); + expect(span!.status).toContain('boom'); + }); + }); + + describe('execute', () => { + it('instruments `execute` like `query`', async () => { + const { Connection } = patch(createFakeMysql2()); + const conn = new Connection(); + + await Sentry.startSpan( + { name: 'root' }, + () => new Promise(resolve => conn.execute('UPDATE users SET x = 1', () => resolve())), + ); + + const span = spanByDescription('UPDATE'); + expect(span).toBeDefined(); + expect(span!.data['db.system']).toBe('mysql'); + }); + }); + + describe('streamable signature (no callback)', () => { + it('creates a span and ends it on the `result` event', async () => { + const { Connection } = patch(createFakeMysql2()); + const conn = new Connection(); + + Sentry.startSpan({ name: 'root' }, () => { + conn.query('SELECT NOW()'); + }); + await tick(); + + const span = spanByDescription('SELECT'); + expect(span).toBeDefined(); + expect(span!.data['db.statement']).toBe('SELECT NOW()'); + }); + + it('sets error status on the `error` event', async () => { + const { Connection } = patch(createFakeMysql2({ reject: true })); + const conn = new Connection(); + + Sentry.startSpan({ name: 'root' }, () => { + conn.query('SELECT NOW()'); + }); + await tick(); + + const span = spanByDescription('SELECT'); + expect(span).toBeDefined(); + expect(span!.status).toContain('boom'); + }); + }); + + describe('parent linking', () => { + it('parents the query span to the active span', async () => { + const { Connection } = patch(createFakeMysql2()); + const conn = new Connection(); + + let rootSpanId: string | undefined; + await Sentry.startSpan({ name: 'root' }, root => { + rootSpanId = root.spanContext().spanId; + return new Promise(resolve => conn.query('SELECT 1', () => resolve())); + }); + + const span = spanByDescription('SELECT'); + expect(span).toBeDefined(); + expect(span!.parent_span_id).toBe(rootSpanId); + }); + }); + + describe('unpatch', () => { + it('stops creating spans after unpatch', async () => { + const fake = patch(createFakeMysql2()); + unpatch(fake); + const conn = new fake.Connection(); + + await Sentry.startSpan( + { name: 'root' }, + () => new Promise(resolve => conn.query('SELECT 1', () => resolve())), + ); + + expect(mysqlSpans()).toHaveLength(0); + }); + }); +});