Skip to content

Commit e47226b

Browse files
fix(test-optimization): disable telemetry in test workers (#9687)
1 parent 6b6da6d commit e47226b

17 files changed

Lines changed: 310 additions & 33 deletions

File tree

ci/init.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
/* eslint-disable no-console */
4+
const exporters = require('../ext/exporters')
45
const log = require('../packages/dd-trace/src/log')
56
const { getEnvironmentVariable, getValueFromEnvSources } = require('../packages/dd-trace/src/config/helper')
67
const { isFalse, isTrue } = require('../packages/dd-trace/src/util')
@@ -13,11 +14,11 @@ const VALIDATION_MODE_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MODE'
1314
const VALIDATION_MANIFEST_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MANIFEST_FILE'
1415
const VALIDATION_OUTPUT_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_OUTPUT_DIR'
1516
const EXPORTER_MAP = {
16-
jest: 'jest_worker',
17-
cucumber: 'cucumber_worker',
18-
mocha: 'mocha_worker',
19-
playwright: 'playwright_worker',
20-
vitest: 'vitest_worker',
17+
jest: exporters.JEST_WORKER,
18+
cucumber: exporters.CUCUMBER_WORKER,
19+
mocha: exporters.MOCHA_WORKER,
20+
playwright: exporters.PLAYWRIGHT_WORKER,
21+
vitest: exporters.VITEST_WORKER,
2122
}
2223

2324
function isPackageManager () {
@@ -70,7 +71,6 @@ if (!isTestWorker && isPackageManager()) {
7071
}
7172

7273
if (isTestWorker) {
73-
baseOptions.telemetry = { enabled: false }
7474
baseOptions.experimental = {
7575
exporter: EXPORTER_MAP[testWorkerType],
7676
}

integration-tests/ci-visibility/test-optimization-startup.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,34 @@ describe('test optimization startup', () => {
147147
assert.match(processOutput, /hello!/)
148148
assert.doesNotMatch(processOutput, /dd-trace will not be initialized/)
149149
})
150+
151+
it('does not log an unknown telemetry option in a Vitest worker', async () => {
152+
childProcess = exec('node -e "console.log(\'hello!\')"',
153+
{
154+
cwd,
155+
env: {
156+
...process.env,
157+
NODE_OPTIONS: '-r dd-trace/ci/init',
158+
DD_TRACE_DEBUG: '1',
159+
TINYPOOL_WORKER_ID: '1',
160+
},
161+
}
162+
)
163+
164+
childProcess.stdout?.on('data', (chunk) => {
165+
processOutput += chunk.toString()
166+
})
167+
childProcess.stderr?.on('data', (chunk) => {
168+
processOutput += chunk.toString()
169+
})
170+
171+
await Promise.all([
172+
once(childProcess, 'exit'),
173+
once(childProcess.stdout, 'end'),
174+
once(childProcess.stderr, 'end'),
175+
])
176+
177+
assert.match(processOutput, /hello!/)
178+
assert.doesNotMatch(processOutput, /Unknown option telemetry/)
179+
})
150180
})

integration-tests/cucumber/cucumber.spec.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,49 @@ describe(`cucumber@${version} commonJS`, () => {
295295
])
296296
})
297297

298+
it('forwards telemetry from parallel workers', async () => {
299+
receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] })
300+
301+
const eventsPromise = receiver
302+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => {
303+
const events = payloads.flatMap(({ payload }) => payload.events)
304+
const testSession = events.find(event => event.type === 'test_session_end').content
305+
306+
assert.strictEqual(testSession.meta[TEST_STATUS], 'pass')
307+
})
308+
const telemetryPromise = receiver
309+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/apmtelemetry'), (payloads) => {
310+
const telemetryMetrics = payloads.flatMap(({ payload }) => payload.payload.series)
311+
const testFinishedMetric = telemetryMetrics.find(({ metric, tags }) =>
312+
metric === 'event_finished' && tags.includes('event_type:test')
313+
)
314+
315+
assert.ok(testFinishedMetric, 'test event telemetry from a worker should be sent')
316+
})
317+
318+
childProcess = exec(
319+
'./node_modules/.bin/cucumber-js ci-visibility/features/farewell.feature --parallel 2',
320+
{
321+
cwd,
322+
env: {
323+
...getCiVisEvpProxyConfig(receiver.port),
324+
DD_TRACE_AGENT_PORT: String(receiver.port),
325+
DD_INSTRUMENTATION_TELEMETRY_ENABLED: 'true',
326+
},
327+
}
328+
)
329+
childProcess.stdout?.on('data', chunk => { testOutput += chunk.toString() })
330+
childProcess.stderr?.on('data', chunk => { testOutput += chunk.toString() })
331+
332+
const [[exitCode]] = await Promise.all([
333+
once(childProcess, 'exit'),
334+
eventsPromise,
335+
telemetryPromise,
336+
])
337+
338+
assert.strictEqual(exitCode, 0, testOutput)
339+
})
340+
298341
onlyLatestIt('waits for the final payload before the programmatic run resolves', async () => {
299342
const completionOrder = []
300343
const completedMessage = 'programmatic Cucumber run completed'
@@ -620,11 +663,7 @@ describe(`cucumber@${version} commonJS`, () => {
620663
})
621664
})
622665

623-
const runModes = ['serial']
624-
625-
if (version !== '7.0.0') { // only on latest or 9 if node is old
626-
runModes.push('parallel')
627-
}
666+
const runModes = ['serial', 'parallel']
628667

629668
runModes.forEach((runMode) => {
630669
it(`(${runMode}) can run and report tests`, (done) => {

integration-tests/mocha/mocha.spec.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const assert = require('node:assert/strict')
66
const { once } = require('node:events')
77
const path = require('path')
88
const { inspect } = require('node:util')
9+
10+
const satisfies = require('semifies')
11+
912
const { assertObjectContains } = require('../helpers')
1013

1114
const {
@@ -119,6 +122,8 @@ const MOCHA_VERSION = requestedMochaVersion === 'oldest' ? oldestMochaVersion :
119122
const mochaMajor = MOCHA_VERSION === 'latest' ? Infinity : Number.parseInt(MOCHA_VERSION, 10)
120123
const supportsMochaRetryEvents = mochaMajor >= 6
121124
const onlyLatestIt = MOCHA_VERSION === 'latest' ? it : it.skip
125+
// Mocha 8.0 through 8.2 use workerpool 6.0.x, which cannot start process workers on supported Node versions.
126+
const parallelIt = MOCHA_VERSION === 'latest' || satisfies(MOCHA_VERSION, '>=8.3.0') ? it : it.skip
122127

123128
describe('mocha failed test replay helpers', () => {
124129
describe('finishDeferredHookEnd', () => {
@@ -313,6 +318,38 @@ describe(`mocha@${MOCHA_VERSION}`, function () {
313318
])
314319
})
315320

321+
parallelIt('forwards telemetry from parallel workers', async () => {
322+
receiver.setInfoResponse({ endpoints: ['/evp_proxy/v4'] })
323+
324+
const telemetryPromise = receiver
325+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/apmtelemetry'), (payloads) => {
326+
const telemetryMetrics = payloads.flatMap(({ payload }) => payload.payload.series)
327+
const testFinishedMetric = telemetryMetrics.find(({ metric, tags }) =>
328+
metric === 'event_finished' && tags.includes('event_type:test')
329+
)
330+
331+
assert.ok(testFinishedMetric, 'test event telemetry from a worker should be sent')
332+
})
333+
334+
childProcess = exec(
335+
runTestsCommand,
336+
{
337+
cwd,
338+
env: {
339+
...getCiVisEvpProxyConfig(receiver.port),
340+
DD_TRACE_AGENT_PORT: String(receiver.port),
341+
DD_INSTRUMENTATION_TELEMETRY_ENABLED: 'true',
342+
RUN_IN_PARALLEL: '1',
343+
},
344+
}
345+
)
346+
347+
await Promise.all([
348+
once(childProcess, 'exit'),
349+
telemetryPromise,
350+
])
351+
})
352+
316353
/**
317354
* @param {boolean} runInParallel
318355
* @returns {Promise<void>}

integration-tests/playwright/playwright-active-test-span.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ versions.forEach((version) => {
257257
const eventFinishedTestEvents = telemetryEvents
258258
.filter(({ metric, tags }) => metric === 'event_finished' && tags.includes('event_type:test'))
259259

260+
assert.ok(eventFinishedTestEvents.length > 0, 'test event telemetry from a worker should be sent')
260261
eventFinishedTestEvents.forEach(({ tags }) => {
261262
assert.ok(tags.includes('is_rum'), `Got: ${inspect(tags)}`)
262263
assert.ok(tags.includes('test_framework:playwright'), `Got: ${inspect(tags)}`)

packages/datadog-instrumentations/src/cucumber.js

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
getTestSuitePath,
2222
getRelativeCoverageFiles,
2323
CUCUMBER_WORKER_TRACE_PAYLOAD_CODE,
24+
CUCUMBER_WORKER_TELEMETRY_PAYLOAD_CODE,
2425
getIsFaultyEarlyFlakeDetection,
2526
applySkippedCoverageToCoverage,
2627
getTestCoverageLinesPercentage,
@@ -62,6 +63,7 @@ const modifiedFilesCh = channel('ci:cucumber:modified-files')
6263
const isModifiedCh = channel('ci:cucumber:is-modified-test')
6364

6465
const workerReportTraceCh = channel('ci:cucumber:worker-report:trace')
66+
const workerReportTelemetryCh = channel('ci:cucumber:worker-report:telemetry')
6567

6668
const itrSkippedSuitesCh = channel('ci:cucumber:itr:skipped-suites')
6769

@@ -303,6 +305,10 @@ function handleDdWorkerMessage (message) {
303305
workerReportTraceCh.publish(payload)
304306
return true
305307
}
308+
if (messageCode === CUCUMBER_WORKER_TELEMETRY_PAYLOAD_CODE) {
309+
workerReportTelemetryCh.publish(payload)
310+
return true
311+
}
306312
}
307313

308314
if (message?.[DD_EFD_RETRY_COUNT_MESSAGE]) {
@@ -351,8 +357,10 @@ function maybeStartParallelSuite (pickle) {
351357
})
352358
}
353359

354-
function handleParallelTestCaseFinished (pickle, worstTestStepResult) {
355-
const { status } = getStatusFromResultLatest(worstTestStepResult)
360+
function handleParallelTestCaseFinished (pickle, worstTestStepResult, usesNumericStatus = false) {
361+
const { status } = usesNumericStatus
362+
? getStatusFromResult(worstTestStepResult)
363+
: getStatusFromResultLatest(worstTestStepResult)
356364
let isNew = false
357365

358366
if (isKnownTestsEnabled) {
@@ -1488,7 +1496,7 @@ function patchCucumberWorkerRunTestCase (runtimeExecutorPackage, isWorker) {
14881496
)
14891497
}
14901498

1491-
function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion) {
1499+
function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion, usesNumericStatus = false) {
14921500
return function (worker, message) {
14931501
if (!testSuiteFinishCh.hasSubscribers) {
14941502
return parseWorkerMessageFunction.apply(this, arguments)
@@ -1539,13 +1547,31 @@ function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion)
15391547
pickle = testCase.pickle
15401548
}
15411549

1542-
handleParallelTestCaseFinished(pickle, worstTestStepResult)
1550+
handleParallelTestCaseFinished(pickle, worstTestStepResult, usesNumericStatus)
15431551
}
15441552

15451553
return parseWorkerResponse
15461554
}
15471555
}
15481556

1557+
/**
1558+
* Adapts Cucumber 7's callback-based parallel coordinator to the Promise contract used by getWrappedStart.
1559+
*
1560+
* @param {Function} run
1561+
* @param {string} frameworkVersion
1562+
* @returns {Function}
1563+
*/
1564+
function getWrappedCoordinatorRun (run, frameworkVersion) {
1565+
const runAsPromise = function (numberOfWorkers) {
1566+
return new Promise(resolve => run.call(this, numberOfWorkers, resolve))
1567+
}
1568+
const wrappedStart = getWrappedStart(runAsPromise, frameworkVersion, true)
1569+
1570+
return function (numberOfWorkers, done) {
1571+
return wrappedStart.call(this, numberOfWorkers).then(done)
1572+
}
1573+
}
1574+
15491575
module.exports.patchCucumberWorkerRunTestCase = patchCucumberWorkerRunTestCase
15501576

15511577
// Test start / finish for older versions. The only hook executed in workers when in parallel mode
@@ -1593,19 +1619,33 @@ addHook({
15931619
return runtimePackage
15941620
})
15951621

1596-
// Only executed in parallel mode.
1597-
// `getWrappedStart` generates session start and finish events
1622+
// Only executed in parallel mode in Cucumber 7 through 10.
1623+
// `getWrappedCoordinatorRun` or `getWrappedStart` generates session start and finish events
15981624
// `getWrappedParseWorkerMessage` generates suite start and finish events
1625+
// Shimmer is required because the coordinator must be changed before it starts workers and exposes no lifecycle hook.
15991626
addHook({
16001627
name: '@cucumber/cucumber',
1601-
versions: ['>=8.0.0 <11.0.0'],
1628+
versions: ['>=7.0.0 <11.0.0'],
16021629
file: 'lib/runtime/parallel/coordinator.js',
16031630
}, (coordinatorPackage, frameworkVersion) => {
1604-
shimmer.wrap(coordinatorPackage.default.prototype, 'start', start => getWrappedStart(start, frameworkVersion, true))
1631+
const isCucumber7 = satisfies(frameworkVersion, '<8.0.0')
1632+
if (isCucumber7) {
1633+
shimmer.wrap(
1634+
coordinatorPackage.default.prototype,
1635+
'run',
1636+
run => getWrappedCoordinatorRun(run, frameworkVersion)
1637+
)
1638+
} else {
1639+
shimmer.wrap(
1640+
coordinatorPackage.default.prototype,
1641+
'start',
1642+
start => getWrappedStart(start, frameworkVersion, true)
1643+
)
1644+
}
16051645
shimmer.wrap(
16061646
coordinatorPackage.default.prototype,
16071647
'parseWorkerMessage',
1608-
parseWorkerMessage => getWrappedParseWorkerMessage(parseWorkerMessage)
1648+
parseWorkerMessage => getWrappedParseWorkerMessage(parseWorkerMessage, false, isCucumber7)
16091649
)
16101650
return coordinatorPackage
16111651
})

packages/datadog-instrumentations/src/mocha/main.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const log = require('../../../dd-trace/src/log')
1717
const { getEnvironmentVariable } = require('../../../dd-trace/src/config/helper')
1818
const {
1919
getTestSuitePath,
20+
MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
2021
MOCHA_WORKER_TRACE_PAYLOAD_CODE,
2122
fromCoverageMapToCoverage,
2223
getCoveredFilesFromCoverage,
@@ -100,6 +101,7 @@ const mochaGlobalRunCh = channel('ci:mocha:global:run')
100101
const testManagementTestsCh = channel('ci:mocha:test-management-tests')
101102
const modifiedFilesCh = channel('ci:mocha:modified-files')
102103
const workerReportTraceCh = channel('ci:mocha:worker-report:trace')
104+
const workerReportTelemetryCh = channel('ci:mocha:worker-report:telemetry')
103105
const testSessionStartCh = channel('ci:mocha:session:start')
104106
const testSessionFinishCh = channel('ci:mocha:session:finish')
105107
const itrSkippedSuitesCh = channel('ci:mocha:itr:skipped-suites')
@@ -994,6 +996,8 @@ function onMessage (message) {
994996
attemptToFixExecutions,
995997
})
996998
workerReportTraceCh.publish(payload)
999+
} else if (messageCode === MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE) {
1000+
workerReportTelemetryCh.publish(payload)
9971001
}
9981002
}
9991003
}
@@ -1127,9 +1131,34 @@ addHook({
11271131
name: 'mocha',
11281132
versions: ['>=8.0.0'],
11291133
file: 'lib/nodejs/buffered-worker-pool.js',
1130-
}, (BufferedWorkerPoolPackage) => {
1134+
}, (BufferedWorkerPoolPackage, frameworkVersion) => {
11311135
const { BufferedWorkerPool } = BufferedWorkerPoolPackage
11321136

1137+
if (satisfies(frameworkVersion, '<9.2.0')) {
1138+
// Shimmer is required because the worker environment must be changed before workerpool forks,
1139+
// before any test lifecycle hook can run. Mocha added this worker ID itself in 9.2.0.
1140+
shimmer.wrap(BufferedWorkerPool, 'create', create => function () {
1141+
const pool = create.apply(this, arguments)
1142+
1143+
if (!testFinishCh.hasSubscribers) return pool
1144+
1145+
let workerId = 0
1146+
shimmer.wrap(pool._pool, '_createWorkerHandler', createWorkerHandler => function () {
1147+
this.forkOpts = {
1148+
...this.forkOpts,
1149+
env: {
1150+
// eslint-disable-next-line eslint-rules/eslint-process-env
1151+
...(this.forkOpts.env || process.env),
1152+
MOCHA_WORKER_ID: String(workerId++),
1153+
},
1154+
}
1155+
return createWorkerHandler.apply(this, arguments)
1156+
})
1157+
1158+
return pool
1159+
})
1160+
}
1161+
11331162
shimmer.wrap(BufferedWorkerPool.prototype, 'run', run => async function (testSuiteAbsolutePath, workerArgs) {
11341163
if (!testFinishCh.hasSubscribers ||
11351164
(!config.isKnownTestsEnabled &&

0 commit comments

Comments
 (0)