diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index ed76bfcbec4f..b0ec56dea204 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -14,7 +14,8 @@ "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "clean": "rimraf -g **/node_modules", + "clean": "rimraf -g **/node_modules && run-p clean:docker:*", + "clean:docker:mysql2": "cd suites/tracing-experimental/mysql2 && docker-compose down --volumes", "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)", "lint": "eslint . --format stylish", @@ -44,6 +45,7 @@ "mongodb-memory-server-global": "^7.6.3", "mongoose": "^5.13.22", "mysql": "^2.18.1", + "mysql2": "^3.7.1", "nock": "^13.1.0", "pg": "^8.7.3", "proxy": "^2.1.1", diff --git a/dev-packages/node-integration-tests/suites/proxy/test.ts b/dev-packages/node-integration-tests/suites/proxy/test.ts index 5e4619d3948d..dc709f5251c6 100644 --- a/dev-packages/node-integration-tests/suites/proxy/test.ts +++ b/dev-packages/node-integration-tests/suites/proxy/test.ts @@ -1,4 +1,8 @@ -import { createRunner } from '../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); test('proxies sentry requests', done => { createRunner(__dirname, 'basic.js') diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts index 43c67c9c8b07..84c63a30ff68 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts @@ -1,7 +1,11 @@ import { conditionalTest } from '../../../utils'; -import { createRunner } from '../../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; conditionalTest({ min: 14 })('mysql auto instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + test('should auto-instrument `mysql` package when using connection.connect()', done => { const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml new file mode 100644 index 000000000000..71ea54ad7e70 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml @@ -0,0 +1,9 @@ +services: + db: + image: mysql:8 + restart: always + container_name: integration-tests-mysql + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: password diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js new file mode 100644 index 000000000000..8858e4ef587f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-experimental'); + +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 mysql = require('mysql2/promise'); + +mysql + .createConnection({ + user: 'root', + password: 'password', + host: 'localhost', + port: 3306, + }) + .then(connection => { + return Sentry.startSpan( + { + op: 'transaction', + name: 'Test Transaction', + }, + async _ => { + await connection.query('SELECT 1 + 1 AS solution'); + await connection.query('SELECT NOW()', ['1', '2']); + }, + ); + }); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts new file mode 100644 index 000000000000..28209009b03e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts @@ -0,0 +1,41 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 14 })('mysql2 auto instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should auto-instrument `mysql` package without connection.connect()', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'SELECT 1 + 1 AS solution', + op: 'db', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'net.peer.name': 'localhost', + 'net.peer.port': 3306, + 'db.user': 'root', + }), + }), + expect.objectContaining({ + description: 'SELECT NOW()', + op: 'db', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'net.peer.name': 'localhost', + 'net.peer.port': 3306, + 'db.user': 'root', + }), + }), + ]), + }; + + createRunner(__dirname, 'scenario.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 31969452ba74..515a2627acf8 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,5 +1,4 @@ -import type { ChildProcess } from 'child_process'; -import { spawn } from 'child_process'; +import { spawn, spawnSync } from 'child_process'; import { join } from 'path'; import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types'; import axios from 'axios'; @@ -30,14 +29,17 @@ export function assertSentryTransaction(actual: Event, expected: Partial) }); } -const CHILD_PROCESSES = new Set(); +const CLEANUP_STEPS = new Set(); export function cleanupChildProcesses(): void { - for (const child of CHILD_PROCESSES) { - child.kill(); + for (const step of CLEANUP_STEPS) { + step(); } + CLEANUP_STEPS.clear(); } +process.on('exit', cleanupChildProcesses); + /** Promise only resolves when fn returns true */ async function waitFor(fn: () => boolean, timeout = 10_000): Promise { let remaining = timeout; @@ -50,6 +52,58 @@ async function waitFor(fn: () => boolean, timeout = 10_000): Promise { } } +type VoidFunction = () => void; + +interface DockerOptions { + /** + * The working directory to run docker compose in + */ + workingDirectory: string[]; + /** + * The strings to look for in the output to know that the docker compose is ready for the test to be run + */ + readyMatches: string[]; +} + +/** + * Runs docker compose up and waits for the readyMatches to appear in the output + * + * Returns a function that can be called to docker compose down + */ +async function runDockerCompose(options: DockerOptions): Promise { + return new Promise((resolve, reject) => { + const cwd = join(...options.workingDirectory); + const close = (): void => { + spawnSync('docker', ['compose', 'down', '--volumes'], { cwd }); + }; + + // ensure we're starting fresh + close(); + + const child = spawn('docker', ['compose', 'up'], { cwd }); + + const timeout = setTimeout(() => { + close(); + reject(new Error('Timed out waiting for docker-compose')); + }, 60_000); + + function newData(data: Buffer): void { + const text = data.toString('utf8'); + + for (const match of options.readyMatches) { + if (text.includes(match)) { + child.stdout.removeAllListeners(); + clearTimeout(timeout); + resolve(close); + } + } + } + + child.stdout.on('data', newData); + child.stderr.on('data', newData); + }); +} + type Expected = | { event: Partial | ((event: Event) => void); @@ -70,6 +124,7 @@ export function createRunner(...paths: string[]) { const flags: string[] = []; const ignored: EnvelopeItemType[] = []; let withSentryServer = false; + let dockerOptions: DockerOptions | undefined; let ensureNoErrorOutput = false; if (testPath.endsWith('.ts')) { @@ -93,6 +148,10 @@ export function createRunner(...paths: string[]) { ignored.push(...types); return this; }, + withDockerCompose: function (options: DockerOptions) { + dockerOptions = options; + return this; + }, ensureNoErrorOutput: function () { ensureNoErrorOutput = true; return this; @@ -182,80 +241,94 @@ export function createRunner(...paths: string[]) { ? createBasicSentryServer(newEnvelope) : Promise.resolve(undefined); + const dockerStartup: Promise = dockerOptions + ? runDockerCompose(dockerOptions) + : Promise.resolve(undefined); + + const startup = Promise.all([dockerStartup, serverStartup]); + // eslint-disable-next-line @typescript-eslint/no-floating-promises - serverStartup.then(mockServerPort => { - const env = mockServerPort - ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } - : process.env; + startup + .then(([dockerChild, mockServerPort]) => { + if (dockerChild) { + CLEANUP_STEPS.add(dockerChild); + } - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); + const env = mockServerPort + ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } + : process.env; - child = spawn('node', [...flags, testPath], { env }); + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); - CHILD_PROCESSES.add(child); + child = spawn('node', [...flags, testPath], { env }); - if (ensureNoErrorOutput) { - child.stderr.on('data', (data: Buffer) => { - const output = data.toString(); - complete(new Error(`Expected no error output but got: '${output}'`)); + CLEANUP_STEPS.add(() => { + child?.kill(); }); - } - - child.on('close', () => { - hasExited = true; if (ensureNoErrorOutput) { - complete(); + child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + complete(new Error(`Expected no error output but got: '${output}'`)); + }); } - }); - // Pass error to done to end the test quickly - child.on('error', e => { - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('scenario error', e); - complete(e); - }); - - function tryParseEnvelopeFromStdoutLine(line: string): void { - // Lines can have leading '[something] [{' which we need to remove - const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); - - // See if we have a port message - if (cleanedLine.startsWith('{"port":')) { - const { port } = JSON.parse(cleanedLine) as { port: number }; - scenarioServerPort = port; - return; - } + child.on('close', () => { + hasExited = true; - // Skip any lines that don't start with envelope JSON - if (!cleanedLine.startsWith('[{')) { - return; - } + if (ensureNoErrorOutput) { + complete(); + } + }); - try { - const envelope = JSON.parse(cleanedLine) as Envelope; - newEnvelope(envelope); - } catch (_) { - // - } - } + // Pass error to done to end the test quickly + child.on('error', e => { + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('scenario error', e); + complete(e); + }); - let buffer = Buffer.alloc(0); - child.stdout.on('data', (data: Buffer) => { - // This is horribly memory inefficient but it's only for tests - buffer = Buffer.concat([buffer, data]); + function tryParseEnvelopeFromStdoutLine(line: string): void { + // Lines can have leading '[something] [{' which we need to remove + const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); - let splitIndex = -1; - while ((splitIndex = buffer.indexOf(0xa)) >= 0) { - const line = buffer.subarray(0, splitIndex).toString(); - buffer = Buffer.from(buffer.subarray(splitIndex + 1)); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('line', line); - tryParseEnvelopeFromStdoutLine(line); + // See if we have a port message + if (cleanedLine.startsWith('{"port":')) { + const { port } = JSON.parse(cleanedLine) as { port: number }; + scenarioServerPort = port; + return; + } + + // Skip any lines that don't start with envelope JSON + if (!cleanedLine.startsWith('[{')) { + return; + } + + try { + const envelope = JSON.parse(cleanedLine) as Envelope; + newEnvelope(envelope); + } catch (_) { + // + } } - }); - }); + + let buffer = Buffer.alloc(0); + child.stdout.on('data', (data: Buffer) => { + // This is horribly memory inefficient but it's only for tests + buffer = Buffer.concat([buffer, data]); + + let splitIndex = -1; + while ((splitIndex = buffer.indexOf(0xa)) >= 0) { + const line = buffer.subarray(0, splitIndex).toString(); + buffer = Buffer.from(buffer.subarray(splitIndex + 1)); + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('line', line); + tryParseEnvelopeFromStdoutLine(line); + } + }); + }) + .catch(e => complete(e)); return { childHasExited: function (): boolean { diff --git a/yarn.lock b/yarn.lock index 12799a710b48..ecbf1157aa3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12744,6 +12744,11 @@ denque@^1.4.1: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -16129,6 +16134,13 @@ gcp-metadata@^4.2.0: gaxios "^4.0.0" json-bigint "^1.0.0" +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -18407,6 +18419,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + is-reference@1.2.1, is-reference@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -20640,6 +20657,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -20708,6 +20730,16 @@ lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-cache@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" + integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== + lru-cache@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83" @@ -22331,6 +22363,20 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +mysql2@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.7.1.tgz#bb088fa3f01deefbfe04adaf0d3ec18571b33410" + integrity sha512-4EEqYu57mnkW5+Bvp5wBebY7PpfyrmvJ3knHcmLkp8FyBu4kqgrF2GxIjsC2tbLNZWqJaL21v/MYH7bU5f03oA== + dependencies: + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru-cache "^8.0.0" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + mysql@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" @@ -22350,6 +22396,13 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + nan@^2.12.1: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" @@ -27953,6 +28006,11 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -28813,6 +28871,11 @@ sqlstring@2.3.1: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + sri-toolbox@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e"