diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index 7461f88b98c..f2257ded0a4 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -13,7 +13,7 @@ variables: tags: ["runner:apm-k8s-tweaked-metal"] image: $MICROBENCHMARKS_CI_IMAGE interruptible: true - timeout: 15m + timeout: 20m script: - git clone --branch dd-trace-js https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/benchmarking-platform platform && cd platform - bp-runner bp-runner.yml --debug diff --git a/benchmark/sirun/plugin-graphql/meta.json b/benchmark/sirun/plugin-graphql/meta.json index 0b5003978dc..ecc7c6cee61 100644 --- a/benchmark/sirun/plugin-graphql/meta.json +++ b/benchmark/sirun/plugin-graphql/meta.json @@ -3,7 +3,7 @@ "run": "node index.js", "run_with_affinity": "bash -c \"taskset -c $CPU_AFFINITY node index.js\"", "cachegrind": false, - "iterations": 2, + "iterations": 30, "instructions": true, "variants": { "control": {}, diff --git a/benchmark/sirun/plugin-graphql/schema.js b/benchmark/sirun/plugin-graphql/schema.js index d7da47739b9..ea748c5bb79 100644 --- a/benchmark/sirun/plugin-graphql/schema.js +++ b/benchmark/sirun/plugin-graphql/schema.js @@ -94,7 +94,7 @@ const Human = new graphql.GraphQLObjectType({ async resolve (obj, args) { const promises = [] - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { promises.push(await Promise.resolve({})) } @@ -115,7 +115,7 @@ const schema = new graphql.GraphQLSchema({ async resolve (obj, args) { const promises = [] - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 20; i++) { promises.push(await Promise.resolve({})) } diff --git a/docker-compose.yml b/docker-compose.yml index 81bdd3c2032..cebd93ba020 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,7 @@ services: ports: - "127.0.0.1:6379:6379" mongo: - image: circleci/mongo:3.6 + image: circleci/mongo:4.4 platform: linux/amd64 ports: - "127.0.0.1:27017:27017" diff --git a/eslint.config.mjs b/eslint.config.mjs index e572ce6e2bd..2ac7bbc98fc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,9 +41,10 @@ export default [ '**/versions', // This is effectively a node_modules tree. '**/acmeair-nodejs', // We don't own this. '**/vendor', // Generally, we didn't author this code. + 'integration-tests/debugger/target-app/source-map-support/minify.min.js', // Generated + 'integration-tests/debugger/target-app/source-map-support/typescript.js', // Generated 'integration-tests/esbuild/out.js', // Generated 'integration-tests/esbuild/aws-sdk-out.js', // Generated - 'packages/dd-trace/src/appsec/blocked_templates.js', // TODO Why is this ignored? 'packages/dd-trace/src/payload-tagging/jsonpath-plus.js' // Vendored ] }, diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index e00cd6c4fc3..f4b9d443be2 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -12,31 +12,43 @@ const DEFAULT_SETTINGS = { code_coverage: true, tests_skipping: true, itr_enabled: true, + require_git: false, early_flake_detection: { enabled: false, slow_test_retries: { '5s': 3 } + }, + flaky_test_retries_enabled: false, + di_enabled: false, + known_tests_enabled: false, + test_management: { + enabled: false } } const DEFAULT_SUITES_TO_SKIP = [] const DEFAULT_GIT_UPLOAD_STATUS = 200 -const DEFAULT_KNOWN_TESTS_UPLOAD_STATUS = 200 +const DEFAULT_KNOWN_TESTS_RESPONSE_STATUS = 200 const DEFAULT_INFO_RESPONSE = { endpoints: ['/evp_proxy/v2', '/debugger/v1/input'] } const DEFAULT_CORRELATION_ID = '1234' const DEFAULT_KNOWN_TESTS = ['test-suite1.js.test-name1', 'test-suite2.js.test-name2'] +const DEFAULT_QUARANTINED_TESTS = {} +const DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS = 200 + let settings = DEFAULT_SETTINGS let suitesToSkip = DEFAULT_SUITES_TO_SKIP let gitUploadStatus = DEFAULT_GIT_UPLOAD_STATUS let infoResponse = DEFAULT_INFO_RESPONSE let correlationId = DEFAULT_CORRELATION_ID let knownTests = DEFAULT_KNOWN_TESTS -let knownTestsStatusCode = DEFAULT_KNOWN_TESTS_UPLOAD_STATUS +let knownTestsStatusCode = DEFAULT_KNOWN_TESTS_RESPONSE_STATUS let waitingTime = 0 +let quarantineResponse = DEFAULT_QUARANTINED_TESTS +let quarantineResponseStatusCode = DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS class FakeCiVisIntake extends FakeAgent { setKnownTestsResponseCode (statusCode) { @@ -71,6 +83,14 @@ class FakeCiVisIntake extends FakeAgent { waitingTime = newWaitingTime } + setQuarantinedTests (newQuarantinedTests) { + quarantineResponse = newQuarantinedTests + } + + setQuarantinedTestsResponseCode (newStatusCode) { + quarantineResponseStatusCode = newStatusCode + } + async start () { const app = express() app.use(bodyParser.raw({ limit: Infinity, type: 'application/msgpack' })) @@ -219,6 +239,25 @@ class FakeCiVisIntake extends FakeAgent { }) }) + app.post([ + '/api/v2/test/libraries/test-management/tests', + '/evp_proxy/:version/api/v2/test/libraries/test-management/tests' + ], (req, res) => { + res.setHeader('content-type', 'application/json') + const data = JSON.stringify({ + data: { + attributes: { + modules: quarantineResponse + } + } + }) + res.status(quarantineResponseStatusCode).send(data) + this.emit('message', { + headers: req.headers, + url: req.url + }) + }) + return new Promise((resolve, reject) => { const timeoutObj = setTimeout(() => { reject(new Error('Intake timed out starting up')) @@ -237,8 +276,10 @@ class FakeCiVisIntake extends FakeAgent { settings = DEFAULT_SETTINGS suitesToSkip = DEFAULT_SUITES_TO_SKIP gitUploadStatus = DEFAULT_GIT_UPLOAD_STATUS - knownTestsStatusCode = DEFAULT_KNOWN_TESTS_UPLOAD_STATUS + knownTestsStatusCode = DEFAULT_KNOWN_TESTS_RESPONSE_STATUS infoResponse = DEFAULT_INFO_RESPONSE + quarantineResponseStatusCode = DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS + quarantineResponse = DEFAULT_QUARANTINED_TESTS this.removeAllListeners() if (this.waitingTimeoutId) { clearTimeout(this.waitingTimeoutId) diff --git a/integration-tests/ci-visibility/features-quarantine/quarantine.feature b/integration-tests/ci-visibility/features-quarantine/quarantine.feature new file mode 100644 index 00000000000..d837149878a --- /dev/null +++ b/integration-tests/ci-visibility/features-quarantine/quarantine.feature @@ -0,0 +1,4 @@ +Feature: Quarantine + Scenario: Say quarantine + When the greeter says quarantine + Then I should have heard "quarantine" diff --git a/integration-tests/ci-visibility/features-quarantine/support/steps.js b/integration-tests/ci-visibility/features-quarantine/support/steps.js new file mode 100644 index 00000000000..86b1a3aa9b6 --- /dev/null +++ b/integration-tests/ci-visibility/features-quarantine/support/steps.js @@ -0,0 +1,10 @@ +const assert = require('assert') +const { When, Then } = require('@cucumber/cucumber') + +Then('I should have heard {string}', function (expectedResponse) { + assert.equal(this.whatIHeard, 'fail') +}) + +When('the greeter says quarantine', function () { + this.whatIHeard = 'quarantine' +}) diff --git a/integration-tests/ci-visibility/playwright-tests-quarantine/quarantine-test.js b/integration-tests/ci-visibility/playwright-tests-quarantine/quarantine-test.js new file mode 100644 index 00000000000..69287e98ecb --- /dev/null +++ b/integration-tests/ci-visibility/playwright-tests-quarantine/quarantine-test.js @@ -0,0 +1,13 @@ +const { test, expect } = require('@playwright/test') + +test.beforeEach(async ({ page }) => { + await page.goto(process.env.PW_BASE_URL) +}) + +test.describe('quarantine', () => { + test('should quarantine failed test', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello Warld' + ]) + }) +}) diff --git a/integration-tests/ci-visibility/quarantine/test-quarantine-1.js b/integration-tests/ci-visibility/quarantine/test-quarantine-1.js new file mode 100644 index 00000000000..c75cb4c5b75 --- /dev/null +++ b/integration-tests/ci-visibility/quarantine/test-quarantine-1.js @@ -0,0 +1,11 @@ +const { expect } = require('chai') + +describe('quarantine tests', () => { + it('can quarantine a test', () => { + expect(1 + 2).to.equal(4) + }) + + it('can pass normally', () => { + expect(1 + 2).to.equal(3) + }) +}) diff --git a/integration-tests/ci-visibility/quarantine/test-quarantine-2.js b/integration-tests/ci-visibility/quarantine/test-quarantine-2.js new file mode 100644 index 00000000000..f94386f1b87 --- /dev/null +++ b/integration-tests/ci-visibility/quarantine/test-quarantine-2.js @@ -0,0 +1,11 @@ +const { expect } = require('chai') + +describe('quarantine tests 2', () => { + it('can quarantine a test', () => { + expect(1 + 2).to.equal(3) + }) + + it('can pass normally', () => { + expect(1 + 2).to.equal(3) + }) +}) diff --git a/integration-tests/ci-visibility/run-jest.js b/integration-tests/ci-visibility/run-jest.js index a1f236be7a2..0a8b7b47ce6 100644 --- a/integration-tests/ci-visibility/run-jest.js +++ b/integration-tests/ci-visibility/run-jest.js @@ -31,8 +31,12 @@ if (process.env.COLLECT_COVERAGE_FROM) { jest.runCLI( options, options.projects -).then(() => { +).then((results) => { if (process.send) { process.send('finished') } + if (process.env.SHOULD_CHECK_RESULTS) { + const exitCode = results.results.success ? 0 : 1 + process.exit(exitCode) + } }) diff --git a/integration-tests/ci-visibility/run-mocha.js b/integration-tests/ci-visibility/run-mocha.js index fc767f4051f..19d009ca9a2 100644 --- a/integration-tests/ci-visibility/run-mocha.js +++ b/integration-tests/ci-visibility/run-mocha.js @@ -12,11 +12,14 @@ if (process.env.TESTS_TO_RUN) { mocha.addFile(require.resolve('./test/ci-visibility-test.js')) mocha.addFile(require.resolve('./test/ci-visibility-test-2.js')) } -mocha.run(() => { +mocha.run((failures) => { if (process.send) { process.send('finished') } -}).on('end', () => { + if (process.env.SHOULD_CHECK_RESULTS && failures > 0) { + process.exit(1) + } +}).on('end', (res) => { // eslint-disable-next-line console.log('end event: can add event listeners to mocha') }) diff --git a/integration-tests/ci-visibility/vitest-tests/test-quarantine.mjs b/integration-tests/ci-visibility/vitest-tests/test-quarantine.mjs new file mode 100644 index 00000000000..d48e61fe64d --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/test-quarantine.mjs @@ -0,0 +1,11 @@ +import { describe, test, expect } from 'vitest' + +describe('quarantine tests', () => { + test('can quarantine a test', () => { + expect(1 + 2).to.equal(4) + }) + + test('can pass normally', () => { + expect(1 + 2).to.equal(3) + }) +}) diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index 3dfef057fd1..64ab0108c0e 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -43,7 +43,9 @@ const { DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -1585,7 +1587,7 @@ versions.forEach(version => { }) // Dynamic instrumentation only supported from >=8.0.0 context('dynamic instrumentation', () => { - it('does not activate if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + it('does not activate if DD_TEST_FAILED_TEST_REPLAY_ENABLED is set to false', (done) => { receiver.setSettings({ flaky_test_retries_enabled: true, di_enabled: true @@ -1619,7 +1621,10 @@ versions.forEach(version => { './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', { cwd, - env: envVars, + env: { + ...envVars, + DD_TEST_FAILED_TEST_REPLAY_ENABLED: 'false' + }, stdio: 'pipe' } ) @@ -1664,10 +1669,7 @@ versions.forEach(version => { './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', { cwd, - env: { - ...envVars, - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' - }, + env: envVars, stdio: 'pipe' } ) @@ -1746,10 +1748,7 @@ versions.forEach(version => { './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', { cwd, - env: { - ...envVars, - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' - }, + env: envVars, stdio: 'pipe' } ) @@ -1798,10 +1797,7 @@ versions.forEach(version => { './node_modules/.bin/cucumber-js ci-visibility/features-di/test-not-hit-breakpoint.feature --retry 1', { cwd, - env: { - ...envVars, - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' - }, + env: envVars, stdio: 'pipe' } ) @@ -2029,5 +2025,94 @@ versions.forEach(version => { }).catch(done) }) }) + + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + cucumber: { + suites: { + 'ci-visibility/features-quarantine/quarantine.feature': { + tests: { + 'Say quarantine': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const failedTest = events.find(event => event.type === 'test').content + const testSession = events.find(event => event.type === 'test_session_end').content + + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + assert.equal(failedTest.resource, 'ci-visibility/features-quarantine/quarantine.feature.Say quarantine') + + assert.equal(failedTest.meta[TEST_STATUS], 'fail') + if (isQuarantining) { + assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runTest = (done, isQuarantining, extraEnvVars) => { + const testAssertionsPromise = getTestAssertions(isQuarantining) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-quarantine/*.feature', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + ...extraEnvVars + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', exitCode => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + // even though a test fails, the exit code is 1 because the test is quarantined + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }).catch(done) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + }) }) }) diff --git a/integration-tests/cypress-esm-config.mjs b/integration-tests/cypress-esm-config.mjs index d6f4c2b8e95..1f27d834070 100644 --- a/integration-tests/cypress-esm-config.mjs +++ b/integration-tests/cypress-esm-config.mjs @@ -1,7 +1,7 @@ import cypress from 'cypress' async function runCypress () { - await cypress.run({ + const results = await cypress.run({ config: { defaultCommandTimeout: 1000, e2e: { @@ -39,6 +39,9 @@ async function runCypress () { screenshotOnRunFailure: false } }) + if (results.totalFailed !== 0) { + process.exit(1) + } } runCypress() diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index a2dd81a74f5..f5e071ace60 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -38,7 +38,9 @@ const { TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_IS_QUARANTINED, + TEST_MANAGEMENT_ENABLED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -1731,5 +1733,104 @@ moduleTypes.forEach(({ }).catch(done) }) }) + + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + cypress: { + suites: { + 'cypress/e2e/quarantine.js': { + tests: { + 'quarantine is quarantined': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const failedTest = events.find(event => event.type === 'test').content + const testSession = events.find(event => event.type === 'test_session_end').content + + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + assert.equal(failedTest.resource, 'cypress/e2e/quarantine.js.quarantine is quarantined') + + if (isQuarantining) { + // TODO: run instead of skipping, but ignore its result + assert.propertyVal(failedTest.meta, TEST_STATUS, 'skip') + assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.propertyVal(failedTest.meta, TEST_STATUS, 'fail') + assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runQuarantineTest = (done, isQuarantining, extraEnvVars) => { + const testAssertionsPromise = getTestAssertions(isQuarantining) + + const { + NODE_OPTIONS, + ...restEnvVars + } = getCiVisEvpProxyConfig(receiver.port) + + const specToRun = 'cypress/e2e/quarantine.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + SPEC_PATTERN: specToRun, + ...extraEnvVars + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', (exitCode) => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }).catch(done) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runQuarantineTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + }) }) }) diff --git a/integration-tests/cypress/e2e/quarantine.js b/integration-tests/cypress/e2e/quarantine.js new file mode 100644 index 00000000000..efbae41cd64 --- /dev/null +++ b/integration-tests/cypress/e2e/quarantine.js @@ -0,0 +1,8 @@ +/* eslint-disable */ +describe('quarantine', () => { + it('is quarantined', () => { + cy.visit('/') + .get('.hello-world') + .should('have.text', 'Hello Warld') + }) +}) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index f51278bc2ee..d8d9debea25 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -535,7 +535,7 @@ function assertBasicInputPayload (t, payload) { service: 'node', message: 'Hello World!', logger: { - name: t.breakpoint.file, + name: t.breakpoint.deployedFile, method: 'fooHandler', version, thread_name: 'MainThread' @@ -544,7 +544,7 @@ function assertBasicInputPayload (t, payload) { probe: { id: t.rcConfig.config.id, version: 0, - location: { file: t.breakpoint.file, lines: [String(t.breakpoint.line)] } + location: { file: t.breakpoint.deployedFile, lines: [String(t.breakpoint.line)] } }, language: 'javascript' } diff --git a/integration-tests/debugger/source-map-support.spec.js b/integration-tests/debugger/source-map-support.spec.js new file mode 100644 index 00000000000..f843d103bfe --- /dev/null +++ b/integration-tests/debugger/source-map-support.spec.js @@ -0,0 +1,50 @@ +'use strict' + +const { assert } = require('chai') +const { setup } = require('./utils') + +describe('Dynamic Instrumentation', function () { + describe('source map support', function () { + describe('Different file extention (TypeScript)', function () { + const t = setup({ + testApp: 'target-app/source-map-support/typescript.js', + testAppSource: 'target-app/source-map-support/typescript.ts' + }) + + beforeEach(t.triggerBreakpoint) + + it('should support source maps', function (done) { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { probe: { location } } }] }) => { + assert.deepEqual(location, { + file: 'target-app/source-map-support/typescript.ts', + lines: ['9'] + }) + done() + }) + + t.agent.addRemoteConfig(t.rcConfig) + }) + }) + + describe('Column information required (Minified)', function () { + const t = setup({ + testApp: 'target-app/source-map-support/minify.min.js', + testAppSource: 'target-app/source-map-support/minify.js' + }) + + beforeEach(t.triggerBreakpoint) + + it('should support source maps', function (done) { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { probe: { location } } }] }) => { + assert.deepEqual(location, { + file: 'target-app/source-map-support/minify.js', + lines: ['6'] + }) + done() + }) + + t.agent.addRemoteConfig(t.rcConfig) + }) + }) + }) +}) diff --git a/integration-tests/debugger/target-app/source-map-support/minify.js b/integration-tests/debugger/target-app/source-map-support/minify.js new file mode 100644 index 00000000000..2baf395873b --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/minify.js @@ -0,0 +1,11 @@ +require('dd-trace/init') + +const { createServer } = require('node:http') + +const server = createServer((req, res) => { + res.end('hello world') // BREAKPOINT: / +}) + +server.listen(process.env.APP_PORT, () => { + process.send?.({ port: process.env.APP_PORT }) +}) diff --git a/integration-tests/debugger/target-app/source-map-support/minify.min.js b/integration-tests/debugger/target-app/source-map-support/minify.min.js new file mode 100644 index 00000000000..782c1ebce15 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/minify.min.js @@ -0,0 +1,2 @@ +require("dd-trace/init");const{createServer}=require("node:http");const server=createServer((req,res)=>{res.end("hello world")});server.listen(process.env.APP_PORT,()=>{process.send?.({port:process.env.APP_PORT})}); +//# sourceMappingURL=minify.min.js.map \ No newline at end of file diff --git a/integration-tests/debugger/target-app/source-map-support/minify.min.js.map b/integration-tests/debugger/target-app/source-map-support/minify.min.js.map new file mode 100644 index 00000000000..b3737180fb7 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/minify.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["integration-tests/debugger/target-app/source-map-support/minify.js"],"names":["require","createServer","server","req","res","end","listen","process","env","APP_PORT","send","port"],"mappings":"AAAAA,QAAQ,eAAe,EAEvB,KAAM,CAAEC,YAAa,EAAID,QAAQ,WAAW,EAE5C,MAAME,OAASD,aAAa,CAACE,IAAKC,OAChCA,IAAIC,IAAI,aAAa,CACvB,CAAC,EAEDH,OAAOI,OAAOC,QAAQC,IAAIC,SAAU,KAClCF,QAAQG,OAAO,CAAEC,KAAMJ,QAAQC,IAAIC,QAAS,CAAC,CAC/C,CAAC"} \ No newline at end of file diff --git a/integration-tests/debugger/target-app/source-map-support/scripts/build-minifiy.sh b/integration-tests/debugger/target-app/source-map-support/scripts/build-minifiy.sh new file mode 100755 index 00000000000..c2da767802f --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/scripts/build-minifiy.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +npx uglify-js integration-tests/debugger/target-app/source-map-support/minify.js \ + -o integration-tests/debugger/target-app/source-map-support/minify.min.js \ + --v8 \ + --source-map url=minify.min.js.map diff --git a/integration-tests/debugger/target-app/source-map-support/scripts/build-typescript.sh b/integration-tests/debugger/target-app/source-map-support/scripts/build-typescript.sh new file mode 100755 index 00000000000..e2bf9a5ab30 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/scripts/build-typescript.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +npx --package=typescript -- tsc --sourceMap integration-tests/debugger/target-app/source-map-support/typescript.ts diff --git a/integration-tests/debugger/target-app/source-map-support/typescript.js b/integration-tests/debugger/target-app/source-map-support/typescript.js new file mode 100644 index 00000000000..de7a4b5e972 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/typescript.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +require('dd-trace/init'); +var node_http_1 = require("node:http"); +var server = (0, node_http_1.createServer)(function (req, res) { + // Blank lines below to ensure line numbers in transpiled file differ from original file + res.end('hello world'); // BREAKPOINT: / +}); +server.listen(process.env.APP_PORT, function () { + var _a; + (_a = process.send) === null || _a === void 0 ? void 0 : _a.call(process, { port: process.env.APP_PORT }); +}); +//# sourceMappingURL=typescript.js.map \ No newline at end of file diff --git a/integration-tests/debugger/target-app/source-map-support/typescript.js.map b/integration-tests/debugger/target-app/source-map-support/typescript.js.map new file mode 100644 index 00000000000..0f09d937224 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/typescript.js.map @@ -0,0 +1 @@ +{"version":3,"file":"typescript.js","sourceRoot":"","sources":["typescript.ts"],"names":[],"mappings":";;AAAA,OAAO,CAAC,eAAe,CAAC,CAAA;AAExB,uCAAwC;AAExC,IAAM,MAAM,GAAG,IAAA,wBAAY,EAAC,UAAC,GAAG,EAAE,GAAG;IACnC,wFAAwF;IAGxF,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA,CAAC,gBAAgB;AACzC,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE;;IAClC,MAAA,OAAO,CAAC,IAAI,wDAAG,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;AAChD,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/integration-tests/debugger/target-app/source-map-support/typescript.ts b/integration-tests/debugger/target-app/source-map-support/typescript.ts new file mode 100644 index 00000000000..a11c267f708 --- /dev/null +++ b/integration-tests/debugger/target-app/source-map-support/typescript.ts @@ -0,0 +1,14 @@ +require('dd-trace/init') + +import { createServer } from 'node:http' + +const server = createServer((req, res) => { + // Blank lines below to ensure line numbers in transpiled file differ from original file + + + res.end('hello world') // BREAKPOINT: / +}) + +server.listen(process.env.APP_PORT, () => { + process.send?.({ port: process.env.APP_PORT }) +}) diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index 9f5175d84fc..f273dcfca2a 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -18,9 +18,13 @@ module.exports = { setup } -function setup ({ env, testApp } = {}) { +function setup ({ env, testApp, testAppSource } = {}) { let sandbox, cwd, appPort - const breakpoints = getBreakpointInfo({ file: testApp, stackIndex: 1 }) // `1` to disregard the `setup` function + const breakpoints = getBreakpointInfo({ + deployedFile: testApp, + sourceFile: testAppSource, + stackIndex: 1 // `1` to disregard the `setup` function + }) const t = { breakpoint: breakpoints[0], breakpoints, @@ -71,7 +75,7 @@ function setup ({ env, testApp } = {}) { sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic cwd = sandbox.folder // The sandbox uses the `integration-tests` folder as its root - t.appFile = join(cwd, 'debugger', breakpoints[0].file) + t.appFile = join(cwd, 'debugger', breakpoints[0].deployedFile) }) after(async function () { @@ -110,8 +114,8 @@ function setup ({ env, testApp } = {}) { return t } -function getBreakpointInfo ({ file, stackIndex = 0 }) { - if (!file) { +function getBreakpointInfo ({ deployedFile, sourceFile = deployedFile, stackIndex = 0 } = {}) { + if (!deployedFile) { // First, get the filename of file that called this function const testFile = new Error().stack .split('\n')[stackIndex + 2] // +2 to skip this function + the first line, which is the error message @@ -120,17 +124,17 @@ function getBreakpointInfo ({ file, stackIndex = 0 }) { .split(':')[0] // Then, find the corresponding file in which the breakpoint(s) exists - file = join('target-app', basename(testFile).replace('.spec', '')) + deployedFile = sourceFile = join('target-app', basename(testFile).replace('.spec', '')) } // Finally, find the line number(s) of the breakpoint(s) - const lines = readFileSync(join(__dirname, file), 'utf8').split('\n') + const lines = readFileSync(join(__dirname, sourceFile), 'utf8').split('\n') const result = [] for (let i = 0; i < lines.length; i++) { const index = lines[i].indexOf(BREAKPOINT_TOKEN) if (index !== -1) { const url = lines[i].slice(index + BREAKPOINT_TOKEN.length + 1).trim() - result.push({ file, line: i + 1, url }) + result.push({ sourceFile, deployedFile, line: i + 1, url }) } } diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 35413ea7e60..4f495d3bdc5 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -40,7 +40,9 @@ const { DI_DEBUG_ERROR_FILE_SUFFIX, DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -507,7 +509,7 @@ describe('jest CommonJS', () => { }).catch(done) }) - it('can work with Dynamic Instrumentation', (done) => { + it('can work with Failed Test Replay', (done) => { receiver.setSettings({ flaky_test_retries_enabled: true, di_enabled: true @@ -563,7 +565,6 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', RUN_IN_PARALLEL: true }, @@ -2535,7 +2536,7 @@ describe('jest CommonJS', () => { }) context('dynamic instrumentation', () => { - it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + it('does not activate dynamic instrumentation if DD_TEST_FAILED_TEST_REPLAY_ENABLED is set to false', (done) => { receiver.setSettings({ flaky_test_retries_enabled: true, di_enabled: true @@ -2569,7 +2570,8 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', + DD_TEST_FAILED_TEST_REPLAY_ENABLED: 'false' }, stdio: 'inherit' } @@ -2616,7 +2618,6 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2701,7 +2702,6 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2751,7 +2751,6 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-not-hit-breakpoint', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2791,7 +2790,6 @@ describe('jest CommonJS', () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', TEST_SHOULD_PASS_AFTER_RETRY: '1' }, @@ -2938,4 +2936,130 @@ describe('jest CommonJS', () => { }).catch(done) }) }) + + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + jest: { + suites: { + 'ci-visibility/quarantine/test-quarantine-1.js': { + tests: { + 'quarantine tests can quarantine a test': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining, isParallel) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const testSession = events.find(event => event.type === 'test_session_end').content + + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + const resourceNames = tests.map(span => span.resource) + + assert.includeMembers(resourceNames, + [ + 'ci-visibility/quarantine/test-quarantine-1.js.quarantine tests can quarantine a test', + 'ci-visibility/quarantine/test-quarantine-1.js.quarantine tests can pass normally' + ] + ) + + if (isParallel) { + // Parallel mode in jest requires more than a single test suite + // Here we check that the second test suite is actually running, so we can be sure that parallel mode is on + assert.includeMembers(resourceNames, [ + 'ci-visibility/quarantine/test-quarantine-2.js.quarantine tests 2 can quarantine a test', + 'ci-visibility/quarantine/test-quarantine-2.js.quarantine tests 2 can pass normally' + ]) + } + + const failedTest = tests.find( + test => test.meta[TEST_NAME] === 'quarantine tests can quarantine a test' + ) + assert.equal(failedTest.meta[TEST_STATUS], 'fail') + + if (isQuarantining) { + assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runQuarantineTest = (done, isQuarantining, extraEnvVars = {}, isParallel = false) => { + const testAssertionsPromise = getTestAssertions(isQuarantining, isParallel) + + childProcess = exec( + runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'quarantine/test-quarantine-1', + SHOULD_CHECK_RESULTS: '1', + ...extraEnvVars + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', exitCode => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + // even though a test fails, the exit code is 1 because the test is quarantined + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }).catch(done) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runQuarantineTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + + it('can quarantine in parallel mode', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest( + done, + true, + { + // we need to run more than 1 suite for parallel mode to kick in + TESTS_TO_RUN: 'quarantine/test-quarantine', + RUN_IN_PARALLEL: true + }, + true + ) + }) + }) }) diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 86d9491b3f0..94bea3eaaaa 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -42,7 +42,9 @@ const { DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -2207,7 +2209,7 @@ describe('mocha CommonJS', function () { }) context('dynamic instrumentation', () => { - it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + it('does not activate dynamic instrumentation if DD_TEST_FAILED_TEST_REPLAY_ENABLED is set to false', (done) => { receiver.setSettings({ flaky_test_retries_enabled: true, di_enabled: true @@ -2245,7 +2247,8 @@ describe('mocha CommonJS', function () { TESTS_TO_RUN: JSON.stringify([ './dynamic-instrumentation/test-hit-breakpoint' ]), - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', + DD_TEST_FAILED_TEST_REPLAY_ENABLED: 'false' }, stdio: 'inherit' } @@ -2297,7 +2300,6 @@ describe('mocha CommonJS', function () { TESTS_TO_RUN: JSON.stringify([ './dynamic-instrumentation/test-hit-breakpoint' ]), - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2387,7 +2389,6 @@ describe('mocha CommonJS', function () { TESTS_TO_RUN: JSON.stringify([ './dynamic-instrumentation/test-hit-breakpoint' ]), - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2441,7 +2442,6 @@ describe('mocha CommonJS', function () { TESTS_TO_RUN: JSON.stringify([ './dynamic-instrumentation/test-not-hit-breakpoint' ]), - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' @@ -2557,4 +2557,108 @@ describe('mocha CommonJS', function () { }).catch(done) }) }) + + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + mocha: { + suites: { + 'ci-visibility/quarantine/test-quarantine-1.js': { + tests: { + 'quarantine tests can quarantine a test': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const testSession = events.find(event => event.type === 'test_session_end').content + + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + const resourceNames = tests.map(span => span.resource) + + assert.includeMembers(resourceNames, + [ + 'ci-visibility/quarantine/test-quarantine-1.js.quarantine tests can quarantine a test', + 'ci-visibility/quarantine/test-quarantine-1.js.quarantine tests can pass normally' + ] + ) + + const failedTest = tests.find( + test => test.meta[TEST_NAME] === 'quarantine tests can quarantine a test' + ) + // The test fails but the exit code is 0 if it's quarantined + assert.equal(failedTest.meta[TEST_STATUS], 'fail') + + if (isQuarantining) { + assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runQuarantineTest = (done, isQuarantining, extraEnvVars = {}) => { + const testAssertionsPromise = getTestAssertions(isQuarantining) + + childProcess = exec( + runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './quarantine/test-quarantine-1.js' + ]), + SHOULD_CHECK_RESULTS: '1', + ...extraEnvVars + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (exitCode) => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }).catch(done) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runQuarantineTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + }) }) diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 03ff3accd0d..023381978ee 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -17,7 +17,8 @@ const { TEST_SOURCE_START, TEST_TYPE, TEST_SOURCE_FILE, - TEST_CONFIGURATION_BROWSER_NAME, + TEST_PARAMETERS, + TEST_BROWSER_NAME, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, @@ -26,7 +27,9 @@ const { TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -153,7 +156,12 @@ versions.forEach((version) => { assert.propertyVal(testEvent.content.meta, 'test.customtag', 'customvalue') assert.propertyVal(testEvent.content.meta, 'test.customtag2', 'customvalue2') // Adds the browser used - assert.propertyVal(testEvent.content.meta, TEST_CONFIGURATION_BROWSER_NAME, 'chromium') + assert.propertyVal(testEvent.content.meta, TEST_BROWSER_NAME, 'chromium') + assert.propertyVal( + testEvent.content.meta, + TEST_PARAMETERS, + JSON.stringify({ arguments: { browser: 'chromium' }, metadata: {} }) + ) assert.exists(testEvent.content.metrics[DD_HOST_CPU_COUNT]) }) @@ -880,5 +888,98 @@ versions.forEach((version) => { receiverPromise.then(() => done()).catch(done) }) }) + + if (version === 'latest') { + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + playwright: { + suites: { + 'quarantine-test.js': { + tests: { + 'quarantine should quarantine failed test': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const testSession = events.find(event => event.type === 'test_session_end').content + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + const failedTest = events.find(event => event.type === 'test').content + + if (isQuarantining) { + // TODO: manage to run the test + assert.equal(failedTest.meta[TEST_STATUS], 'skip') + assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.equal(failedTest.meta[TEST_STATUS], 'fail') + assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runQuarantineTest = (done, isQuarantining, extraEnvVars) => { + const testAssertionsPromise = getTestAssertions(isQuarantining) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + PW_BASE_URL: `http://localhost:${webAppPort}`, + TEST_DIR: './ci-visibility/playwright-tests-quarantine', + ...extraEnvVars + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', (exitCode) => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }).catch(done) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runQuarantineTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + }) + } }) }) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index 56f060ce509..acdfa121b09 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -31,7 +31,9 @@ const { DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -985,7 +987,7 @@ versions.forEach((version) => { // dynamic instrumentation only supported from >=2.0.0 if (version === 'latest') { context('dynamic instrumentation', () => { - it('does not activate it if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + it('does not activate it if DD_TEST_FAILED_TEST_REPLAY_ENABLED is set to false', (done) => { receiver.setSettings({ flaky_test_retries_enabled: true, di_enabled: true @@ -1023,7 +1025,8 @@ versions.forEach((version) => { env: { ...getCiVisAgentlessConfig(receiver.port), TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', - NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_FAILED_TEST_REPLAY_ENABLED: 'false' }, stdio: 'pipe' } @@ -1073,8 +1076,7 @@ versions.forEach((version) => { env: { ...getCiVisAgentlessConfig(receiver.port), TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', - NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' }, stdio: 'pipe' } @@ -1160,8 +1162,7 @@ versions.forEach((version) => { env: { ...getCiVisAgentlessConfig(receiver.port), TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', - NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' }, stdio: 'pipe' } @@ -1215,8 +1216,7 @@ versions.forEach((version) => { env: { ...getCiVisAgentlessConfig(receiver.port), TEST_DIR: 'ci-visibility/vitest-tests/breakpoint-not-hit*', - NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' }, stdio: 'pipe' } @@ -1334,5 +1334,113 @@ versions.forEach((version) => { }).catch(done) }) }) + + if (version === 'latest') { + context('quarantine', () => { + beforeEach(() => { + receiver.setQuarantinedTests({ + vitest: { + suites: { + 'ci-visibility/vitest-tests/test-quarantine.mjs': { + tests: { + 'quarantine tests can quarantine a test': { + properties: { + quarantined: true + } + } + } + } + } + } + }) + }) + + const getTestAssertions = (isQuarantining) => + receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.equal(tests.length, 2) + + const testSession = events.find(event => event.type === 'test_session_end').content + + if (isQuarantining) { + assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true') + } else { + assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED) + } + + const resourceNames = tests.map(span => span.resource) + + assert.includeMembers(resourceNames, + [ + 'ci-visibility/vitest-tests/test-quarantine.mjs.quarantine tests can quarantine a test', + 'ci-visibility/vitest-tests/test-quarantine.mjs.quarantine tests can pass normally' + ] + ) + + const quarantinedTest = tests.find( + test => test.meta[TEST_NAME] === 'quarantine tests can quarantine a test' + ) + + if (isQuarantining) { + // TODO: do not flip the status of the test but still ignore failures + assert.equal(quarantinedTest.meta[TEST_STATUS], 'pass') + assert.propertyVal(quarantinedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } else { + assert.equal(quarantinedTest.meta[TEST_STATUS], 'fail') + assert.notProperty(quarantinedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED) + } + }) + + const runQuarantineTest = (done, isQuarantining, extraEnvVars = {}) => { + const testAssertionsPromise = getTestAssertions(isQuarantining) + + childProcess = exec( + './node_modules/.bin/vitest run', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/test-quarantine*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init --no-warnings', + ...extraEnvVars + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (exitCode) => { + testAssertionsPromise.then(() => { + if (isQuarantining) { + // exit code 0 even though one of the tests failed + assert.equal(exitCode, 0) + } else { + assert.equal(exitCode, 1) + } + done() + }) + }) + } + + it('can quarantine tests', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, true) + }) + + it('fails if quarantine is not enabled', (done) => { + receiver.setSettings({ test_management: { enabled: false } }) + + runQuarantineTest(done, false) + }) + + it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => { + receiver.setSettings({ test_management: { enabled: true } }) + + runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' }) + }) + }) + } }) }) diff --git a/package.json b/package.json index c0d9526f160..8fc24a40358 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js", "type:doc": "cd docs && yarn && yarn build", "type:test": "cd docs && yarn && yarn test", - "lint": "node scripts/check_licenses.js && eslint . && yarn audit", - "lint:fix": "node scripts/check_licenses.js && eslint . --fix && yarn audit", + "lint": "node scripts/check_licenses.js && eslint . --max-warnings 0 && yarn audit", + "lint:fix": "node scripts/check_licenses.js && eslint . --max-warnings 0 --fix && yarn audit", "release:proposal": "node scripts/release/proposal", "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services", "test": "SERVICES=* yarn services && mocha --expose-gc 'packages/dd-trace/test/setup/node.js' 'packages/*/test/**/*.spec.js'", @@ -131,7 +131,7 @@ "checksum": "^1.0.0", "cli-table3": "^0.6.3", "dotenv": "16.3.1", - "esbuild": "0.16.12", + "esbuild": "^0.25.0", "eslint": "^9.19.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.31.0", diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 639f955cc56..a97b8842938 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -22,6 +22,7 @@ const knownTestsCh = channel('ci:cucumber:known-tests') const skippableSuitesCh = channel('ci:cucumber:test-suite:skippable') const sessionStartCh = channel('ci:cucumber:session:start') const sessionFinishCh = channel('ci:cucumber:session:finish') +const quarantinedTestsCh = channel('ci:cucumber:quarantined-tests') const workerReportTraceCh = channel('ci:cucumber:worker-report:trace') @@ -71,6 +72,8 @@ let earlyFlakeDetectionFaultyThreshold = 0 let isEarlyFlakeDetectionFaulty = false let isFlakyTestRetriesEnabled = false let isKnownTestsEnabled = false +let isQuarantinedTestsEnabled = false +let quarantinedTests = {} let numTestRetries = 0 let knownTests = [] let skippedSuites = [] @@ -117,6 +120,17 @@ function isNewTest (testSuite, testName) { return !testsForSuite.includes(testName) } +function isQuarantinedTest (testSuite, testName) { + return quarantinedTests + ?.cucumber + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined +} + function getTestStatusFromRetries (testStatuses) { if (testStatuses.every(status => status === 'fail')) { return 'fail' @@ -293,12 +307,17 @@ function wrapRun (pl, isLatestVersion) { } let isNew = false let isEfdRetry = false + let isQuarantined = false if (isKnownTestsEnabled && status !== 'skip') { const numRetries = numRetriesByPickleId.get(this.pickle.id) isNew = numRetries !== undefined isEfdRetry = numRetries > 0 } + if (isQuarantinedTestsEnabled) { + const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd()) + isQuarantined = isQuarantinedTest(testSuitePath, this.pickle.name) + } const attemptAsyncResource = numAttemptToAsyncResource.get(numAttempt) const error = getErrorFromCucumberResult(result) @@ -307,7 +326,15 @@ function wrapRun (pl, isLatestVersion) { await promises.hitBreakpointPromise } attemptAsyncResource.runInAsyncScope(() => { - testFinishCh.publish({ status, skipReason, error, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0 }) + testFinishCh.publish({ + status, + skipReason, + error, + isNew, + isEfdRetry, + isFlakyRetry: numAttempt > 0, + isQuarantined + }) }) }) return promise @@ -396,6 +423,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin isFlakyTestRetriesEnabled = configurationResponse.libraryConfig?.isFlakyTestRetriesEnabled numTestRetries = configurationResponse.libraryConfig?.flakyTestRetriesCount isKnownTestsEnabled = configurationResponse.libraryConfig?.isKnownTestsEnabled + isQuarantinedTestsEnabled = configurationResponse.libraryConfig?.isQuarantinedTestsEnabled if (isKnownTestsEnabled) { const knownTestsResponse = await getChannelPromise(knownTestsCh) @@ -453,6 +481,15 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin } } + if (isQuarantinedTestsEnabled) { + const quarantinedTestsResponse = await getChannelPromise(quarantinedTestsCh) + if (!quarantinedTestsResponse.err) { + quarantinedTests = quarantinedTestsResponse.quarantinedTests + } else { + isQuarantinedTestsEnabled = false + } + } + const processArgv = process.argv.slice(2).join(' ') const command = process.env.npm_lifecycle_script || `cucumber-js ${processArgv}` @@ -500,6 +537,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin hasForcedToRunSuites: isForcedToRun, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, isParallel }) }) @@ -536,6 +574,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa } let isNew = false + let isQuarantined = false if (isKnownTestsEnabled) { isNew = isNewTest(testSuitePath, pickle.name) @@ -543,6 +582,10 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa numRetriesByPickleId.set(pickle.id, 0) } } + if (isQuarantinedTestsEnabled) { + isQuarantined = isQuarantinedTest(testSuitePath, pickle.name) + } + // TODO: for >=11 we could use `runTestCaseResult` instead of accumulating results in `lastStatusByPickleId` let runTestCaseResult = await runTestCaseFunction.apply(this, arguments) @@ -557,6 +600,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa } let testStatus = lastTestStatus let shouldBePassedByEFD = false + let shouldBePassedByQuarantine = false if (isNew && isEarlyFlakeDetectionEnabled) { /** * If Early Flake Detection (EFD) is enabled the logic is as follows: @@ -573,6 +617,11 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa } } + if (isQuarantinedTestsEnabled && isQuarantined) { + this.success = true + shouldBePassedByQuarantine = true + } + if (!pickleResultByFile[testFileAbsolutePath]) { pickleResultByFile[testFileAbsolutePath] = [testStatus] } else { @@ -604,6 +653,10 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa return shouldBePassedByEFD } + if (isNewerCucumberVersion && isQuarantinedTestsEnabled && isQuarantined) { + return shouldBePassedByQuarantine + } + return runTestCaseResult } } diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index d4f01cf7e5d..da31b18e6d1 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -43,6 +43,7 @@ const testErrCh = channel('ci:jest:test:err') const skippableSuitesCh = channel('ci:jest:test-suite:skippable') const libraryConfigurationCh = channel('ci:jest:library-configuration') const knownTestsCh = channel('ci:jest:known-tests') +const quarantinedTestsCh = channel('ci:jest:quarantined-tests') const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites') @@ -70,6 +71,8 @@ let earlyFlakeDetectionFaultyThreshold = 30 let isEarlyFlakeDetectionFaulty = false let hasFilteredSkippableSuites = false let isKnownTestsEnabled = false +let isQuarantinedTestsEnabled = false +let quarantinedTests = {} const sessionAsyncResource = new AsyncResource('bound-anonymous-fn') @@ -140,6 +143,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { this.flakyTestRetriesCount = this.testEnvironmentOptions._ddFlakyTestRetriesCount this.isDiEnabled = this.testEnvironmentOptions._ddIsDiEnabled this.isKnownTestsEnabled = this.testEnvironmentOptions._ddIsKnownTestsEnabled + this.isQuarantinedTestsEnabled = this.testEnvironmentOptions._ddIsQuarantinedTestsEnabled if (this.isKnownTestsEnabled) { try { @@ -161,6 +165,18 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { this.global[RETRY_TIMES] = this.flakyTestRetriesCount } } + + if (this.isQuarantinedTestsEnabled) { + try { + const hasQuarantinedTests = !!quarantinedTests.jest + this.quarantinedTestsForThisSuite = hasQuarantinedTests + ? this.getQuarantinedTestsForSuite(quarantinedTests.jest.suites[this.testSuite].tests) + : this.getQuarantinedTestsForSuite(this.testEnvironmentOptions._ddQuarantinedTests) + } catch (e) { + log.error('Error parsing quarantined tests', e) + this.isQuarantinedTestsEnabled = false + } + } } getHasSnapshotTests () { @@ -193,8 +209,25 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { return knownTestsForSuite } + getQuarantinedTestsForSuite (quaratinedTests) { + if (this.quarantinedTestsForThisSuite) { + return this.quarantinedTestsForThisSuite + } + let quarantinedTestsForSuite = quaratinedTests + // If jest is using workers, quarantined tests are serialized to json. + // If jest runs in band, they are not. + if (typeof quarantinedTestsForSuite === 'string') { + quarantinedTestsForSuite = JSON.parse(quarantinedTestsForSuite) + } + return Object.entries(quarantinedTestsForSuite).reduce((acc, [testName, { properties }]) => { + if (properties?.quarantined) { + acc.push(testName) + } + return acc + }, []) + } + // Add the `add_test` event we don't have the test object yet, so - // we use its describe block to get the full name getTestNameFromAddTestEvent (event, state) { const describeSuffix = getJestTestName(state.currentDescribeBlock) const fullTestName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName @@ -303,6 +336,12 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } } + let isQuarantined = false + + if (this.isQuarantinedTestsEnabled) { + const testName = getJestTestName(event.test) + isQuarantined = this.quarantinedTestsForThisSuite?.includes(testName) + } const promises = {} const numRetries = this.global[RETRY_TIMES] @@ -337,7 +376,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { testFinishCh.publish({ status, testStartLine: getTestLineStart(event.test.asyncError, this.testSuite), - promises + isQuarantined }) }) @@ -485,6 +524,7 @@ function cliWrapper (cli, jestVersion) { earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled + isQuarantinedTestsEnabled = libraryConfig.isQuarantinedTestsEnabled } } catch (err) { log.error('Jest library configuration error', err) @@ -532,6 +572,25 @@ function cliWrapper (cli, jestVersion) { } } + if (isQuarantinedTestsEnabled) { + const quarantinedTestsPromise = new Promise((resolve) => { + onDone = resolve + }) + + sessionAsyncResource.runInAsyncScope(() => { + quarantinedTestsCh.publish({ onDone }) + }) + + try { + const { err, quarantinedTests: receivedQuarantinedTests } = await quarantinedTestsPromise + if (!err) { + quarantinedTests = receivedQuarantinedTests + } + } catch (err) { + log.error('Jest quarantined tests error', err) + } + } + const processArgv = process.argv.slice(2).join(' ') sessionAsyncResource.runInAsyncScope(() => { testSessionStartCh.publish({ command: `jest ${processArgv}`, frameworkVersion: jestVersion }) @@ -601,6 +660,7 @@ function cliWrapper (cli, jestVersion) { error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, onDone }) }) @@ -634,6 +694,37 @@ function cliWrapper (cli, jestVersion) { } } + if (isQuarantinedTestsEnabled) { + const failedTests = result + .results + .testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => ( + testResults.map(({ fullName: testName, status }) => ({ testName, testSuiteAbsolutePath, status })) + )) + .filter(({ status }) => status === 'failed') + + let numFailedQuarantinedTests = 0 + + for (const { testName, testSuiteAbsolutePath } of failedTests) { + const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir) + const isQuarantined = quarantinedTests + ?.jest + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined + if (isQuarantined) { + numFailedQuarantinedTests++ + } + } + + // If every test that failed was quarantined, we'll consider the suite passed + if (numFailedQuarantinedTests !== 0 && result.results.numFailedTests === numFailedQuarantinedTests) { + result.results.success = true + } + } + return result }) @@ -825,6 +916,8 @@ addHook({ _ddFlakyTestRetriesCount, _ddIsDiEnabled, _ddIsKnownTestsEnabled, + _ddIsQuarantinedTestsEnabled, + _ddQuarantinedTests, ...restOfTestEnvironmentOptions } = testEnvironmentOptions @@ -936,8 +1029,9 @@ addHook({ }) /* -* This hook does two things: +* This hook does three things: * - Pass known tests to the workers. +* - Pass quarantined tests to the workers. * - Receive trace, coverage and logs payloads from the workers. */ addHook({ @@ -947,7 +1041,7 @@ addHook({ }, (childProcessWorker) => { const ChildProcessWorker = childProcessWorker.default shimmer.wrap(ChildProcessWorker.prototype, 'send', send => function (request) { - if (!isKnownTestsEnabled) { + if (!isKnownTestsEnabled && !isQuarantinedTestsEnabled) { return send.apply(this, arguments) } const [type] = request @@ -967,11 +1061,15 @@ addHook({ const [{ globalConfig, config, path: testSuiteAbsolutePath }] = args const testSuite = getTestSuitePath(testSuiteAbsolutePath, globalConfig.rootDir || process.cwd()) const suiteKnownTests = knownTests.jest?.[testSuite] || [] + + const suiteQuarantinedTests = quarantinedTests.jest?.suites?.[testSuite]?.tests || {} + args[0].config = { ...config, testEnvironmentOptions: { ...config.testEnvironmentOptions, - _ddKnownTests: suiteKnownTests + _ddKnownTests: suiteKnownTests, + _ddQuarantinedTests: suiteQuarantinedTests } } } diff --git a/packages/datadog-instrumentations/src/mocha/main.js b/packages/datadog-instrumentations/src/mocha/main.js index afa7bfe0fc4..143935da3fb 100644 --- a/packages/datadog-instrumentations/src/mocha/main.js +++ b/packages/datadog-instrumentations/src/mocha/main.js @@ -27,6 +27,7 @@ const { getOnPendingHandler, testFileToSuiteAr, newTests, + testsQuarantined, getTestFullName, getRunTestsWrapper } = require('./utils') @@ -61,6 +62,7 @@ const testSuiteCodeCoverageCh = channel('ci:mocha:test-suite:code-coverage') const libraryConfigurationCh = channel('ci:mocha:library-configuration') const knownTestsCh = channel('ci:mocha:known-tests') const skippableSuitesCh = channel('ci:mocha:test-suite:skippable') +const quarantinedTestsCh = channel('ci:mocha:quarantined-tests') const workerReportTraceCh = channel('ci:mocha:worker-report:trace') const testSessionStartCh = channel('ci:mocha:session:start') const testSessionFinishCh = channel('ci:mocha:session:finish') @@ -135,6 +137,18 @@ function getOnEndHandler (isParallel) { } } + // We subtract the errors from quarantined tests from the total number of failures + if (config.isQuarantinedTestsEnabled) { + let numFailedQuarantinedTests = 0 + for (const test of testsQuarantined) { + if (isTestFailed(test)) { + numFailedQuarantinedTests++ + } + } + this.stats.failures -= numFailedQuarantinedTests + this.failures -= numFailedQuarantinedTests + } + if (status === 'fail') { error = new Error(`Failed tests: ${this.failures}.`) } @@ -165,6 +179,7 @@ function getOnEndHandler (isParallel) { error, isEarlyFlakeDetectionEnabled: config.isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty: config.isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled: config.isQuarantinedTestsEnabled, isParallel }) }) @@ -173,6 +188,22 @@ function getOnEndHandler (isParallel) { function getExecutionConfiguration (runner, isParallel, onFinishRequest) { const mochaRunAsyncResource = new AsyncResource('bound-anonymous-fn') + const onReceivedQuarantinedTests = ({ err, quarantinedTests: receivedQuarantinedTests }) => { + if (err) { + config.quarantinedTests = {} + config.isQuarantinedTestsEnabled = false + } else { + config.quarantinedTests = receivedQuarantinedTests + } + if (config.isSuitesSkippingEnabled) { + skippableSuitesCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) + }) + } else { + onFinishRequest() + } + } + const onReceivedSkippableSuites = ({ err, skippableSuites, itrCorrelationId: responseItrCorrelationId }) => { if (err) { suitesToSkip = [] @@ -205,8 +236,11 @@ function getExecutionConfiguration (runner, isParallel, onFinishRequest) { } else { config.knownTests = knownTests } - - if (config.isSuitesSkippingEnabled) { + if (config.isQuarantinedTestsEnabled) { + quarantinedTestsCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedQuarantinedTests) + }) + } else if (config.isSuitesSkippingEnabled) { skippableSuitesCh.publish({ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) }) @@ -224,15 +258,20 @@ function getExecutionConfiguration (runner, isParallel, onFinishRequest) { config.earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries config.earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold config.isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled - // ITR and auto test retries are not supported in parallel mode yet + // ITR, auto test retries and quarantine are not supported in parallel mode yet config.isSuitesSkippingEnabled = !isParallel && libraryConfig.isSuitesSkippingEnabled config.isFlakyTestRetriesEnabled = !isParallel && libraryConfig.isFlakyTestRetriesEnabled config.flakyTestRetriesCount = !isParallel && libraryConfig.flakyTestRetriesCount + config.isQuarantinedTestsEnabled = !isParallel && libraryConfig.isQuarantinedTestsEnabled if (config.isKnownTestsEnabled) { knownTestsCh.publish({ onDone: mochaRunAsyncResource.bind(onReceivedKnownTests) }) + } else if (config.isQuarantinedTestsEnabled) { + quarantinedTestsCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedQuarantinedTests) + }) } else if (config.isSuitesSkippingEnabled) { skippableSuitesCh.publish({ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) @@ -357,7 +396,7 @@ addHook({ this.once('end', getOnEndHandler(false)) - this.on('test', getOnTestHandler(true, newTests)) + this.on('test', getOnTestHandler(true)) this.on('test end', getOnTestEndHandler()) @@ -579,6 +618,7 @@ addHook({ const testPath = getTestSuitePath(testSuiteAbsolutePath, process.cwd()) const testSuiteKnownTests = config.knownTests.mocha?.[testPath] || [] + const testSuiteQuarantinedTests = config.quarantinedTests?.modules?.mocha?.suites?.[testPath] || [] // We pass the known tests for the test file to the worker const testFileResult = await run.apply( @@ -589,6 +629,8 @@ addHook({ ...workerArgs, _ddEfdNumRetries: config.earlyFlakeDetectionNumRetries, _ddIsEfdEnabled: config.isEarlyFlakeDetectionEnabled, + _ddIsQuarantinedEnabled: config.isQuarantinedTestsEnabled, + _ddQuarantinedTests: testSuiteQuarantinedTests, _ddKnownTests: { mocha: { [testPath]: testSuiteKnownTests diff --git a/packages/datadog-instrumentations/src/mocha/utils.js b/packages/datadog-instrumentations/src/mocha/utils.js index 30710ab645b..40fcbdc4ff7 100644 --- a/packages/datadog-instrumentations/src/mocha/utils.js +++ b/packages/datadog-instrumentations/src/mocha/utils.js @@ -26,6 +26,27 @@ const testToStartLine = new WeakMap() const testFileToSuiteAr = new Map() const wrappedFunctions = new WeakSet() const newTests = {} +const testsQuarantined = new Set() + +function isQuarantinedTest (test, testsToQuarantine) { + const testSuite = getTestSuitePath(test.file, process.cwd()) + const testName = test.fullTitle() + + const isQuarantined = (testsToQuarantine + .mocha + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined) ?? false + + if (isQuarantined) { + testsQuarantined.add(test) + } + + return isQuarantined +} function isNewTest (test, knownTests) { const testSuite = getTestSuitePath(test.file, process.cwd()) @@ -171,7 +192,8 @@ function getOnTestHandler (isMain) { file: testSuiteAbsolutePath, title, _ddIsNew: isNew, - _ddIsEfdRetry: isEfdRetry + _ddIsEfdRetry: isEfdRetry, + _ddIsQuarantined: isQuarantined } = test const testInfo = { @@ -187,6 +209,7 @@ function getOnTestHandler (isMain) { testInfo.isNew = isNew testInfo.isEfdRetry = isEfdRetry + testInfo.isQuarantined = isQuarantined // We want to store the result of the new tests if (isNew) { const testFullName = getTestFullName(test) @@ -360,6 +383,15 @@ function getRunTestsWrapper (runTests, config) { } }) } + + if (config.isQuarantinedTestsEnabled) { + suite.tests.forEach(test => { + if (isQuarantinedTest(test, config.quarantinedTests)) { + test._ddIsQuarantined = true + } + }) + } + return runTests.apply(this, arguments) } } @@ -384,5 +416,6 @@ module.exports = { getOnPendingHandler, testFileToSuiteAr, getRunTestsWrapper, - newTests + newTests, + testsQuarantined } diff --git a/packages/datadog-instrumentations/src/mocha/worker.js b/packages/datadog-instrumentations/src/mocha/worker.js index 56a9dc75270..88a2b33b498 100644 --- a/packages/datadog-instrumentations/src/mocha/worker.js +++ b/packages/datadog-instrumentations/src/mocha/worker.js @@ -33,6 +33,13 @@ addHook({ delete this.options._ddIsEfdEnabled delete this.options._ddKnownTests delete this.options._ddEfdNumRetries + delete this.options._ddQuarantinedTests + } + if (this.options._ddIsQuarantinedEnabled) { + config.isQuarantinedEnabled = true + config.quarantinedTests = this.options._ddQuarantinedTests + delete this.options._ddIsQuarantinedEnabled + delete this.options._ddQuarantinedTests } return run.apply(this, arguments) }) diff --git a/packages/datadog-instrumentations/src/openai.js b/packages/datadog-instrumentations/src/openai.js index 0e921fb2b43..e41db136854 100644 --- a/packages/datadog-instrumentations/src/openai.js +++ b/packages/datadog-instrumentations/src/openai.js @@ -97,6 +97,14 @@ const V4_PACKAGE_SHIMS = [ targetClass: 'Translations', baseResource: 'audio.translations', methods: ['create'] + }, + { + file: 'resources/chat/completions/completions.js', + targetClass: 'Completions', + baseResource: 'chat.completions', + methods: ['create'], + streamedResponse: true, + versions: ['>=4.85.0'] } ] diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index bcd389dd09f..ee219e5290c 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -13,6 +13,7 @@ const testSessionFinishCh = channel('ci:playwright:session:finish') const libraryConfigurationCh = channel('ci:playwright:library-configuration') const knownTestsCh = channel('ci:playwright:known-tests') +const quarantinedTestsCh = channel('ci:playwright:quarantined-tests') const testSuiteStartCh = channel('ci:playwright:test-suite:start') const testSuiteFinishCh = channel('ci:playwright:test-suite:finish') @@ -41,9 +42,25 @@ let earlyFlakeDetectionNumRetries = 0 let isFlakyTestRetriesEnabled = false let flakyTestRetriesCount = 0 let knownTests = {} +let isQuarantinedTestsEnabled = false +let quarantinedTests = {} let rootDir = '' const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' +function isQuarantineTest (test) { + const testName = getTestFullname(test) + const testSuite = getTestSuitePath(test._requireFile, rootDir) + + return quarantinedTests + ?.playwright + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined +} + function isNewTest (test) { const testSuite = getTestSuitePath(test._requireFile, rootDir) const testsForSuite = knownTests?.playwright?.[testSuite] || [] @@ -296,6 +313,7 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) { error, extraTags: annotationTags, isNew: test._ddIsNew, + isQuarantined: test._ddIsQuarantined, isEfdRetry: test._ddIsEfdRetry }) }) @@ -424,10 +442,12 @@ function runnerHook (runnerExport, playwrightVersion) { earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount + isQuarantinedTestsEnabled = libraryConfig.isQuarantinedTestsEnabled } } catch (e) { isEarlyFlakeDetectionEnabled = false isKnownTestsEnabled = false + isQuarantinedTestsEnabled = false log.error('Playwright session start error', e) } @@ -447,6 +467,20 @@ function runnerHook (runnerExport, playwrightVersion) { } } + if (isQuarantinedTestsEnabled && satisfies(playwrightVersion, MINIMUM_SUPPORTED_VERSION_RANGE_EFD)) { + try { + const { err, quarantinedTests: receivedQuarantinedTests } = await getChannelPromise(quarantinedTestsCh) + if (!err) { + quarantinedTests = receivedQuarantinedTests + } else { + isQuarantinedTestsEnabled = false + } + } catch (err) { + isQuarantinedTestsEnabled = false + log.error('Playwright quarantined tests error', err) + } + } + const projects = getProjectsFromRunner(this) if (isFlakyTestRetriesEnabled && flakyTestRetriesCount > 0) { @@ -479,6 +513,7 @@ function runnerHook (runnerExport, playwrightVersion) { testSessionFinishCh.publish({ status: STATUS_TO_TEST_STATUS[sessionStatus], isEarlyFlakeDetectionEnabled, + isQuarantinedTestsEnabled, onDone }) }) @@ -487,6 +522,8 @@ function runnerHook (runnerExport, playwrightVersion) { startedSuites = [] remainingTestsByFile = {} + // TODO: we can trick playwright into thinking the session passed by returning + // 'passed' here. We might be able to use this for both EFD and Quarantined tests. return runAllTestsReturn }) @@ -557,26 +594,37 @@ addHook({ const oldCreateRootSuite = loadUtilsPackage.createRootSuite async function newCreateRootSuite () { + if (!isKnownTestsEnabled && !isQuarantinedTestsEnabled) { + return oldCreateRootSuite.apply(this, arguments) + } const rootSuite = await oldCreateRootSuite.apply(this, arguments) - if (!isKnownTestsEnabled) { - return rootSuite + + const allTests = rootSuite.allTests() + + if (isQuarantinedTestsEnabled) { + const testsToBeIgnored = allTests.filter(isQuarantineTest) + testsToBeIgnored.forEach(test => { + test._ddIsQuarantined = true + test.expectedStatus = 'skipped' + }) } - const newTests = rootSuite - .allTests() - .filter(isNewTest) - - newTests.forEach(newTest => { - newTest._ddIsNew = true - if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped') { - const fileSuite = getSuiteType(newTest, 'file') - const projectSuite = getSuiteType(newTest, 'project') - for (let repeatEachIndex = 0; repeatEachIndex < earlyFlakeDetectionNumRetries; repeatEachIndex++) { - const copyFileSuite = deepCloneSuite(fileSuite, isNewTest) - applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1) - projectSuite._addSuite(copyFileSuite) + + if (isKnownTestsEnabled) { + const newTests = allTests.filter(isNewTest) + + newTests.forEach(newTest => { + newTest._ddIsNew = true + if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped') { + const fileSuite = getSuiteType(newTest, 'file') + const projectSuite = getSuiteType(newTest, 'project') + for (let repeatEachIndex = 0; repeatEachIndex < earlyFlakeDetectionNumRetries; repeatEachIndex++) { + const copyFileSuite = deepCloneSuite(fileSuite, isNewTest) + applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1) + projectSuite._addSuite(copyFileSuite) + } } - } - }) + }) + } return rootSuite } diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index ebde98b4789..340fd188340 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -9,6 +9,7 @@ const testPassCh = channel('ci:vitest:test:pass') const testErrorCh = channel('ci:vitest:test:error') const testSkipCh = channel('ci:vitest:test:skip') const isNewTestCh = channel('ci:vitest:test:is-new') +const isQuarantinedCh = channel('ci:vitest:test:is-quarantined') // test suite hooks const testSuiteStartCh = channel('ci:vitest:test-suite:start') @@ -21,10 +22,12 @@ const testSessionFinishCh = channel('ci:vitest:session:finish') const libraryConfigurationCh = channel('ci:vitest:library-configuration') const knownTestsCh = channel('ci:vitest:known-tests') const isEarlyFlakeDetectionFaultyCh = channel('ci:vitest:is-early-flake-detection-faulty') +const quarantinedTestsCh = channel('ci:vitest:quarantined-tests') const taskToAsync = new WeakMap() const taskToStatuses = new WeakMap() const newTasks = new WeakSet() +const quarantinedTasks = new WeakSet() let isRetryReasonEfd = false const switchedStatuses = new WeakSet() const sessionAsyncResource = new AsyncResource('bound-anonymous-fn') @@ -46,7 +49,9 @@ function getProvidedContext () { _ddIsDiEnabled, _ddKnownTests: knownTests, _ddEarlyFlakeDetectionNumRetries: numRepeats, - _ddIsKnownTestsEnabled: isKnownTestsEnabled + _ddIsKnownTestsEnabled: isKnownTestsEnabled, + _ddIsQuarantinedTestsEnabled: isQuarantinedTestsEnabled, + _ddQuarantinedTests: quarantinedTests } = globalThis.__vitest_worker__.providedContext return { @@ -54,7 +59,9 @@ function getProvidedContext () { isEarlyFlakeDetectionEnabled: _ddIsEarlyFlakeDetectionEnabled, knownTests, numRepeats, - isKnownTestsEnabled + isKnownTestsEnabled, + isQuarantinedTestsEnabled, + quarantinedTests } } catch (e) { log.error('Vitest workers could not parse provided context, so some features will not work.') @@ -63,7 +70,9 @@ function getProvidedContext () { isEarlyFlakeDetectionEnabled: false, knownTests: {}, numRepeats: 0, - isKnownTestsEnabled: false + isKnownTestsEnabled: false, + isQuarantinedTestsEnabled: false, + quarantinedTests: {} } } } @@ -158,8 +167,10 @@ function getSortWrapper (sort) { let earlyFlakeDetectionNumRetries = 0 let isEarlyFlakeDetectionFaulty = false let isKnownTestsEnabled = false + let isQuarantinedTestsEnabled = false let isDiEnabled = false let knownTests = {} + let quarantinedTests = {} try { const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh) @@ -170,6 +181,7 @@ function getSortWrapper (sort) { earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries isDiEnabled = libraryConfig.isDiEnabled isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled + isQuarantinedTestsEnabled = libraryConfig.isQuarantinedTestsEnabled } } catch (e) { isFlakyTestRetriesEnabled = false @@ -229,6 +241,23 @@ function getSortWrapper (sort) { } } + if (isQuarantinedTestsEnabled) { + const { err, quarantinedTests: receivedQuarantinedTests } = await getChannelPromise(quarantinedTestsCh) + if (!err) { + quarantinedTests = receivedQuarantinedTests + try { + const workspaceProject = this.ctx.getCoreWorkspaceProject() + workspaceProject._provided._ddIsQuarantinedTestsEnabled = isQuarantinedTestsEnabled + workspaceProject._provided._ddQuarantinedTests = quarantinedTests + } catch (e) { + log.warn('Could not send quarantined tests to workers so Quarantine will not work.') + } + } else { + isQuarantinedTestsEnabled = false + log.error('Could not get quarantined tests.') + } + } + let testCodeCoverageLinesTotal if (this.ctx.coverageProvider?.generateCoverage) { @@ -263,6 +292,7 @@ function getSortWrapper (sort) { error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, onFinish }) }) @@ -332,7 +362,7 @@ addHook({ // `onAfterRunTask` is run after all repetitions or attempts are run shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => async function (task) { - const { isEarlyFlakeDetectionEnabled } = getProvidedContext() + const { isEarlyFlakeDetectionEnabled, isQuarantinedTestsEnabled } = getProvidedContext() if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task)) { const statuses = taskToStatuses.get(task) @@ -345,6 +375,12 @@ addHook({ } } + if (isQuarantinedTestsEnabled) { + if (quarantinedTasks.has(task)) { + task.result.state = 'pass' + } + } + return onAfterRunTask.apply(this, arguments) }) @@ -356,17 +392,34 @@ addHook({ } const testName = getTestName(task) let isNew = false + let isQuarantined = false const { isKnownTestsEnabled, isEarlyFlakeDetectionEnabled, - isDiEnabled + isDiEnabled, + isQuarantinedTestsEnabled, + quarantinedTests } = getProvidedContext() if (isKnownTestsEnabled) { isNew = newTasks.has(task) } + if (isQuarantinedTestsEnabled) { + isQuarantinedCh.publish({ + quarantinedTests, + testSuiteAbsolutePath: task.file.filepath, + testName, + onDone: (isTestQuarantined) => { + isQuarantined = isTestQuarantined + if (isTestQuarantined) { + quarantinedTasks.add(task) + } + } + }) + } + const { retry: numAttempt, repeats: numRepetition } = retryInfo // We finish the previous test here because we know it has failed already @@ -448,7 +501,8 @@ addHook({ isRetry: numAttempt > 0 || numRepetition > 0, isRetryReasonEfd, isNew, - mightHitProbe: isDiEnabled && numAttempt > 0 + mightHitProbe: isDiEnabled && numAttempt > 0, + isQuarantined }) }) return onBeforeTryTask.apply(this, arguments) diff --git a/packages/datadog-plugin-cucumber/src/index.js b/packages/datadog-plugin-cucumber/src/index.js index 1c0cc85a26e..a79601c6799 100644 --- a/packages/datadog-plugin-cucumber/src/index.js +++ b/packages/datadog-plugin-cucumber/src/index.js @@ -27,7 +27,9 @@ const { TEST_MODULE_ID, TEST_SUITE, CUCUMBER_IS_PARALLEL, - TEST_RETRY_REASON + TEST_RETRY_REASON, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../dd-trace/src/plugins/util/test') const { RESOURCE_NAME } = require('../../../ext/tags') const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants') @@ -47,6 +49,7 @@ const { const id = require('../../dd-trace/src/id') const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200 +const BREAKPOINT_SET_GRACE_PERIOD_MS = 200 const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID function getTestSuiteTags (testSuiteSpan) { @@ -83,6 +86,7 @@ class CucumberPlugin extends CiPlugin { hasForcedToRunSuites, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, isParallel }) => { const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.libraryConfig || {} @@ -109,6 +113,9 @@ class CucumberPlugin extends CiPlugin { if (isParallel) { this.testSessionSpan.setTag(CUCUMBER_IS_PARALLEL, 'true') } + if (isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } this.testSessionSpan.setTag(TEST_STATUS, status) this.testModuleSpan.setTag(TEST_STATUS, status) @@ -251,7 +258,12 @@ class CucumberPlugin extends CiPlugin { const { file, line, stackIndex } = probeInformation this.runningTestProbe = { file, line } this.testErrorStackIndex = stackIndex - // TODO: we're not waiting for setProbePromise to be resolved, so there might be race conditions + const waitUntil = Date.now() + BREAKPOINT_SET_GRACE_PERIOD_MS + while (Date.now() < waitUntil) { + // TODO: To avoid a race condition, we should wait until `probeInformation.setProbePromise` has resolved. + // However, Cucumber doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to + // fall back to a fixed syncronous delay. + } } } span.setTag(TEST_STATUS, 'fail') @@ -311,7 +323,8 @@ class CucumberPlugin extends CiPlugin { errorMessage, isNew, isEfdRetry, - isFlakyRetry + isFlakyRetry, + isQuarantined }) => { const span = storage('legacy').getStore().span const statusTag = isStep ? 'step.status' : TEST_STATUS @@ -340,6 +353,10 @@ class CucumberPlugin extends CiPlugin { span.setTag(TEST_IS_RETRY, 'true') } + if (isQuarantined) { + span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } + span.finish() if (!isStep) { const spanTags = span.context()._tags diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index 67487e47dbb..470ff290625 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -33,7 +33,9 @@ const { TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, TEST_RETRY_REASON, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_IS_QUARANTINED, + TEST_MANAGEMENT_ENABLED } = require('../../dd-trace/src/plugins/util/test') const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util') const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants') @@ -152,6 +154,20 @@ function getKnownTests (tracer, testConfiguration) { }) } +function getQuarantinedTests (tracer, testConfiguration) { + return new Promise(resolve => { + if (!tracer._tracer._exporter?.getQuarantinedTests) { + return resolve({ err: new Error('Test Optimization was not initialized correctly') }) + } + tracer._tracer._exporter.getQuarantinedTests(testConfiguration, (err, quarantinedTests) => { + resolve({ + err, + quarantinedTests + }) + }) + }) +} + function getSuiteStatus (suiteStats) { if (!suiteStats) { return 'skip' @@ -240,7 +256,8 @@ class CypressPlugin { earlyFlakeDetectionNumRetries, isFlakyTestRetriesEnabled, flakyTestRetriesCount, - isKnownTestsEnabled + isKnownTestsEnabled, + isQuarantinedTestsEnabled } } = libraryConfigurationResponse this.isSuitesSkippingEnabled = isSuitesSkippingEnabled @@ -251,12 +268,24 @@ class CypressPlugin { if (isFlakyTestRetriesEnabled) { this.cypressConfig.retries.runMode = flakyTestRetriesCount } + this.isQuarantinedTestsEnabled = isQuarantinedTestsEnabled } return this.cypressConfig }) return this.libraryConfigurationPromise } + getIsQuarantinedTest (testSuite, testName) { + return this.quarantinedTests + ?.cypress + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined + } + getTestSuiteSpan ({ testSuite, testSuiteAbsolutePath }) { const testSuiteSpanMetadata = getTestSuiteCommonTags(this.command, this.frameworkVersion, testSuite, TEST_FRAMEWORK_NAME) @@ -351,10 +380,6 @@ class CypressPlugin { }) } - isNewTest (testName, testSuite) { - return !this.knownTestsByTestSuite?.[testSuite]?.includes(testName) - } - async beforeRun (details) { // We need to make sure that the plugin is initialized before running the tests // This is for the case where the user has not returned the promise from the init function @@ -393,6 +418,19 @@ class CypressPlugin { } } + if (this.isQuarantinedTestsEnabled) { + const quarantinedTestsResponse = await getQuarantinedTests( + this.tracer, + this.testConfiguration + ) + if (quarantinedTestsResponse.err) { + log.error('Cypress quarantined tests response error', quarantinedTestsResponse.err) + this.isQuarantinedTestsEnabled = false + } else { + this.quarantinedTests = quarantinedTestsResponse.quarantinedTests + } + } + // `details.specs` are test files details.specs?.forEach(({ absolute, relative }) => { const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute }) @@ -471,6 +509,10 @@ class CypressPlugin { } ) + if (this.isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } + this.testModuleSpan.finish() this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() @@ -545,6 +587,13 @@ class CypressPlugin { if (this.itrCorrelationId) { skippedTestSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId) } + + const isQuarantined = this.getIsQuarantinedTest(spec.relative, cypressTestName) + + if (isQuarantined) { + skippedTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } + skippedTestSpan.finish() }) @@ -648,6 +697,7 @@ class CypressPlugin { }) const isUnskippable = this.unskippableSuites.includes(testSuite) const isForcedToRun = shouldSkip && isUnskippable + const isQuarantined = this.getIsQuarantinedTest(testSuite, testName) // skip test if (shouldSkip && !isUnskippable) { @@ -656,6 +706,12 @@ class CypressPlugin { return { shouldSkip: true } } + // TODO: I haven't found a way to trick cypress into ignoring a test + // The way we'll implement quarantine in cypress is by skipping the test altogether + if (isQuarantined) { + return { shouldSkip: true } + } + if (!this.activeTestSpan) { this.activeTestSpan = this.getTestSpan({ testName, @@ -681,7 +737,8 @@ class CypressPlugin { testSuiteAbsolutePath, testName, isNew, - isEfdRetry + isEfdRetry, + isQuarantined } = test if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) { const coverageFiles = getCoveredFilenamesFromCoverage(coverage) @@ -720,6 +777,9 @@ class CypressPlugin { this.activeTestSpan.setTag(TEST_RETRY_REASON, 'efd') } } + if (isQuarantined) { + this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } const finishedTest = { testName, testStatus, diff --git a/packages/datadog-plugin-dd-trace-api/src/index.js b/packages/datadog-plugin-dd-trace-api/src/index.js index dee4d1aa5c0..4fa4bf77317 100644 --- a/packages/datadog-plugin-dd-trace-api/src/index.js +++ b/packages/datadog-plugin-dd-trace-api/src/index.js @@ -28,7 +28,7 @@ module.exports = class DdTraceApiPlugin extends Plugin { }) const handleEvent = (name) => { - const counter = apiMetrics.count('dd_trace_api.called', [ + const counter = apiMetrics.count('public_api.called', [ `name:${name.replaceAll(':', '.')}`, 'api_version:v1', injectionEnabledTag @@ -48,14 +48,6 @@ module.exports = class DdTraceApiPlugin extends Plugin { self = objectMap.get(self) } - // `trace` returns the value that's returned from the original callback - // passed to it, so we need to detect that happening and bypass the check - // for a proxy, since a proxy isn't needed, since the object originates - // from the caller. In callbacks, we'll assign return values to this - // value, and bypass the proxy check if `ret.value` is exactly this - // value. - let passthroughRetVal - for (let i = 0; i < args.length; i++) { if (objectMap.has(args[i])) { args[i] = objectMap.get(args[i]) @@ -71,8 +63,7 @@ module.exports = class DdTraceApiPlugin extends Plugin { } } // TODO do we need to apply(this, ...) here? - passthroughRetVal = orig(...fnArgs) - return passthroughRetVal + return orig(...fnArgs) } } } @@ -83,8 +74,6 @@ module.exports = class DdTraceApiPlugin extends Plugin { const proxyVal = proxy() objectMap.set(proxyVal, ret.value) ret.value = proxyVal - } else if (ret.value && typeof ret.value === 'object' && passthroughRetVal !== ret.value) { - throw new TypeError(`Objects need proxies when returned via API (${name})`) } } catch (e) { ret.error = e diff --git a/packages/datadog-plugin-dd-trace-api/test/index.spec.js b/packages/datadog-plugin-dd-trace-api/test/index.spec.js index 16e78bb06da..b02109c4aee 100644 --- a/packages/datadog-plugin-dd-trace-api/test/index.spec.js +++ b/packages/datadog-plugin-dd-trace-api/test/index.spec.js @@ -220,20 +220,6 @@ describe('Plugin', () => { describeMethod('extract', null) describeMethod('getRumData', '') describeMethod('trace') - - describe('trace with return value', () => { - it('should return the exact same value', () => { - const obj = { mustBeThis: 'value' } - tracer.trace.resetHistory() // clear previous call to `trace` - testChannel({ - name: 'trace', - fn: tracer.trace, - ret: obj, - proxy: false, - args: ['foo', {}, () => obj] - }) - }) - }) describeMethod('wrap') describeMethod('use', SELF) describeMethod('profilerStarted', Promise.resolve(false)) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index 3ec965efdbd..77e9409cebf 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -24,7 +24,9 @@ const { TEST_IS_RUM_ACTIVE, TEST_BROWSER_DRIVER, getFormattedError, - TEST_RETRY_REASON + TEST_RETRY_REASON, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const id = require('../../dd-trace/src/id') @@ -106,6 +108,7 @@ class JestPlugin extends CiPlugin { error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, onDone }) => { this.testSessionSpan.setTag(TEST_STATUS, status) @@ -137,6 +140,9 @@ class JestPlugin extends CiPlugin { if (isEarlyFlakeDetectionFaulty) { this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty') } + if (isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } this.testModuleSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') @@ -166,6 +172,7 @@ class JestPlugin extends CiPlugin { config._ddEarlyFlakeDetectionNumRetries = this.libraryConfig?.earlyFlakeDetectionNumRetries ?? 0 config._ddRepositoryRoot = this.repositoryRoot config._ddIsFlakyTestRetriesEnabled = this.libraryConfig?.isFlakyTestRetriesEnabled ?? false + config._ddIsQuarantinedTestsEnabled = this.libraryConfig?.isQuarantinedTestsEnabled ?? false config._ddFlakyTestRetriesCount = this.libraryConfig?.flakyTestRetriesCount config._ddIsDiEnabled = this.libraryConfig?.isDiEnabled ?? false config._ddIsKnownTestsEnabled = this.libraryConfig?.isKnownTestsEnabled ?? false @@ -325,12 +332,15 @@ class JestPlugin extends CiPlugin { this.activeTestSpan = span }) - this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => { + this.addSub('ci:jest:test:finish', ({ status, testStartLine, isQuarantined }) => { const span = storage('legacy').getStore().span span.setTag(TEST_STATUS, status) if (testStartLine) { span.setTag(TEST_SOURCE_START, testStartLine) } + if (isQuarantined) { + span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } const spanTags = span.context()._tags this.telemetry.ciVisEvent( diff --git a/packages/datadog-plugin-mocha/src/index.js b/packages/datadog-plugin-mocha/src/index.js index 5918a3a5db5..b96517e0ea5 100644 --- a/packages/datadog-plugin-mocha/src/index.js +++ b/packages/datadog-plugin-mocha/src/index.js @@ -31,7 +31,9 @@ const { MOCHA_IS_PARALLEL, TEST_IS_RUM_ACTIVE, TEST_BROWSER_DRIVER, - TEST_RETRY_REASON + TEST_RETRY_REASON, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -48,6 +50,8 @@ const { const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') +const BREAKPOINT_SET_GRACE_PERIOD_MS = 200 + function getTestSuiteLevelVisibilityTags (testSuiteSpan) { const testSuiteSpanContext = testSuiteSpan.context() const suiteTags = { @@ -279,7 +283,12 @@ class MochaPlugin extends CiPlugin { this.runningTestProbe = { file, line } this.testErrorStackIndex = stackIndex test._ddShouldWaitForHitProbe = true - // TODO: we're not waiting for setProbePromise to be resolved, so there might be race conditions + const waitUntil = Date.now() + BREAKPOINT_SET_GRACE_PERIOD_MS + while (Date.now() < waitUntil) { + // TODO: To avoid a race condition, we should wait until `probeInformation.setProbePromise` has resolved. + // However, Mocha doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to + // fall back to a fixed syncronous delay. + } } } @@ -302,6 +311,7 @@ class MochaPlugin extends CiPlugin { error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, isParallel }) => { if (this.testSessionSpan) { @@ -318,6 +328,10 @@ class MochaPlugin extends CiPlugin { this.testSessionSpan.setTag(MOCHA_IS_PARALLEL, 'true') } + if (isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } + addIntelligentTestRunnerSpanTags( this.testSessionSpan, this.testModuleSpan, @@ -390,7 +404,8 @@ class MochaPlugin extends CiPlugin { isNew, isEfdRetry, testStartLine, - isParallel + isParallel, + isQuarantined } = testInfo const testName = removeEfdStringFromTestName(testInfo.testName) @@ -409,6 +424,10 @@ class MochaPlugin extends CiPlugin { extraTags[MOCHA_IS_PARALLEL] = 'true' } + if (isQuarantined) { + extraTags[TEST_MANAGEMENT_IS_QUARANTINED] = 'true' + } + const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot) const testSuiteSpan = this._testSuites.get(testSuite) diff --git a/packages/datadog-plugin-mongodb-core/src/index.js b/packages/datadog-plugin-mongodb-core/src/index.js index 076d65917b5..a60182458e1 100644 --- a/packages/datadog-plugin-mongodb-core/src/index.js +++ b/packages/datadog-plugin-mongodb-core/src/index.js @@ -11,8 +11,9 @@ class MongodbCorePlugin extends DatabasePlugin { start ({ ns, ops, options = {}, name }) { const query = getQuery(ops) const resource = truncate(getResource(this, ns, query, name)) - this.startSpan(this.operationName(), { - service: this.serviceName({ pluginConfig: this.config }), + const service = this.serviceName({ pluginConfig: this.config }) + const span = this.startSpan(this.operationName(), { + service, resource, type: 'mongodb', kind: 'client', @@ -24,6 +25,7 @@ class MongodbCorePlugin extends DatabasePlugin { 'out.port': options.port } }) + ops = this.injectDbmCommand(span, ops, service) } getPeerService (tags) { @@ -34,6 +36,30 @@ class MongodbCorePlugin extends DatabasePlugin { } return super.getPeerService(tags) } + + injectDbmCommand (span, command, serviceName) { + const dbmTraceComment = this.createDbmComment(span, serviceName) + + if (!dbmTraceComment) { + return command + } + + // create a copy of the command to avoid mutating the original + const dbmTracedCommand = { ...command } + + if (dbmTracedCommand.comment) { + // if the command already has a comment, append the dbm trace comment + if (typeof dbmTracedCommand.comment === 'string') { + dbmTracedCommand.comment += `,${dbmTraceComment}` + } else if (Array.isArray(dbmTracedCommand.comment)) { + dbmTracedCommand.comment.push(dbmTraceComment) + } // do nothing if the comment is not a string or an array + } else { + dbmTracedCommand.comment = dbmTraceComment + } + + return dbmTracedCommand + } } function sanitizeBigInt (data) { diff --git a/packages/datadog-plugin-mongodb-core/test/core.spec.js b/packages/datadog-plugin-mongodb-core/test/core.spec.js index 13a346077cf..98b483d79aa 100644 --- a/packages/datadog-plugin-mongodb-core/test/core.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/core.spec.js @@ -1,10 +1,14 @@ 'use strict' +const sinon = require('sinon') const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const { expectedSchema, rawExpectedSchema } = require('./naming') +const MongodbCorePlugin = require('../../datadog-plugin-mongodb-core/src/index') +const ddpv = require('mocha/package.json').version + const withTopologies = fn => { withVersions('mongodb-core', ['mongodb-core', 'mongodb'], '<4', (version, moduleName) => { describe('using the server topology', () => { @@ -29,6 +33,7 @@ describe('Plugin', () => { let id let tracer let collection + let injectDbmCommandSpy describe('mongodb-core (core)', () => { withTopologies(getServer => { @@ -397,6 +402,181 @@ describe('Plugin', () => { } ) }) + + describe('with dbmPropagationMode service', () => { + before(() => { + return agent.load('mongodb-core', { dbmPropagationMode: 'service' }) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(done => { + const Server = getServer() + + server = new Server({ + host: '127.0.0.1', + port: 27017, + reconnect: false + }) + + server.on('connect', () => done()) + server.on('error', done) + + server.connect() + + injectDbmCommandSpy = sinon.spy(MongodbCorePlugin.prototype, 'injectDbmCommand') + }) + + afterEach(() => { + injectDbmCommandSpy?.restore() + }) + + it('DBM propagation should inject service mode as comment', done => { + agent + .use(traces => { + const span = traces[0][0] + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.equal( + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}'` + ) + }) + .then(done) + .catch(done) + + server.insert(`test.${collection}`, [{ a: 1 }], () => {}) + }) + + it('DBM propagation should inject service mode after eixsting str comment', done => { + agent + .use(traces => { + const span = traces[0][0] + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.equal( + 'test comment,' + + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}'` + ) + }) + .then(done) + .catch(done) + + server.command(`test.${collection}`, { + find: `test.${collection}`, + query: { + _id: Buffer.from('1234') + }, + comment: 'test comment' + }, () => {}) + }) + + it('DBM propagation should inject service mode after eixsting array comment', done => { + agent + .use(traces => { + const span = traces[0][0] + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.deep.equal([ + 'test comment', + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}'` + ]) + }) + .then(done) + .catch(done) + + server.command(`test.${collection}`, { + find: `test.${collection}`, + query: { + _id: Buffer.from('1234') + }, + comment: ['test comment'] + }, () => {}) + }) + }) + + describe('with dbmPropagationMode full', () => { + before(() => { + return agent.load('mongodb-core', { dbmPropagationMode: 'full' }) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(done => { + const Server = getServer() + + server = new Server({ + host: '127.0.0.1', + port: 27017, + reconnect: false + }) + + server.on('connect', () => done()) + server.on('error', done) + + server.connect() + + injectDbmCommandSpy = sinon.spy(MongodbCorePlugin.prototype, 'injectDbmCommand') + }) + + afterEach(() => { + injectDbmCommandSpy?.restore() + }) + + it('DBM propagation should inject full mode with traceparent as comment', done => { + agent + .use(traces => { + const span = traces[0][0] + const traceId = span.meta['_dd.p.tid'] + span.trace_id.toString(16).padStart(16, '0') + const spanId = span.span_id.toString(16).padStart(16, '0') + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.equal( + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}',` + + `traceparent='00-${traceId}-${spanId}-00'` + ) + }) + .then(done) + .catch(done) + + server.insert(`test.${collection}`, [{ a: 1 }], () => {}) + }) + }) }) }) }) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 0e16a3fd71a..db6ee8ffeec 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -1,9 +1,13 @@ 'use strict' +const sinon = require('sinon') const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { expectedSchema, rawExpectedSchema } = require('./naming') +const MongodbCorePlugin = require('../../datadog-plugin-mongodb-core/src/index') +const ddpv = require('mocha/package.json').version + const withTopologies = fn => { const isOldNode = semver.satisfies(process.version, '<=14') const range = isOldNode ? '>=2 <6' : '>=2' // TODO: remove when 3.x support is removed. @@ -44,6 +48,7 @@ describe('Plugin', () => { let collection let db let BSON + let injectDbmCommandSpy describe('mongodb-core', () => { withTopologies(createClient => { @@ -334,6 +339,109 @@ describe('Plugin', () => { } ) }) + + describe('with dbmPropagationMode service', () => { + before(() => { + return agent.load('mongodb-core', { + dbmPropagationMode: 'service' + }) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(async () => { + client = await createClient() + db = client.db('test') + collection = db.collection(collectionName) + + injectDbmCommandSpy = sinon.spy(MongodbCorePlugin.prototype, 'injectDbmCommand') + }) + + afterEach(() => { + injectDbmCommandSpy?.restore() + }) + + it('DBM propagation should inject service mode as comment', done => { + agent + .use(traces => { + const span = traces[0][0] + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.equal( + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}'` + ) + }) + .then(done) + .catch(done) + + collection.find({ + _id: Buffer.from('1234') + }).toArray() + }) + }) + + describe('with dbmPropagationMode full', () => { + before(() => { + return agent.load('mongodb-core', { + dbmPropagationMode: 'full' + }) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(async () => { + client = await createClient() + db = client.db('test') + collection = db.collection(collectionName) + + injectDbmCommandSpy = sinon.spy(MongodbCorePlugin.prototype, 'injectDbmCommand') + }) + + afterEach(() => { + injectDbmCommandSpy?.restore() + }) + + it('DBM propagation should inject full mode with traceparent as comment', done => { + agent + .use(traces => { + const span = traces[0][0] + const traceId = span.meta['_dd.p.tid'] + span.trace_id.toString(16).padStart(16, '0') + const spanId = span.span_id.toString(16).padStart(16, '0') + + expect(injectDbmCommandSpy.called).to.be.true + const instrumentedCommand = injectDbmCommandSpy.getCall(0).returnValue + expect(instrumentedCommand).to.have.property('comment') + expect(instrumentedCommand.comment).to.equal( + `dddb='${encodeURIComponent(span.meta['db.name'])}',` + + 'dddbs=\'test-mongodb\',' + + 'dde=\'tester\',' + + `ddh='${encodeURIComponent(span.meta['out.host'])}',` + + `ddps='${encodeURIComponent(span.meta.service)}',` + + `ddpv='${ddpv}',` + + `ddprs='${encodeURIComponent(span.meta['peer.service'])}',` + + `traceparent='00-${traceId}-${spanId}-00'` + ) + }) + .then(done) + .catch(done) + + collection.find({ + _id: Buffer.from('1234') + }).toArray() + }) + }) }) }) }) diff --git a/packages/datadog-plugin-openai/src/tracing.js b/packages/datadog-plugin-openai/src/tracing.js index 79eaed2a52d..7193242e826 100644 --- a/packages/datadog-plugin-openai/src/tracing.js +++ b/packages/datadog-plugin-openai/src/tracing.js @@ -795,7 +795,7 @@ function truncateApiKey (apiKey) { function tagChatCompletionRequestContent (contents, messageIdx, tags) { if (typeof contents === 'string') { - tags[`openai.request.messages.${messageIdx}.content`] = contents + tags[`openai.request.messages.${messageIdx}.content`] = normalize(contents) } else if (Array.isArray(contents)) { // content can also be an array of objects // which represent text input or image url diff --git a/packages/datadog-plugin-openai/test/index.spec.js b/packages/datadog-plugin-openai/test/index.spec.js index 8df38a11650..03ac66fb2e5 100644 --- a/packages/datadog-plugin-openai/test/index.spec.js +++ b/packages/datadog-plugin-openai/test/index.spec.js @@ -7,6 +7,7 @@ const semver = require('semver') const nock = require('nock') const sinon = require('sinon') const { spawn } = require('child_process') +const { useEnv } = require('../../../integration-tests/helpers') const agent = require('../../dd-trace/test/plugins/agent') const { DogStatsDClient } = require('../../dd-trace/src/dogstatsd') @@ -31,12 +32,12 @@ describe('Plugin', () => { tracer = require(tracerRequirePath) }) - before(() => { + beforeEach(() => { return agent.load('openai') }) - after(() => { - return agent.close({ ritmReset: false }) + afterEach(() => { + return agent.close({ ritmReset: false, wipe: true }) }) beforeEach(() => { @@ -72,6 +73,67 @@ describe('Plugin', () => { sinon.restore() }) + describe('with configuration', () => { + useEnv({ + DD_OPENAI_SPAN_CHAR_LIMIT: 0 + }) + + it('should truncate both inputs and outputs', async () => { + if (version === '3.0.0') return + nock('https://api.openai.com:443') + .post('/v1/chat/completions') + .reply(200, { + model: 'gpt-3.5-turbo-0301', + choices: [{ + message: { + role: 'assistant', + content: "In that case, it's best to avoid peanut" + } + }] + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0][0].meta).to.have.property('openai.request.messages.0.content', + '...') + expect(traces[0][0].meta).to.have.property('openai.request.messages.1.content', + '...') + expect(traces[0][0].meta).to.have.property('openai.request.messages.2.content', '...') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.message.content', + '...') + }) + + const params = { + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'user', + content: 'Peanut Butter or Jelly?', + name: 'hunter2' + }, + { + role: 'assistant', + content: 'Are you allergic to peanuts?', + name: 'hal' + }, + { + role: 'user', + content: 'Deathly allergic!', + name: 'hunter2' + } + ] + } + + if (semver.satisfies(realVersion, '>=4.0.0')) { + await openai.chat.completions.create(params) + } else { + await openai.createChatCompletion(params) + } + + await checkTraces + }) + }) + describe('without initialization', () => { it('should not error', (done) => { spawn('node', ['no-init'], { diff --git a/packages/datadog-plugin-playwright/src/index.js b/packages/datadog-plugin-playwright/src/index.js index 56601abf051..f75fee37a3d 100644 --- a/packages/datadog-plugin-playwright/src/index.js +++ b/packages/datadog-plugin-playwright/src/index.js @@ -11,12 +11,15 @@ const { TEST_SOURCE_START, TEST_CODE_OWNERS, TEST_SOURCE_FILE, - TEST_CONFIGURATION_BROWSER_NAME, + TEST_PARAMETERS, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, TELEMETRY_TEST_SESSION, - TEST_RETRY_REASON + TEST_RETRY_REASON, + TEST_MANAGEMENT_IS_QUARANTINED, + TEST_MANAGEMENT_ENABLED, + TEST_BROWSER_NAME } = require('../../dd-trace/src/plugins/util/test') const { RESOURCE_NAME } = require('../../../ext/tags') const { COMPONENT } = require('../../dd-trace/src/constants') @@ -38,7 +41,12 @@ class PlaywrightPlugin extends CiPlugin { this.numFailedTests = 0 this.numFailedSuites = 0 - this.addSub('ci:playwright:session:finish', ({ status, isEarlyFlakeDetectionEnabled, onDone }) => { + this.addSub('ci:playwright:session:finish', ({ + status, + isEarlyFlakeDetectionEnabled, + isQuarantinedTestsEnabled, + onDone + }) => { this.testModuleSpan.setTag(TEST_STATUS, status) this.testSessionSpan.setTag(TEST_STATUS, status) @@ -56,6 +64,10 @@ class PlaywrightPlugin extends CiPlugin { this.testSessionSpan.setTag('error', error) } + if (isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } + this.testModuleSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() @@ -128,7 +140,16 @@ class PlaywrightPlugin extends CiPlugin { this.enter(span, store) }) - this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry, isRetry }) => { + this.addSub('ci:playwright:test:finish', ({ + testStatus, + steps, + error, + extraTags, + isNew, + isEfdRetry, + isRetry, + isQuarantined + }) => { const store = storage('legacy').getStore() const span = store && store.span if (!span) return @@ -151,6 +172,9 @@ class PlaywrightPlugin extends CiPlugin { if (isRetry) { span.setTag(TEST_IS_RETRY, 'true') } + if (isQuarantined) { + span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true') + } steps.forEach(step => { const stepStartTime = step.startTime.getTime() @@ -202,7 +226,9 @@ class PlaywrightPlugin extends CiPlugin { extraTags[TEST_SOURCE_FILE] = testSourceFile || testSuite } if (browserName) { - extraTags[TEST_CONFIGURATION_BROWSER_NAME] = browserName + // Added as parameter too because it should affect the test fingerprint + extraTags[TEST_PARAMETERS] = JSON.stringify({ arguments: { browser: browserName }, metadata: {} }) + extraTags[TEST_BROWSER_NAME] = browserName } return super.startTestSpan(testName, testSuite, testSuiteSpan, extraTags) diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index 2aa88f2f38b..6b0b5cabf60 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -18,7 +18,9 @@ const { TEST_IS_NEW, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, - TEST_RETRY_REASON + TEST_RETRY_REASON, + TEST_MANAGEMENT_ENABLED, + TEST_MANAGEMENT_IS_QUARANTINED } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -48,6 +50,20 @@ class VitestPlugin extends CiPlugin { onDone(!testsForThisTestSuite.includes(testName)) }) + this.addSub('ci:vitest:test:is-quarantined', ({ quarantinedTests, testSuiteAbsolutePath, testName, onDone }) => { + const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot) + const isQuarantined = quarantinedTests + ?.vitest + ?.suites + ?.[testSuite] + ?.tests + ?.[testName] + ?.properties + ?.quarantined + + onDone(isQuarantined ?? false) + }) + this.addSub('ci:vitest:is-early-flake-detection-faulty', ({ knownTests, testFilepaths, @@ -66,6 +82,7 @@ class VitestPlugin extends CiPlugin { testSuiteAbsolutePath, isRetry, isNew, + isQuarantined, mightHitProbe, isRetryReasonEfd }) => { @@ -84,6 +101,9 @@ class VitestPlugin extends CiPlugin { if (isRetryReasonEfd) { extraTags[TEST_RETRY_REASON] = 'efd' } + if (isQuarantined) { + extraTags[TEST_MANAGEMENT_IS_QUARANTINED] = 'true' + } const span = this.startTestSpan( testName, @@ -257,6 +277,7 @@ class VitestPlugin extends CiPlugin { testCodeCoverageLinesTotal, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, + isQuarantinedTestsEnabled, onFinish }) => { this.testSessionSpan.setTag(TEST_STATUS, status) @@ -275,6 +296,9 @@ class VitestPlugin extends CiPlugin { if (isEarlyFlakeDetectionFaulty) { this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty') } + if (isQuarantinedTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } this.testModuleSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() diff --git a/packages/dd-trace/src/appsec/blocked_templates.js b/packages/dd-trace/src/appsec/blocked_templates.js index 3017d4de9db..6a90c034ee2 100644 --- a/packages/dd-trace/src/appsec/blocked_templates.js +++ b/packages/dd-trace/src/appsec/blocked_templates.js @@ -1,11 +1,11 @@ /* eslint-disable @stylistic/js/max-len */ 'use strict' -const html = `You've been blocked

Sorry, you cannot access this page. Please contact the customer service team.

` +const html = 'You\'ve been blocked

Sorry, you cannot access this page. Please contact the customer service team.

' -const json = `{"errors":[{"title":"You've been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}` +const json = '{"errors":[{"title":"You\'ve been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}' -const graphqlJson = `{"errors":[{"message":"You've been blocked","extensions":{"detail":"Sorry, you cannot perform this operation. Please contact the customer service team. Security provided by Datadog."}}]}` +const graphqlJson = '{"errors":[{"message":"You\'ve been blocked","extensions":{"detail":"Sorry, you cannot perform this operation. Please contact the customer service team. Security provided by Datadog."}}]}' module.exports = { html, diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index df892054786..8e0715a2d5d 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -70,7 +70,7 @@ class TestVisDynamicInstrumentation { env: { ...process.env, DD_TRACE_ENABLED: 0, - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 0 + DD_TEST_FAILED_TEST_REPLAY_ENABLED: 0 }, workerData: { config: config.serialize(), diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index de41291da73..19b5df31f22 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -1,5 +1,5 @@ 'use strict' -const path = require('path') + const { workerData: { breakpointSetChannel, @@ -8,10 +8,11 @@ const { } } = require('worker_threads') const { randomUUID } = require('crypto') -const sourceMap = require('source-map') // TODO: move debugger/devtools_client/session to common place const session = require('../../../debugger/devtools_client/session') +// TODO: move debugger/devtools_client/source-maps to common place +const { getGeneratedPosition } = require('../../../debugger/devtools_client/source-maps') // TODO: move debugger/devtools_client/snapshot to common place const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot') // TODO: move debugger/devtools_client/state to common place @@ -98,17 +99,23 @@ async function addBreakpoint (probe) { throw new Error(`No loaded script found for ${file}`) } - const [path, scriptId, sourceMapURL] = script + const { url, scriptId, sourceMapURL, source } = script - log.warn(`Adding breakpoint at ${path}:${line}`) + log.warn(`Adding breakpoint at ${url}:${line}`) let lineNumber = line + let columnNumber = 0 - if (sourceMapURL && sourceMapURL.startsWith('data:')) { + if (sourceMapURL) { try { - lineNumber = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) + ({ line: lineNumber, column: columnNumber } = await getGeneratedPosition(url, source, line, sourceMapURL)) } catch (err) { - log.error('Error processing script with inline source map', err) + log.error('Error processing script with source map', err) + } + if (lineNumber === null) { + log.error('Could not find generated position for %s:%s', url, line) + lineNumber = line + columnNumber = 0 } } @@ -116,14 +123,15 @@ async function addBreakpoint (probe) { const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: lineNumber - 1 + lineNumber: lineNumber - 1, + columnNumber } }) breakpointIdToProbe.set(breakpointId, probe) probeIdToBreakpointId.set(probe.id, breakpointId) } catch (e) { - log.error(`Error setting breakpoint at ${path}:${line}:`, e) + log.error('Error setting breakpoint at %s:%s', url, line, e) } } @@ -131,43 +139,3 @@ function start () { sessionStarted = true return session.post('Debugger.enable') // return instead of await to reduce number of promises created } - -async function processScriptWithInlineSourceMap (params) { - const { file, line, sourceMapURL } = params - - // Extract the base64-encoded source map - const base64SourceMap = sourceMapURL.split('base64,')[1] - - // Decode the base64 source map - const decodedSourceMap = Buffer.from(base64SourceMap, 'base64').toString('utf8') - - // Parse the source map - const consumer = await new sourceMap.SourceMapConsumer(decodedSourceMap) - - let generatedPosition - - // Map to the generated position. We'll attempt with the full file path first, then with the basename. - // TODO: figure out why sometimes the full path doesn't work - generatedPosition = consumer.generatedPositionFor({ - source: file, - line, - column: 0 - }) - if (generatedPosition.line === null) { - generatedPosition = consumer.generatedPositionFor({ - source: path.basename(file), - line, - column: 0 - }) - } - - consumer.destroy() - - // If we can't find the line, just return the original line - if (generatedPosition.line === null) { - log.error(`Could not find generated position for ${file}:${line}`) - return line - } - - return generatedPosition.line -} diff --git a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js index 3cbd64afbc2..c738ec68ff1 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +++ b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js @@ -6,6 +6,7 @@ const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata' const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration') const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites') const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests') +const { getQuarantinedTests: getQuarantinedTestsRequest } = require('../quarantined-tests/get-quarantined-tests') const log = require('../../log') const AgentInfoExporter = require('../../exporters/common/agent-info-exporter') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags') @@ -92,6 +93,14 @@ class CiVisibilityExporter extends AgentInfoExporter { ) } + shouldRequestQuarantinedTests () { + return !!( + this._canUseCiVisProtocol && + this._config.isTestManagementEnabled && + this._libraryConfig?.isQuarantinedTestsEnabled + ) + } + shouldRequestLibraryConfiguration () { return this._config.isIntelligentTestRunnerEnabled } @@ -138,6 +147,13 @@ class CiVisibilityExporter extends AgentInfoExporter { getKnownTestsRequest(this.getRequestConfiguration(testConfiguration), callback) } + getQuarantinedTests (testConfiguration, callback) { + if (!this.shouldRequestQuarantinedTests()) { + return callback(null) + } + getQuarantinedTestsRequest(this.getRequestConfiguration(testConfiguration), callback) + } + /** * We can't request library configuration until we know whether we can use the * CI Visibility Protocol, hence the this._canUseCiVisProtocol promise. @@ -197,7 +213,8 @@ class CiVisibilityExporter extends AgentInfoExporter { earlyFlakeDetectionFaultyThreshold, isFlakyTestRetriesEnabled, isDiEnabled, - isKnownTestsEnabled + isKnownTestsEnabled, + isQuarantinedTestsEnabled } = remoteConfiguration return { isCodeCoverageEnabled, @@ -210,7 +227,8 @@ class CiVisibilityExporter extends AgentInfoExporter { isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && this._config.isFlakyTestRetriesEnabled, flakyTestRetriesCount: this._config.flakyTestRetriesCount, isDiEnabled: isDiEnabled && this._config.isTestDynamicInstrumentationEnabled, - isKnownTestsEnabled + isKnownTestsEnabled, + isQuarantinedTestsEnabled: isQuarantinedTestsEnabled && this._config.isTestManagementEnabled } } diff --git a/packages/dd-trace/src/ci-visibility/quarantined-tests/get-quarantined-tests.js b/packages/dd-trace/src/ci-visibility/quarantined-tests/get-quarantined-tests.js new file mode 100644 index 00000000000..bc8c40a9c22 --- /dev/null +++ b/packages/dd-trace/src/ci-visibility/quarantined-tests/get-quarantined-tests.js @@ -0,0 +1,62 @@ +const request = require('../../exporters/common/request') +const id = require('../../id') + +function getQuarantinedTests ({ + url, + isEvpProxy, + evpProxyPrefix, + isGzipCompatible, + repositoryUrl +}, done) { + const options = { + path: '/api/v2/test/libraries/test-management/tests', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 20000, + url + } + + if (isGzipCompatible) { + options.headers['accept-encoding'] = 'gzip' + } + + if (isEvpProxy) { + options.path = `${evpProxyPrefix}/api/v2/test/libraries/test-management/tests` + options.headers['X-Datadog-EVP-Subdomain'] = 'api' + } else { + const apiKey = process.env.DATADOG_API_KEY || process.env.DD_API_KEY + if (!apiKey) { + return done(new Error('Quarantined tests were not fetched because Datadog API key is not defined.')) + } + + options.headers['dd-api-key'] = apiKey + } + + const data = JSON.stringify({ + data: { + id: id().toString(10), + type: 'ci_app_libraries_tests_request', + attributes: { + repository_url: repositoryUrl + } + } + }) + + request(data, options, (err, res) => { + if (err) { + done(err) + } else { + try { + const { data: { attributes: { modules: quarantinedTests } } } = JSON.parse(res) + + done(null, quarantinedTests) + } catch (err) { + done(err) + } + } + }) +} + +module.exports = { getQuarantinedTests } diff --git a/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js b/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js index 26d818bcdd2..707e3bb12d4 100644 --- a/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +++ b/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js @@ -94,7 +94,8 @@ function getLibraryConfiguration ({ early_flake_detection: earlyFlakeDetectionConfig, flaky_test_retries_enabled: isFlakyTestRetriesEnabled, di_enabled: isDiEnabled, - known_tests_enabled: isKnownTestsEnabled + known_tests_enabled: isKnownTestsEnabled, + test_management: testManagementConfig } } } = JSON.parse(res) @@ -111,7 +112,9 @@ function getLibraryConfiguration ({ earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD, isFlakyTestRetriesEnabled, isDiEnabled: isDiEnabled && isFlakyTestRetriesEnabled, - isKnownTestsEnabled + isKnownTestsEnabled, + // TODO: should it be test management? + isQuarantinedTestsEnabled: (testManagementConfig?.enabled ?? false) } log.debug(() => `Remote settings: ${JSON.stringify(settings)}`) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index f0481c64f7c..2a8dd621004 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -520,6 +520,8 @@ class Config { this._setValue(defaults, 'legacyBaggageEnabled', true) this._setValue(defaults, 'isTestDynamicInstrumentationEnabled', false) this._setValue(defaults, 'isServiceUserProvided', false) + this._setValue(defaults, 'testManagementAttemptToFixRetries', 20) + this._setValue(defaults, 'isTestManagementEnabled', false) this._setValue(defaults, 'logInjection', false) this._setValue(defaults, 'lookup', undefined) this._setValue(defaults, 'inferredProxyServicesEnabled', false) @@ -1142,7 +1144,9 @@ class Config { DD_CIVISIBILITY_FLAKY_RETRY_COUNT, DD_TEST_SESSION_NAME, DD_AGENTLESS_LOG_SUBMISSION_ENABLED, - DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED + DD_TEST_FAILED_TEST_REPLAY_ENABLED, + DD_TEST_MANAGEMENT_ENABLED, + DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES } = process.env if (DD_CIVISIBILITY_AGENTLESS_URL) { @@ -1160,8 +1164,13 @@ class Config { this._setBoolean(calc, 'isManualApiEnabled', !isFalse(this._isCiVisibilityManualApiEnabled())) this._setString(calc, 'ciVisibilityTestSessionName', DD_TEST_SESSION_NAME) this._setBoolean(calc, 'ciVisAgentlessLogSubmissionEnabled', isTrue(DD_AGENTLESS_LOG_SUBMISSION_ENABLED)) - this._setBoolean(calc, 'isTestDynamicInstrumentationEnabled', isTrue(DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED)) + this._setBoolean(calc, 'isTestDynamicInstrumentationEnabled', !isFalse(DD_TEST_FAILED_TEST_REPLAY_ENABLED)) this._setBoolean(calc, 'isServiceUserProvided', !!this._env.service) + this._setBoolean(calc, 'isTestManagementEnabled', !isFalse(DD_TEST_MANAGEMENT_ENABLED)) + this._setValue(calc, + 'testManagementAttemptToFixRetries', + coalesce(maybeInt(DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES), 20) + ) } this._setString(calc, 'dogstatsd.hostname', this._getHostname()) this._setBoolean(calc, 'isGitUploadEnabled', diff --git a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js index a93f587a5b4..229abf42df6 100644 --- a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +++ b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js @@ -1,5 +1,6 @@ 'use strict' +const { getGeneratedPosition } = require('./source-maps') const session = require('./session') const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults') const { findScriptFromPartialPath, probes, breakpoints } = require('./state') @@ -16,10 +17,11 @@ async function addBreakpoint (probe) { if (!sessionStarted) await start() const file = probe.where.sourceFile - const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints + let lineNumber = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints + let columnNumber = 0 // Probes do not contain/support column information // Optimize for sending data to /debugger/v1/input endpoint - probe.location = { file, lines: [String(line)] } + probe.location = { file, lines: [String(lineNumber)] } delete probe.where // Optimize for fast calculations when probe is hit @@ -34,17 +36,22 @@ async function addBreakpoint (probe) { // not continue untill all scripts have been parsed? const script = findScriptFromPartialPath(file) if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) - const [path, scriptId] = script + const { url, scriptId, sourceMapURL, source } = script + + if (sourceMapURL) { + ({ line: lineNumber, column: columnNumber } = await getGeneratedPosition(url, source, lineNumber, sourceMapURL)) + } log.debug( - '[debugger:devtools_client] Adding breakpoint at %s:%d (probe: %s, version: %d)', - path, line, probe.id, probe.version + '[debugger:devtools_client] Adding breakpoint at %s:%d:%d (probe: %s, version: %d)', + url, lineNumber, columnNumber, probe.id, probe.version ) const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: line - 1 // Beware! lineNumber is zero-indexed + lineNumber: lineNumber - 1, // Beware! lineNumber is zero-indexed + columnNumber } }) @@ -66,7 +73,7 @@ async function removeBreakpoint ({ id }) { probes.delete(id) breakpoints.delete(breakpointId) - if (breakpoints.size === 0) await stop() + if (breakpoints.size === 0) return stop() // return instead of await to reduce number of promises created } async function start () { diff --git a/packages/dd-trace/src/debugger/devtools_client/source-maps.js b/packages/dd-trace/src/debugger/devtools_client/source-maps.js new file mode 100644 index 00000000000..36e12f3e5bd --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/source-maps.js @@ -0,0 +1,50 @@ +'use strict' + +const { join, dirname } = require('path') +const { readFileSync } = require('fs') +const { readFile } = require('fs/promises') +const { SourceMapConsumer } = require('source-map') + +const cache = new Map() +let cacheTimer = null + +const self = module.exports = { + async loadSourceMap (dir, url) { + if (url.startsWith('data:')) return loadInlineSourceMap(url) + const path = join(dir, url) + if (cache.has(path)) return cache.get(path) + return cacheIt(path, JSON.parse(await readFile(path, 'utf8'))) + }, + + loadSourceMapSync (dir, url) { + if (url.startsWith('data:')) return loadInlineSourceMap(url) + const path = join(dir, url) + if (cache.has(path)) return cache.get(path) + return cacheIt(path, JSON.parse(readFileSync(path, 'utf8'))) + }, + + async getGeneratedPosition (url, source, line, sourceMapURL) { + const dir = dirname(new URL(url).pathname) + return await SourceMapConsumer.with( + await self.loadSourceMap(dir, sourceMapURL), + null, + (consumer) => consumer.generatedPositionFor({ source, line, column: 0 }) + ) + } +} + +function cacheIt (key, value) { + clearTimeout(cacheTimer) + cacheTimer = setTimeout(function () { + // Optimize for app boot, where a lot of reads might happen + // Clear cache a few seconds after it was last used + cache.clear() + }, 10_000).unref() + cache.set(key, value) + return value +} + +function loadInlineSourceMap (data) { + data = data.slice(data.indexOf('base64,') + 7) + return JSON.parse(Buffer.from(data, 'base64').toString('utf8')) +} diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index 4c16f336233..389bc6591b6 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -1,10 +1,13 @@ 'use strict' +const { join, dirname } = require('path') +const { loadSourceMapSync } = require('./source-maps') const session = require('./session') +const log = require('../../log') const WINDOWS_DRIVE_LETTER_REGEX = /[a-zA-Z]/ -const scriptIds = [] +const loadedScripts = [] const scriptUrls = new Map() module.exports = { @@ -15,18 +18,17 @@ module.exports = { * Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths. * * @param {string} path - Partial or absolute path to match against loaded scripts - * @returns {[string, string, string | undefined] | null} - Array containing [url, scriptId, sourceMapURL] - * or null if no match + * @returns {Object | null} - Object containing `url`, `scriptId`, `sourceMapURL`, and `source` - or null if no match */ findScriptFromPartialPath (path) { if (!path) return null // This shouldn't happen, but better safe than sorry path = path.toLowerCase() - const bestMatch = new Array(3) + const bestMatch = { url: null, scriptId: null, sourceMapURL: null, source: null } let maxMatchLength = -1 - for (const [url, scriptId, sourceMapURL] of scriptIds) { + for (const { url, sourceUrl, scriptId, sourceMapURL, source } of loadedScripts) { let i = url.length - 1 let j = path.length - 1 let matchLength = 0 @@ -75,12 +77,13 @@ module.exports = { // If we found a valid match and it's better than our previous best if (atBoundary && ( lastBoundaryPos > maxMatchLength || - (lastBoundaryPos === maxMatchLength && url.length < bestMatch[0].length) // Prefer shorter paths + (lastBoundaryPos === maxMatchLength && url.length < bestMatch.url.length) // Prefer shorter paths )) { maxMatchLength = lastBoundaryPos - bestMatch[0] = url - bestMatch[1] = scriptId - bestMatch[2] = sourceMapURL + bestMatch.url = sourceUrl || url + bestMatch.scriptId = scriptId + bestMatch.sourceMapURL = sourceMapURL + bestMatch.source = source } } @@ -112,6 +115,31 @@ module.exports = { session.on('Debugger.scriptParsed', ({ params }) => { scriptUrls.set(params.scriptId, params.url) if (params.url.startsWith('file:')) { - scriptIds.push([params.url, params.scriptId, params.sourceMapURL]) + if (params.sourceMapURL) { + const dir = dirname(new URL(params.url).pathname) + let sources + try { + sources = loadSourceMapSync(dir, params.sourceMapURL).sources + } catch (err) { + if (typeof params.sourceMapURL === 'string' && params.sourceMapURL.startsWith('data:')) { + log.error('[debugger:devtools_client] could not load inline source map for "%s"', params.url, err) + } else { + log.error('[debugger:devtools_client] could not load source map "%s" from "%s" for "%s"', + params.sourceMapURL, dir, params.url, err) + } + return + } + for (const source of sources) { + // TODO: Take source map `sourceRoot` into account? + loadedScripts.push({ + ...params, + sourceUrl: params.url, + url: new URL(join(dir, source), 'file:').href, + source + }) + } + } else { + loadedScripts.push(params) + } } }) diff --git a/packages/dd-trace/src/llmobs/tagger.js b/packages/dd-trace/src/llmobs/tagger.js index edffe4065f0..ae7f0e0e35f 100644 --- a/packages/dd-trace/src/llmobs/tagger.js +++ b/packages/dd-trace/src/llmobs/tagger.js @@ -100,7 +100,12 @@ class LLMObsTagger { } tagMetadata (span, metadata) { - this._setTag(span, METADATA, metadata) + const existingMetadata = registry.get(span)?.[METADATA] + if (existingMetadata) { + Object.assign(existingMetadata, metadata) + } else { + this._setTag(span, METADATA, metadata) + } } tagMetrics (span, metrics) { @@ -128,7 +133,12 @@ class LLMObsTagger { } } - this._setTag(span, METRICS, filterdMetrics) + const existingMetrics = registry.get(span)?.[METRICS] + if (existingMetrics) { + Object.assign(existingMetrics, filterdMetrics) + } else { + this._setTag(span, METRICS, filterdMetrics) + } } tagSpanTags (span, tags) { diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index d08462a813c..6206d0252e4 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -50,7 +50,7 @@ module.exports = class CiPlugin extends Plugin { this.addSub(`ci:${this.constructor.id}:library-configuration`, ({ onDone }) => { if (!this.tracer._exporter || !this.tracer._exporter.getLibraryConfiguration) { - return onDone({ err: new Error('CI Visibility was not initialized correctly') }) + return onDone({ err: new Error('Test optimization was not initialized correctly') }) } this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => { if (err) { @@ -64,7 +64,7 @@ module.exports = class CiPlugin extends Plugin { this.addSub(`ci:${this.constructor.id}:test-suite:skippable`, ({ onDone }) => { if (!this.tracer._exporter?.getSkippableSuites) { - return onDone({ err: new Error('CI Visibility was not initialized correctly') }) + return onDone({ err: new Error('Test optimization was not initialized correctly') }) } this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => { if (err) { @@ -153,7 +153,7 @@ module.exports = class CiPlugin extends Plugin { this.addSub(`ci:${this.constructor.id}:known-tests`, ({ onDone }) => { if (!this.tracer._exporter?.getKnownTests) { - return onDone({ err: new Error('CI Visibility was not initialized correctly') }) + return onDone({ err: new Error('Test optimization was not initialized correctly') }) } this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => { if (err) { @@ -164,6 +164,19 @@ module.exports = class CiPlugin extends Plugin { onDone({ err, knownTests }) }) }) + + this.addSub(`ci:${this.constructor.id}:quarantined-tests`, ({ onDone }) => { + if (!this.tracer._exporter?.getQuarantinedTests) { + return onDone({ err: new Error('Test optimization was not initialized correctly') }) + } + this.tracer._exporter.getQuarantinedTests(this.testConfiguration, (err, quarantinedTests) => { + if (err) { + log.error('Quarantined tests could not be fetched. %s', err.message) + this.libraryConfig.isQuarantinedTestsEnabled = false + } + onDone({ err, quarantinedTests }) + }) + }) } get telemetry () { @@ -189,15 +202,15 @@ module.exports = class CiPlugin extends Plugin { configure (config, shouldGetEnvironmentData = true) { super.configure(config) + if (!shouldGetEnvironmentData) { + return + } + if (config.isTestDynamicInstrumentationEnabled && !this.di) { const testVisibilityDynamicInstrumentation = require('../ci-visibility/dynamic-instrumentation') this.di = testVisibilityDynamicInstrumentation } - if (!shouldGetEnvironmentData) { - return - } - this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config) const { diff --git a/packages/dd-trace/src/plugins/database.js b/packages/dd-trace/src/plugins/database.js index 9296ae46d6d..cd688133761 100644 --- a/packages/dd-trace/src/plugins/database.js +++ b/packages/dd-trace/src/plugins/database.js @@ -63,25 +63,35 @@ class DatabasePlugin extends StoragePlugin { return tracerService } - injectDbmQuery (span, query, serviceName, isPreparedStatement = false) { + createDbmComment (span, serviceName, isPreparedStatement = false) { const mode = this.config.dbmPropagationMode const dbmService = this.getDbmServiceName(span, serviceName) if (mode === 'disabled') { - return query + return null } const servicePropagation = this.createDBMPropagationCommentService(dbmService, span) if (isPreparedStatement || mode === 'service') { - return `/*${servicePropagation}*/ ${query}` + return servicePropagation } else if (mode === 'full') { span.setTag('_dd.dbm_trace_injected', 'true') const traceparent = span._spanContext.toTraceparent() - return `/*${servicePropagation},traceparent='${traceparent}'*/ ${query}` + return `${servicePropagation},traceparent='${traceparent}'` } } + injectDbmQuery (span, query, serviceName, isPreparedStatement = false) { + const dbmTraceComment = this.createDbmComment(span, serviceName, isPreparedStatement) + + if (!dbmTraceComment) { + return query + } + + return `/*${dbmTraceComment}*/ ${query}` + } + maybeTruncate (query) { const maxLength = typeof this.config.truncate === 'number' ? this.config.truncate diff --git a/packages/dd-trace/src/plugins/util/inferred_proxy.js b/packages/dd-trace/src/plugins/util/inferred_proxy.js index 83628084ead..5ee1664ceb5 100644 --- a/packages/dd-trace/src/plugins/util/inferred_proxy.js +++ b/packages/dd-trace/src/plugins/util/inferred_proxy.js @@ -2,7 +2,6 @@ const log = require('../../log') const tags = require('../../../../../ext/tags') const RESOURCE_NAME = tags.RESOURCE_NAME -const SPAN_KIND = tags.SPAN_KIND const SPAN_TYPE = tags.SPAN_TYPE const HTTP_URL = tags.HTTP_URL const HTTP_METHOD = tags.HTTP_METHOD @@ -49,7 +48,6 @@ function createInferredProxySpan (headers, childOf, tracer, context) { tags: { service: proxyContext.domainName || tracer._config.service, component: proxySpanInfo.component, - [SPAN_KIND]: 'internal', [SPAN_TYPE]: 'web', [HTTP_METHOD]: proxyContext.method, [HTTP_URL]: proxyContext.domainName + proxyContext.path, @@ -71,7 +69,7 @@ function createInferredProxySpan (headers, childOf, tracer, context) { function setInferredProxySpanTags (span, proxyContext) { span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`) - span.setTag('_dd.inferred_span', '1') + span.setTag('_dd.inferred_span', 1) return span } diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 407676d5c57..676acae1770 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -52,8 +52,6 @@ const TEST_MODULE_ID = 'test_module_id' const TEST_SUITE_ID = 'test_suite_id' const TEST_TOOLCHAIN = 'test.toolchain' const TEST_SKIPPED_BY_ITR = 'test.skipped_by_itr' -// Browser used in browser test. Namespaced by test.configuration because it affects the fingerprint -const TEST_CONFIGURATION_BROWSER_NAME = 'test.configuration.browser_name' // Early flake detection const TEST_IS_NEW = 'test.is_new' const TEST_IS_RETRY = 'test.is_retry' @@ -117,6 +115,9 @@ const DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX = 'snapshot_id' const DI_DEBUG_ERROR_FILE_SUFFIX = 'file' const DI_DEBUG_ERROR_LINE_SUFFIX = 'line' +const TEST_MANAGEMENT_IS_QUARANTINED = 'test.test_management.is_quarantined' +const TEST_MANAGEMENT_ENABLED = 'test.test_management.enabled' + module.exports = { TEST_CODE_OWNERS, TEST_SESSION_NAME, @@ -143,7 +144,6 @@ module.exports = { MOCHA_WORKER_TRACE_PAYLOAD_CODE, TEST_SOURCE_START, TEST_SKIPPED_BY_ITR, - TEST_CONFIGURATION_BROWSER_NAME, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, @@ -202,7 +202,9 @@ module.exports = { DI_DEBUG_ERROR_FILE_SUFFIX, DI_DEBUG_ERROR_LINE_SUFFIX, getFormattedError, - DD_TEST_IS_USER_PROVIDED_SERVICE + DD_TEST_IS_USER_PROVIDED_SERVICE, + TEST_MANAGEMENT_IS_QUARANTINED, + TEST_MANAGEMENT_ENABLED } // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19 diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 8f0331d536e..9a481e375d1 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -1638,7 +1638,7 @@ describe('Config', () => { it('should not transform the lookup parameter', () => { const lookup = () => 'test' - const config = new Config({ lookup: lookup }) + const config = new Config({ lookup }) expect(config.lookup).to.equal(lookup) }) @@ -2039,7 +2039,7 @@ describe('Config', () => { delete process.env.DD_CIVISIBILITY_FLAKY_RETRY_COUNT delete process.env.DD_TEST_SESSION_NAME delete process.env.JEST_WORKER_ID - delete process.env.DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED + delete process.env.DD_TEST_FAILED_TEST_REPLAY_ENABLED delete process.env.DD_AGENTLESS_LOG_SUBMISSION_ENABLED options = {} }) @@ -2138,15 +2138,16 @@ describe('Config', () => { const config = new Config(options) expect(config).to.have.property('ciVisAgentlessLogSubmissionEnabled', true) }) - it('should not set isTestDynamicInstrumentationEnabled by default', () => { - const config = new Config(options) - expect(config).to.have.property('isTestDynamicInstrumentationEnabled', false) - }) - it('should set isTestDynamicInstrumentationEnabled if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is passed', () => { - process.env.DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED = 'true' + it('should set isTestDynamicInstrumentationEnabled by default', () => { const config = new Config(options) expect(config).to.have.property('isTestDynamicInstrumentationEnabled', true) }) + it('should set isTestDynamicInstrumentationEnabled to false if DD_TEST_FAILED_TEST_REPLAY_ENABLED is false', + () => { + process.env.DD_TEST_FAILED_TEST_REPLAY_ENABLED = 'false' + const config = new Config(options) + expect(config).to.have.property('isTestDynamicInstrumentationEnabled', false) + }) }) context('ci visibility mode is not enabled', () => { it('should not activate intelligent test runner or git metadata upload', () => { diff --git a/packages/dd-trace/test/debugger/devtools_client/source-maps.spec.js b/packages/dd-trace/test/debugger/devtools_client/source-maps.spec.js new file mode 100644 index 00000000000..68cbea0986c --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/source-maps.spec.js @@ -0,0 +1,168 @@ +'use strict' + +require('../../setup/mocha') + +const parsedSourceMap = { + version: 3, + file: 'index.js', + sourceRoot: '', + sources: ['index.ts'], + names: [], + mappings: ';AAAA,MAAM,UAAU,GAAG,IAAI,CAAC;AACxB,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC' +} +const dir = '/foo' +const sourceMapURL = 'index.map.js' +const rawSourceMap = JSON.stringify(parsedSourceMap) +const inlineSourceMap = `data:application/json;base64,${Buffer.from(rawSourceMap).toString('base64')}` + +describe('source map utils', function () { + let loadSourceMap, loadSourceMapSync, getGeneratedPosition, readFileSync, readFile + + describe('basic', function () { + beforeEach(function () { + readFileSync = sinon.stub().returns(rawSourceMap) + readFile = sinon.stub().resolves(rawSourceMap) + + const sourceMaps = proxyquire('../src/debugger/devtools_client/source-maps', { + fs: { readFileSync }, + 'fs/promises': { readFile } + }) + + loadSourceMap = sourceMaps.loadSourceMap + loadSourceMapSync = sourceMaps.loadSourceMapSync + getGeneratedPosition = sourceMaps.getGeneratedPosition + }) + + describe('loadSourceMap', function () { + it('should return parsed inline source map', async function () { + const sourceMap = await loadSourceMap(dir, inlineSourceMap) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFile).to.not.have.been.called + }) + + it('should throw is inline source map is invalid', function (done) { + loadSourceMap(dir, inlineSourceMap.slice(0, -10)) + .then(() => { + done(new Error('Should not resolve promise')) + }) + .catch(() => { + done() + }) + }) + + it('should return parsed source map', async function () { + const sourceMap = await loadSourceMap(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFile).to.have.been.calledOnceWith('/foo/index.map.js', 'utf8') + }) + }) + + describe('loadSourceMapSync', function () { + it('should return parsed inline source map', function () { + const sourceMap = loadSourceMapSync(dir, inlineSourceMap) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFileSync).to.not.have.been.called + }) + + it('should throw if inline source map is invalid', function () { + expect(() => { + loadSourceMapSync(dir, inlineSourceMap.slice(0, -10)) + }).to.throw() + }) + + it('should return parsed source map', function () { + const sourceMap = loadSourceMapSync(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFileSync).to.have.been.calledOnceWith('/foo/index.map.js', 'utf8') + }) + }) + + describe('getGeneratedPosition', function () { + const url = `file://${dir}/${parsedSourceMap.file}` + const source = parsedSourceMap.sources[0] + const line = 1 + + it('should return expected line for inline source map', async function () { + const pos = await getGeneratedPosition(url, source, line, sourceMapURL) + expect(pos).to.deep.equal({ line: 2, column: 0, lastColumn: 5 }) + }) + + it('should return expected line for non-inline source map', async function () { + const pos = await getGeneratedPosition(url, source, line, inlineSourceMap) + expect(pos).to.deep.equal({ line: 2, column: 0, lastColumn: 5 }) + }) + }) + }) + + describe('cache', function () { + let clock + + function setup () { + clock = sinon.useFakeTimers() + readFileSync = sinon.stub().returns(rawSourceMap) + readFile = sinon.stub().resolves(rawSourceMap) + + const sourceMaps = proxyquire('../src/debugger/devtools_client/source-maps', { + fs: { readFileSync }, + 'fs/promises': { readFile } + }) + + loadSourceMap = sourceMaps.loadSourceMap + loadSourceMapSync = sourceMaps.loadSourceMapSync + } + + function teardown () { + clock.restore() + } + + describe('loadSourceMap', function () { + before(setup) + + after(teardown) + + it('should read from disk on the fist call', async function () { + const sourceMap = await loadSourceMap(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFile.callCount).to.equal(1) + }) + + it('should not read from disk on the second call', async function () { + const sourceMap = await loadSourceMap(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFile.callCount).to.equal(1) + }) + + it('should clear cache after 10 seconds', async function () { + clock.tick(10_000) + const sourceMap = await loadSourceMap(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFile.callCount).to.equal(2) + }) + }) + + describe('loadSourceMapSync', function () { + before(setup) + + after(teardown) + + it('should read from disk on the fist call', function () { + const sourceMap = loadSourceMapSync(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFileSync.callCount).to.equal(1) + }) + + it('should not read from disk on the second call', function () { + const sourceMap = loadSourceMapSync(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFileSync.callCount).to.equal(1) + }) + + it('should clear cache after 10 seconds', function () { + clock.tick(10_000) + const sourceMap = loadSourceMapSync(dir, sourceMapURL) + expect(sourceMap).to.deep.equal(parsedSourceMap) + expect(readFileSync.callCount).to.equal(2) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/debugger/devtools_client/state.spec.js b/packages/dd-trace/test/debugger/devtools_client/state.spec.js index dced50d51e3..133b0b72049 100644 --- a/packages/dd-trace/test/debugger/devtools_client/state.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/state.spec.js @@ -14,6 +14,14 @@ describe('findScriptFromPartialPath', function () { before(function () { state = proxyquire('../src/debugger/devtools_client/state', { + './source-maps': proxyquire('../src/debugger/devtools_client/source-maps', { + fs: { + // Mock reading the source map file + readFileSync: () => JSON.stringify({ + sources: ['index.ts'] + }) + } + }), './session': { '@noCallThru': true, on (event, listener) { @@ -32,6 +40,15 @@ describe('findScriptFromPartialPath', function () { // The same, but in reverse order to ensure this doesn't influence the result listener({ params: { scriptId: 'should-match-shortest-b', url: 'file:///bar/index.js' } }) listener({ params: { scriptId: 'should-not-match-longest-b', url: 'file:///node_modules/bar/index.js' } }) + + // Test case for source maps + listener({ + params: { + scriptId: 'should-match-source-mapped', + url: 'file:///source-mapped/index.js', + sourceMapURL: 'index.js.map' + } + }) } } } @@ -117,7 +134,7 @@ describe('findScriptFromPartialPath', function () { function testPath (path) { return function () { const result = state.findScriptFromPartialPath(path) - expect(result).to.deep.equal([url, scriptId, undefined]) + expect(result).to.deep.equal({ url, scriptId, sourceMapURL: undefined, source: undefined }) } } }) @@ -126,15 +143,33 @@ describe('findScriptFromPartialPath', function () { describe('multiple partial matches', function () { it('should match the longest partial match', function () { const result = state.findScriptFromPartialPath('server/index.js') - expect(result).to.deep.equal(['file:///server/index.js', 'should-match', undefined]) + expect(result).to.deep.equal({ + url: 'file:///server/index.js', scriptId: 'should-match', sourceMapURL: undefined, source: undefined + }) }) it('should match the shorter of two equal length partial matches', function () { const result1 = state.findScriptFromPartialPath('foo/index.js') - expect(result1).to.deep.equal(['file:///foo/index.js', 'should-match-shortest-a', undefined]) + expect(result1).to.deep.equal({ + url: 'file:///foo/index.js', scriptId: 'should-match-shortest-a', sourceMapURL: undefined, source: undefined + }) const result2 = state.findScriptFromPartialPath('bar/index.js') - expect(result2).to.deep.equal(['file:///bar/index.js', 'should-match-shortest-b', undefined]) + expect(result2).to.deep.equal({ + url: 'file:///bar/index.js', scriptId: 'should-match-shortest-b', sourceMapURL: undefined, source: undefined + }) + }) + }) + + describe('source maps', function () { + it('should match the source map path', function () { + const result = state.findScriptFromPartialPath('source-mapped/index.ts') + expect(result).to.deep.equal({ + url: 'file:///source-mapped/index.js', + scriptId: 'should-match-source-mapped', + sourceMapURL: 'index.js.map', + source: 'index.ts' + }) }) }) diff --git a/packages/dd-trace/test/debugger/devtools_client/utils.js b/packages/dd-trace/test/debugger/devtools_client/utils.js index 2da3216cea1..5d0ca8fb1fe 100644 --- a/packages/dd-trace/test/debugger/devtools_client/utils.js +++ b/packages/dd-trace/test/debugger/devtools_client/utils.js @@ -28,7 +28,7 @@ function generateProbeConfig (breakpoint, overrides = {}) { version: 0, type: 'LOG_PROBE', language: 'javascript', - where: { sourceFile: breakpoint.file, lines: [String(breakpoint.line)] }, + where: { sourceFile: breakpoint.sourceFile, lines: [String(breakpoint.line)] }, tags: [], template: 'Hello World!', segments: [{ str: 'Hello World!' }], diff --git a/packages/dd-trace/test/llmobs/tagger.spec.js b/packages/dd-trace/test/llmobs/tagger.spec.js index c8f5e17c189..db8b7aabf22 100644 --- a/packages/dd-trace/test/llmobs/tagger.spec.js +++ b/packages/dd-trace/test/llmobs/tagger.spec.js @@ -168,6 +168,14 @@ describe('tagger', () => { '_ml_obs.meta.metadata': { a: 'foo', b: 'bar' } }) }) + + it('updates instead of overriding', () => { + Tagger.tagMap.set(span, { '_ml_obs.meta.metadata': { a: 'foo' } }) + tagger.tagMetadata(span, { b: 'bar' }) + expect(Tagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.metadata': { a: 'foo', b: 'bar' } + }) + }) }) describe('tagMetrics', () => { @@ -202,6 +210,14 @@ describe('tagger', () => { tagger._register(span) expect(() => tagger.tagMetrics(span, metrics)).to.throw() }) + + it('updates instead of overriding', () => { + Tagger.tagMap.set(span, { '_ml_obs.metrics': { a: 1 } }) + tagger.tagMetrics(span, { b: 2 }) + expect(Tagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.metrics': { a: 1, b: 2 } + }) + }) }) describe('tagSpanTags', () => { diff --git a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js b/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js index 0a02c149336..51d33f84389 100644 --- a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js +++ b/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js @@ -81,9 +81,8 @@ describe('Inferred Proxy Spans', function () { expect(spans[0].meta).to.have.property('http.url', 'example.com/test') expect(spans[0].meta).to.have.property('http.method', 'GET') expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'internal') expect(spans[0].meta).to.have.property('component', 'aws-apigateway') - expect(spans[0].meta).to.have.property('_dd.inferred_span', '1') + expect(spans[0].metrics).to.have.property('_dd.inferred_span', 1) expect(spans[0].start.toString()).to.be.equal('1729780025472999936') expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) @@ -129,7 +128,6 @@ describe('Inferred Proxy Spans', function () { expect(spans[0].meta).to.have.property('http.url', 'example.com/test') expect(spans[0].meta).to.have.property('http.method', 'GET') expect(spans[0].meta).to.have.property('http.status_code', '500') - expect(spans[0].meta).to.have.property('span.kind', 'internal') expect(spans[0].meta).to.have.property('component', 'aws-apigateway') expect(spans[0].error).to.be.equal(1) expect(spans[0].start.toString()).to.be.equal('1729780025472999936') diff --git a/packages/memwatch/package.json b/packages/memwatch/package.json deleted file mode 100644 index d1af0db74b1..00000000000 --- a/packages/memwatch/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "memwatch", - "version": "1.0.0", - "license": "BSD-3-Clause", - "private": true, - "dependencies": { - "@airbnb/node-memwatch": "^1.0.2" - } -} diff --git a/yarn.lock b/yarn.lock index 2c240734dd8..8504417827d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -452,115 +452,130 @@ resolved "https://registry.npmjs.org/@datadog/sketches-js/-/sketches-js-2.1.0.tgz" integrity sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew== -"@esbuild/android-arm64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.12.tgz#86c4fdd7c0d65fe9dcbe138fbe72720658ec3b88" - integrity sha512-0LacmiIW+X0/LOLMZqYtZ7d4uY9fxYABAYhSSOu+OGQVBqH4N5eIYgkT7bBFnR4Nm3qo6qS3RpHKVrDASqj/uQ== - -"@esbuild/android-arm@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.12.tgz#15e33bb1c8c2f560fbb27cda227c0fa22d83d0ef" - integrity sha512-CTWgMJtpCyCltrvipZrrcjjRu+rzm6pf9V8muCsJqtKujR3kPmU4ffbckvugNNaRmhxAF1ZI3J+0FUIFLFg8KA== - -"@esbuild/android-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.12.tgz#3b0ddaf59fdf94e8e9fcb2aa6537cbab93d5fe22" - integrity sha512-sS5CR3XBKQXYpSGMM28VuiUnbX83Z+aWPZzClW+OB2JquKqxoiwdqucJ5qvXS8pM6Up3RtJfDnRQZkz3en2z5g== - -"@esbuild/darwin-arm64@0.16.12": - version "0.16.12" - resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.12.tgz" - integrity sha512-Dpe5hOAQiQRH20YkFAg+wOpcd4PEuXud+aGgKBQa/VriPJA8zuVlgCOSTwna1CgYl05lf6o5els4dtuyk1qJxQ== - -"@esbuild/darwin-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.12.tgz#3433e6432dd474994302bcfe35c5420fae46a206" - integrity sha512-ApGRA6X5txIcxV0095X4e4KKv87HAEXfuDRcGTniDWUUN+qPia8sl/BqG/0IomytQWajnUn4C7TOwHduk/FXBQ== - -"@esbuild/freebsd-arm64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.12.tgz#b150587dc54dc2369cb826e6ee9f94fc5ec14635" - integrity sha512-AMdK2gA9EU83ccXCWS1B/KcWYZCj4P3vDofZZkl/F/sBv/fphi2oUqUTox/g5GMcIxk8CF1CVYTC82+iBSyiUg== - -"@esbuild/freebsd-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.12.tgz#e682a61cde8d6332aaeb4c2b28fce0d833928903" - integrity sha512-KUKB9w8G/xaAbD39t6gnRBuhQ8vIYYlxGT2I+mT6UGRnCGRr1+ePFIGBQmf5V16nxylgUuuWVW1zU2ktKkf6WQ== - -"@esbuild/linux-arm64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.12.tgz#d0d75e10796d4f1414ecaf16a8071ce05446cb9f" - integrity sha512-29HXMLpLklDfmw7T2buGqq3HImSUaZ1ArmrPOMaNiZZQptOSZs32SQtOHEl8xWX5vfdwZqrBfNf8Te4nArVzKQ== - -"@esbuild/linux-arm@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.12.tgz#945ebcd99205fadea5ee22bff624189bd95c0484" - integrity sha512-vhDdIv6z4eL0FJyNVfdr3C/vdd/Wc6h1683GJsFoJzfKb92dU/v88FhWdigg0i6+3TsbSDeWbsPUXb4dif2abg== - -"@esbuild/linux-ia32@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.12.tgz#132e61b2124eee6033bf7f0d5b312c02524d39db" - integrity sha512-JFDuNDTTfgD1LJg7wHA42o2uAO/9VzHYK0leAVnCQE/FdMB599YMH73ux+nS0xGr79pv/BK+hrmdRin3iLgQjg== - -"@esbuild/linux-loong64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.12.tgz#d27dc1e203c0d0516c1daadb7988f88b643f8ea2" - integrity sha512-xTGzVPqm6WKfCC0iuj1fryIWr1NWEM8DMhAIo+4rFgUtwy/lfHl+Obvus4oddzRDbBetLLmojfVZGmt/g/g+Rw== - -"@esbuild/linux-mips64el@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.12.tgz#9616c378ca76f12d06ffaf242da68a58be966a18" - integrity sha512-zI1cNgHa3Gol+vPYjIYHzKhU6qMyOQrvZ82REr5Fv7rlh5PG6SkkuCoH7IryPqR+BK2c/7oISGsvPJPGnO2bHQ== - -"@esbuild/linux-ppc64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.12.tgz#b033a248212249c05c162b64124744345a041f92" - integrity sha512-/C8OFXExoMmvTDIOAM54AhtmmuDHKoedUd0Otpfw3+AuuVGemA1nQK99oN909uZbLEU6Bi+7JheFMG3xGfZluQ== - -"@esbuild/linux-riscv64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.12.tgz#b6476abff413b5b472e6cf093086b9d5be4553a8" - integrity sha512-qeouyyc8kAGV6Ni6Isz8hUsKMr00EHgVwUKWNp1r4l88fHEoNTDB8mmestvykW6MrstoGI7g2EAsgr0nxmuGYg== - -"@esbuild/linux-s390x@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.12.tgz#981a639f8c2a2e0646f47eba0fae7c2c270b208b" - integrity sha512-s9AyI/5vz1U4NNqnacEGFElqwnHusWa81pskAf8JNDM2eb6b2E6PpBmT8RzeZv6/TxE6/TADn2g9bb0jOUmXwQ== - -"@esbuild/linux-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.12.tgz#01b777229d8baf068eeeb7cd7c396aea4d1ebd36" - integrity sha512-e8YA7GQGLWhvakBecLptUiKxOk4E/EPtSckS1i0MGYctW8ouvNUoh7xnU15PGO2jz7BYl8q1R6g0gE5HFtzpqQ== - -"@esbuild/netbsd-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.12.tgz#6d4b9de7dc3ac99bf04653fe640b3be63c57b1aa" - integrity sha512-z2+kUxmOqBS+6SRVd57iOLIHE8oGOoEnGVAmwjm2aENSP35HPS+5cK+FL1l+rhrsJOFIPrNHqDUNechpuG96Sg== - -"@esbuild/openbsd-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.12.tgz#2a28010b1848466586d5e2189e9f1b8334b65708" - integrity sha512-PAonw4LqIybwn2/vJujhbg1N9W2W8lw9RtXIvvZoyzoA/4rA4CpiuahVbASmQohiytRsixbNoIOUSjRygKXpyA== - -"@esbuild/sunos-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.12.tgz#3ee120008cc759d604825dd25501152071ef30f0" - integrity sha512-+wr1tkt1RERi+Zi/iQtkzmMH4nS8+7UIRxjcyRz7lur84wCkAITT50Olq/HiT4JN2X2bjtlOV6vt7ptW5Gw60Q== - -"@esbuild/win32-arm64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.12.tgz#8c599a91f1c55b3df304c450ac0613855c10502e" - integrity sha512-XEjeUSHmjsAOJk8+pXJu9pFY2O5KKQbHXZWQylJzQuIBeiGrpMeq9sTVrHefHxMOyxUgoKQTcaTS+VK/K5SviA== - -"@esbuild/win32-ia32@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.12.tgz#102b5a44b514f8849a10cc4cc618c60c70a4c536" - integrity sha512-eRKPM7e0IecUAUYr2alW7JGDejrFJXmpjt4MlfonmQ5Rz9HWpKFGCjuuIRgKO7W9C/CWVFXdJ2GjddsBXqQI4A== - -"@esbuild/win32-x64@0.16.12": - version "0.16.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.12.tgz#31197bb509049b63c059c4808ac58e66fdff7479" - integrity sha512-iPYKN78t3op2+erv2frW568j1q0RpqX6JOLZ7oPPaAV1VaF7dDstOrNw37PVOYoTWE11pV4A1XUitpdEFNIsPg== +"@esbuild/aix-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" + integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== + +"@esbuild/android-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" + integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== + +"@esbuild/android-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" + integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== + +"@esbuild/android-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" + integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== + +"@esbuild/darwin-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" + integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== + +"@esbuild/darwin-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" + integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== + +"@esbuild/freebsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" + integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== + +"@esbuild/freebsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" + integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== + +"@esbuild/linux-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" + integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== + +"@esbuild/linux-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" + integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== + +"@esbuild/linux-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" + integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== + +"@esbuild/linux-loong64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" + integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== + +"@esbuild/linux-mips64el@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" + integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== + +"@esbuild/linux-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" + integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== + +"@esbuild/linux-riscv64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" + integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== + +"@esbuild/linux-s390x@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" + integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== + +"@esbuild/linux-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" + integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== + +"@esbuild/netbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" + integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== + +"@esbuild/netbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" + integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== + +"@esbuild/openbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" + integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== + +"@esbuild/openbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" + integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== + +"@esbuild/sunos-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" + integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== + +"@esbuild/win32-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" + integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== + +"@esbuild/win32-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" + integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== + +"@esbuild/win32-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" + integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.4.1": version "4.4.1" @@ -2113,33 +2128,36 @@ es6-error@^4.0.1: resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@0.16.12: - version "0.16.12" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.16.12.tgz" - integrity sha512-eq5KcuXajf2OmivCl4e89AD3j8fbV+UTE9vczEzq5haA07U9oOTzBWlh3+6ZdjJR7Rz2QfWZ2uxZyhZxBgJ4+g== +esbuild@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" + integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== optionalDependencies: - "@esbuild/android-arm" "0.16.12" - "@esbuild/android-arm64" "0.16.12" - "@esbuild/android-x64" "0.16.12" - "@esbuild/darwin-arm64" "0.16.12" - "@esbuild/darwin-x64" "0.16.12" - "@esbuild/freebsd-arm64" "0.16.12" - "@esbuild/freebsd-x64" "0.16.12" - "@esbuild/linux-arm" "0.16.12" - "@esbuild/linux-arm64" "0.16.12" - "@esbuild/linux-ia32" "0.16.12" - "@esbuild/linux-loong64" "0.16.12" - "@esbuild/linux-mips64el" "0.16.12" - "@esbuild/linux-ppc64" "0.16.12" - "@esbuild/linux-riscv64" "0.16.12" - "@esbuild/linux-s390x" "0.16.12" - "@esbuild/linux-x64" "0.16.12" - "@esbuild/netbsd-x64" "0.16.12" - "@esbuild/openbsd-x64" "0.16.12" - "@esbuild/sunos-x64" "0.16.12" - "@esbuild/win32-arm64" "0.16.12" - "@esbuild/win32-ia32" "0.16.12" - "@esbuild/win32-x64" "0.16.12" + "@esbuild/aix-ppc64" "0.25.0" + "@esbuild/android-arm" "0.25.0" + "@esbuild/android-arm64" "0.25.0" + "@esbuild/android-x64" "0.25.0" + "@esbuild/darwin-arm64" "0.25.0" + "@esbuild/darwin-x64" "0.25.0" + "@esbuild/freebsd-arm64" "0.25.0" + "@esbuild/freebsd-x64" "0.25.0" + "@esbuild/linux-arm" "0.25.0" + "@esbuild/linux-arm64" "0.25.0" + "@esbuild/linux-ia32" "0.25.0" + "@esbuild/linux-loong64" "0.25.0" + "@esbuild/linux-mips64el" "0.25.0" + "@esbuild/linux-ppc64" "0.25.0" + "@esbuild/linux-riscv64" "0.25.0" + "@esbuild/linux-s390x" "0.25.0" + "@esbuild/linux-x64" "0.25.0" + "@esbuild/netbsd-arm64" "0.25.0" + "@esbuild/netbsd-x64" "0.25.0" + "@esbuild/openbsd-arm64" "0.25.0" + "@esbuild/openbsd-x64" "0.25.0" + "@esbuild/sunos-x64" "0.25.0" + "@esbuild/win32-arm64" "0.25.0" + "@esbuild/win32-ia32" "0.25.0" + "@esbuild/win32-x64" "0.25.0" escalade@^3.1.1: version "3.1.1"