Sorry, you cannot access this page. Please contact the customer service team.
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 = `
Sorry, you cannot access this page. Please contact the customer service team.
Sorry, you cannot access this page. Please contact the customer service team.