dd-trace 5.75.0 → 5.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ci/init.js +1 -0
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/openai.js +8 -0
- package/packages/datadog-instrumentations/src/playwright.js +47 -0
- package/packages/datadog-instrumentations/src/vitest.js +94 -8
- package/packages/datadog-plugin-openai/src/stream-helpers.js +26 -1
- package/packages/datadog-plugin-openai/src/tracing.js +46 -1
- package/packages/datadog-plugin-vitest/src/index.js +5 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +30 -7
- package/packages/dd-trace/src/llmobs/plugins/openai.js +216 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +2 -1
- package/packages/dd-trace/src/llmobs/tagger.js +9 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +3 -1
- package/packages/dd-trace/src/plugins/util/test.js +6 -0
- package/packages/dd-trace/src/profiling/config.js +32 -10
- package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
- package/packages/dd-trace/src/remote_config/index.js +4 -0
- package/packages/dd-trace/src/supported-configurations.json +1 -0
package/ci/init.js
CHANGED
|
@@ -29,6 +29,7 @@ function detectTestWorkerType () {
|
|
|
29
29
|
if (getEnvironmentVariable('MOCHA_WORKER_ID')) return 'mocha'
|
|
30
30
|
if (getEnvironmentVariable('DD_PLAYWRIGHT_WORKER')) return 'playwright'
|
|
31
31
|
if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) return 'vitest'
|
|
32
|
+
if (getEnvironmentVariable('DD_VITEST_WORKER')) return 'vitest'
|
|
32
33
|
return null
|
|
33
34
|
}
|
|
34
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.76.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -125,8 +125,8 @@
|
|
|
125
125
|
"@datadog/native-appsec": "10.3.0",
|
|
126
126
|
"@datadog/native-iast-taint-tracking": "4.0.0",
|
|
127
127
|
"@datadog/native-metrics": "3.1.1",
|
|
128
|
-
"@datadog/openfeature-node-server": "0.1.0-preview.
|
|
129
|
-
"@datadog/pprof": "5.
|
|
128
|
+
"@datadog/openfeature-node-server": "0.1.0-preview.13",
|
|
129
|
+
"@datadog/pprof": "5.12.0",
|
|
130
130
|
"@datadog/sketches-js": "2.1.1",
|
|
131
131
|
"@datadog/wasm-js-rewriter": "4.0.1",
|
|
132
132
|
"@isaacs/ttlcache": "^1.4.1",
|
|
@@ -22,6 +22,14 @@ const V4_PACKAGE_SHIMS = [
|
|
|
22
22
|
methods: ['create'],
|
|
23
23
|
streamedResponse: true
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
file: 'resources/responses/responses',
|
|
27
|
+
targetClass: 'Responses',
|
|
28
|
+
baseResource: 'responses',
|
|
29
|
+
methods: ['create'],
|
|
30
|
+
streamedResponse: true,
|
|
31
|
+
versions: ['>=4.87.0']
|
|
32
|
+
},
|
|
25
33
|
{
|
|
26
34
|
file: 'resources/embeddings',
|
|
27
35
|
targetClass: 'Embeddings',
|
|
@@ -68,6 +68,8 @@ let modifiedFiles = {}
|
|
|
68
68
|
const quarantinedOrDisabledTestsAttemptToFix = []
|
|
69
69
|
let quarantinedButNotAttemptToFixFqns = new Set()
|
|
70
70
|
let rootDir = ''
|
|
71
|
+
let sessionProjects = []
|
|
72
|
+
|
|
71
73
|
const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
|
|
72
74
|
|
|
73
75
|
function isValidKnownTests (receivedKnownTests) {
|
|
@@ -495,6 +497,7 @@ function dispatcherHook (dispatcherExport) {
|
|
|
495
497
|
const dispatcher = this
|
|
496
498
|
const worker = createWorker.apply(this, arguments)
|
|
497
499
|
const projects = getProjectsFromDispatcher(dispatcher)
|
|
500
|
+
sessionProjects = projects
|
|
498
501
|
|
|
499
502
|
// for older versions of playwright, `shouldCreateTestSpan` should always be true,
|
|
500
503
|
// since the `_runTest` function wrapper is not available for older versions
|
|
@@ -535,6 +538,7 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
|
|
|
535
538
|
const dispatcher = this
|
|
536
539
|
const worker = createWorker.apply(this, arguments)
|
|
537
540
|
const projects = getProjectsFromDispatcher(dispatcher)
|
|
541
|
+
sessionProjects = projects
|
|
538
542
|
|
|
539
543
|
worker.on('testBegin', ({ testId }) => {
|
|
540
544
|
const test = getTestByTestId(dispatcher, testId)
|
|
@@ -1255,3 +1259,46 @@ addHook({
|
|
|
1255
1259
|
|
|
1256
1260
|
return workerPackage
|
|
1257
1261
|
})
|
|
1262
|
+
|
|
1263
|
+
function generateSummaryWrapper (generateSummary) {
|
|
1264
|
+
return function () {
|
|
1265
|
+
for (const test of this.suite.allTests()) {
|
|
1266
|
+
// https://github.com/microsoft/playwright/blob/bf92ffecff6f30a292b53430dbaee0207e0c61ad/packages/playwright/src/reporters/base.ts#L279
|
|
1267
|
+
const didNotRun = test.outcome() === 'skipped' &&
|
|
1268
|
+
(!test.results.length || test.expectedStatus !== 'skipped')
|
|
1269
|
+
if (didNotRun) {
|
|
1270
|
+
const {
|
|
1271
|
+
_requireFile: testSuiteAbsolutePath,
|
|
1272
|
+
location: { line: testSourceLine },
|
|
1273
|
+
} = test
|
|
1274
|
+
const browserName = getBrowserNameFromProjects(sessionProjects, test)
|
|
1275
|
+
|
|
1276
|
+
testSkipCh.publish({
|
|
1277
|
+
testName: getTestFullname(test),
|
|
1278
|
+
testSuiteAbsolutePath,
|
|
1279
|
+
testSourceLine,
|
|
1280
|
+
browserName,
|
|
1281
|
+
})
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return generateSummary.apply(this, arguments)
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// If a playwright project B has a dependency on project A,
|
|
1289
|
+
// and project A fails, the tests in project B will not run.
|
|
1290
|
+
// This hook is used to report tests that did not run as skipped.
|
|
1291
|
+
// Note: this is different from tests skipped via test.skip() or test.fixme()
|
|
1292
|
+
addHook({
|
|
1293
|
+
name: 'playwright',
|
|
1294
|
+
file: 'lib/reporters/base.js',
|
|
1295
|
+
versions: ['>=1.38.0']
|
|
1296
|
+
}, (reportersPackage) => {
|
|
1297
|
+
// v1.50.0 changed the name of the base reporter from BaseReporter to TerminalReporter
|
|
1298
|
+
if (reportersPackage.TerminalReporter) {
|
|
1299
|
+
shimmer.wrap(reportersPackage.TerminalReporter.prototype, 'generateSummary', generateSummaryWrapper)
|
|
1300
|
+
} else if (reportersPackage.BaseReporter) {
|
|
1301
|
+
shimmer.wrap(reportersPackage.BaseReporter.prototype, 'generateSummary', generateSummaryWrapper)
|
|
1302
|
+
}
|
|
1303
|
+
return reportersPackage
|
|
1304
|
+
})
|
|
@@ -60,6 +60,7 @@ let testManagementAttemptToFixRetries = 0
|
|
|
60
60
|
let isDiEnabled = false
|
|
61
61
|
let testCodeCoverageLinesTotal
|
|
62
62
|
let isSessionStarted = false
|
|
63
|
+
let vitestPool = null
|
|
63
64
|
|
|
64
65
|
const BREAKPOINT_HIT_GRACE_PERIOD_MS = 400
|
|
65
66
|
|
|
@@ -157,6 +158,14 @@ function isTestPackage (testPackage) {
|
|
|
157
158
|
return testPackage.V?.name === 'VitestTestRunner'
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function hasForksPoolWorker (vitestPackage) {
|
|
162
|
+
return vitestPackage.f?.name === 'ForksPoolWorker'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasThreadsPoolWorker (vitestPackage) {
|
|
166
|
+
return vitestPackage.T?.name === 'ThreadsPoolWorker'
|
|
167
|
+
}
|
|
168
|
+
|
|
160
169
|
function getSessionStatus (state) {
|
|
161
170
|
if (state.getCountOfFailedTests() > 0) {
|
|
162
171
|
return 'fail'
|
|
@@ -389,6 +398,7 @@ function getFinishWrapper (exitOrClose) {
|
|
|
389
398
|
isEarlyFlakeDetectionEnabled,
|
|
390
399
|
isEarlyFlakeDetectionFaulty,
|
|
391
400
|
isTestManagementTestsEnabled,
|
|
401
|
+
vitestPool,
|
|
392
402
|
onFinish
|
|
393
403
|
})
|
|
394
404
|
|
|
@@ -418,11 +428,27 @@ function getCreateCliWrapper (vitestPackage, frameworkVersion) {
|
|
|
418
428
|
}
|
|
419
429
|
|
|
420
430
|
function threadHandler (thread) {
|
|
421
|
-
|
|
431
|
+
const { runtime } = thread
|
|
432
|
+
let workerProcess
|
|
433
|
+
if (runtime === 'child_process') {
|
|
434
|
+
vitestPool = 'child_process'
|
|
435
|
+
workerProcess = thread.process
|
|
436
|
+
} else if (runtime === 'worker_threads') {
|
|
437
|
+
vitestPool = 'worker_threads'
|
|
438
|
+
workerProcess = thread.thread
|
|
439
|
+
} else {
|
|
440
|
+
vitestPool = 'unknown'
|
|
441
|
+
}
|
|
442
|
+
if (!workerProcess) {
|
|
443
|
+
log.error('Vitest error: could not get process or thread from TinyPool#run')
|
|
422
444
|
return
|
|
423
445
|
}
|
|
424
|
-
|
|
425
|
-
|
|
446
|
+
|
|
447
|
+
if (workerProcesses.has(workerProcess)) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
workerProcesses.add(workerProcess)
|
|
451
|
+
workerProcess.on('message', (message) => {
|
|
426
452
|
if (message.__tinypool_worker_message__ && message.data) {
|
|
427
453
|
if (message.interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) {
|
|
428
454
|
workerReportTraceCh.publish(message.data)
|
|
@@ -433,11 +459,7 @@ function threadHandler (thread) {
|
|
|
433
459
|
})
|
|
434
460
|
}
|
|
435
461
|
|
|
436
|
-
|
|
437
|
-
name: 'tinypool',
|
|
438
|
-
versions: ['>=1.0.0'],
|
|
439
|
-
file: 'dist/index.js'
|
|
440
|
-
}, (TinyPool) => {
|
|
462
|
+
function wrapTinyPoolRun (TinyPool) {
|
|
441
463
|
shimmer.wrap(TinyPool.prototype, 'run', run => async function () {
|
|
442
464
|
// We have to do this before and after because the threads list gets recycled, that is, the processes are re-created
|
|
443
465
|
this.threads.forEach(threadHandler)
|
|
@@ -445,15 +467,79 @@ addHook({
|
|
|
445
467
|
this.threads.forEach(threadHandler)
|
|
446
468
|
return runResult
|
|
447
469
|
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
addHook({
|
|
473
|
+
name: 'tinypool',
|
|
474
|
+
// version from tinypool@0.8 was used in vitest@1.6.0
|
|
475
|
+
versions: ['>=0.8.0 <1.0.0'],
|
|
476
|
+
file: 'dist/esm/index.js'
|
|
477
|
+
}, (TinyPool) => {
|
|
478
|
+
wrapTinyPoolRun(TinyPool)
|
|
479
|
+
return TinyPool
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
addHook({
|
|
483
|
+
name: 'tinypool',
|
|
484
|
+
versions: ['>=1.0.0'],
|
|
485
|
+
file: 'dist/index.js'
|
|
486
|
+
}, (TinyPool) => {
|
|
487
|
+
wrapTinyPoolRun(TinyPool)
|
|
448
488
|
|
|
449
489
|
return TinyPool
|
|
450
490
|
})
|
|
451
491
|
|
|
492
|
+
function getWrappedOn (on) {
|
|
493
|
+
return function (event, callback) {
|
|
494
|
+
if (event !== 'message') {
|
|
495
|
+
return on.apply(this, arguments)
|
|
496
|
+
}
|
|
497
|
+
// `arguments[1]` is the callback function, which
|
|
498
|
+
// we modify to intercept our messages to not interfere
|
|
499
|
+
// with vitest's own messages
|
|
500
|
+
arguments[1] = shimmer.wrapFunction(callback, callback => function (message) {
|
|
501
|
+
if (message.type !== 'Buffer' && Array.isArray(message)) {
|
|
502
|
+
const [interprocessCode, data] = message
|
|
503
|
+
if (interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) {
|
|
504
|
+
workerReportTraceCh.publish(data)
|
|
505
|
+
} else if (interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE) {
|
|
506
|
+
workerReportLogsCh.publish(data)
|
|
507
|
+
}
|
|
508
|
+
// If we execute the callback vitest crashes, as the message is not supported
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
return callback.apply(this, arguments)
|
|
512
|
+
})
|
|
513
|
+
return on.apply(this, arguments)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
452
517
|
function getStartVitestWrapper (cliApiPackage, frameworkVersion) {
|
|
453
518
|
if (!isCliApiPackage(cliApiPackage)) {
|
|
454
519
|
return cliApiPackage
|
|
455
520
|
}
|
|
456
521
|
shimmer.wrap(cliApiPackage, 's', getCliOrStartVitestWrapper(frameworkVersion))
|
|
522
|
+
|
|
523
|
+
if (hasForksPoolWorker(cliApiPackage)) {
|
|
524
|
+
// function is async
|
|
525
|
+
shimmer.wrap(cliApiPackage.f.prototype, 'start', start => function () {
|
|
526
|
+
vitestPool = 'child_process'
|
|
527
|
+
this.env.DD_VITEST_WORKER = '1'
|
|
528
|
+
|
|
529
|
+
return start.apply(this, arguments)
|
|
530
|
+
})
|
|
531
|
+
shimmer.wrap(cliApiPackage.f.prototype, 'on', getWrappedOn)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (hasThreadsPoolWorker(cliApiPackage)) {
|
|
535
|
+
// function is async
|
|
536
|
+
shimmer.wrap(cliApiPackage.T.prototype, 'start', start => function () {
|
|
537
|
+
vitestPool = 'worker_threads'
|
|
538
|
+
this.env.DD_VITEST_WORKER = '1'
|
|
539
|
+
return start.apply(this, arguments)
|
|
540
|
+
})
|
|
541
|
+
shimmer.wrap(cliApiPackage.T.prototype, 'on', getWrappedOn)
|
|
542
|
+
}
|
|
457
543
|
return cliApiPackage
|
|
458
544
|
}
|
|
459
545
|
|
|
@@ -107,8 +107,33 @@ function constructChatCompletionResponseFromStreamedChunks (chunks, n) {
|
|
|
107
107
|
})
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Constructs the entire response from a stream of OpenAI responses chunks.
|
|
112
|
+
* The responses API uses event-based streaming with delta chunks.
|
|
113
|
+
* @param {Array<Record<string, any>>} chunks
|
|
114
|
+
* @returns {Record<string, any>}
|
|
115
|
+
*/
|
|
116
|
+
function constructResponseResponseFromStreamedChunks (chunks) {
|
|
117
|
+
// The responses API streams events with different types:
|
|
118
|
+
// - response.output_text.delta: incremental text deltas
|
|
119
|
+
// - response.output_text.done: complete text for a content part
|
|
120
|
+
// - response.output_item.done: complete output item with role
|
|
121
|
+
// - response.done/response.incomplete/response.completed: final response with output array and usage
|
|
122
|
+
|
|
123
|
+
// Find the last chunk with a complete response object (status: done, incomplete, or completed)
|
|
124
|
+
const responseStatusSet = new Set(['done', 'incomplete', 'completed'])
|
|
125
|
+
|
|
126
|
+
for (let i = chunks.length - 1; i >= 0; i--) {
|
|
127
|
+
const chunk = chunks[i]
|
|
128
|
+
if (chunk.response && responseStatusSet.has(chunk.response.status)) {
|
|
129
|
+
return chunk.response
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
module.exports = {
|
|
111
135
|
convertBuffersToObjects,
|
|
112
136
|
constructCompletionResponseFromStreamedChunks,
|
|
113
|
-
constructChatCompletionResponseFromStreamedChunks
|
|
137
|
+
constructChatCompletionResponseFromStreamedChunks,
|
|
138
|
+
constructResponseResponseFromStreamedChunks
|
|
114
139
|
}
|
|
@@ -11,7 +11,8 @@ const { MEASURED } = require('../../../ext/tags')
|
|
|
11
11
|
const {
|
|
12
12
|
convertBuffersToObjects,
|
|
13
13
|
constructCompletionResponseFromStreamedChunks,
|
|
14
|
-
constructChatCompletionResponseFromStreamedChunks
|
|
14
|
+
constructChatCompletionResponseFromStreamedChunks,
|
|
15
|
+
constructResponseResponseFromStreamedChunks
|
|
15
16
|
} = require('./stream-helpers')
|
|
16
17
|
|
|
17
18
|
const { DD_MAJOR } = require('../../../version')
|
|
@@ -59,6 +60,8 @@ class OpenAiTracingPlugin extends TracingPlugin {
|
|
|
59
60
|
response = constructCompletionResponseFromStreamedChunks(chunks, n)
|
|
60
61
|
} else if (methodName === 'createChatCompletion') {
|
|
61
62
|
response = constructChatCompletionResponseFromStreamedChunks(chunks, n)
|
|
63
|
+
} else if (methodName === 'createResponse') {
|
|
64
|
+
response = constructResponseResponseFromStreamedChunks(chunks)
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
ctx.result = { data: response }
|
|
@@ -134,6 +137,10 @@ class OpenAiTracingPlugin extends TracingPlugin {
|
|
|
134
137
|
case 'createEdit':
|
|
135
138
|
createEditRequestExtraction(tags, payload, openaiStore)
|
|
136
139
|
break
|
|
140
|
+
|
|
141
|
+
case 'createResponse':
|
|
142
|
+
createResponseRequestExtraction(tags, payload, openaiStore)
|
|
143
|
+
break
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
span.addTags(tags)
|
|
@@ -313,6 +320,10 @@ function normalizeMethodName (methodName) {
|
|
|
313
320
|
case 'embeddings.create':
|
|
314
321
|
return 'createEmbedding'
|
|
315
322
|
|
|
323
|
+
// responses
|
|
324
|
+
case 'responses.create':
|
|
325
|
+
return 'createResponse'
|
|
326
|
+
|
|
316
327
|
// files
|
|
317
328
|
case 'files.create':
|
|
318
329
|
return 'createFile'
|
|
@@ -376,6 +387,16 @@ function createEditRequestExtraction (tags, payload, openaiStore) {
|
|
|
376
387
|
openaiStore.instruction = instruction
|
|
377
388
|
}
|
|
378
389
|
|
|
390
|
+
function createResponseRequestExtraction (tags, payload, openaiStore) {
|
|
391
|
+
// Extract model information
|
|
392
|
+
if (payload.model) {
|
|
393
|
+
tags['openai.request.model'] = payload.model
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Store the full payload for response extraction
|
|
397
|
+
openaiStore.responseData = payload
|
|
398
|
+
}
|
|
399
|
+
|
|
379
400
|
function retrieveModelRequestExtraction (tags, payload) {
|
|
380
401
|
tags['openai.request.id'] = payload.id
|
|
381
402
|
}
|
|
@@ -410,6 +431,10 @@ function responseDataExtractionByMethod (methodName, tags, body, openaiStore) {
|
|
|
410
431
|
commonCreateResponseExtraction(tags, body, openaiStore, methodName)
|
|
411
432
|
break
|
|
412
433
|
|
|
434
|
+
case 'createResponse':
|
|
435
|
+
createResponseResponseExtraction(tags, body, openaiStore)
|
|
436
|
+
break
|
|
437
|
+
|
|
413
438
|
case 'listFiles':
|
|
414
439
|
case 'listFineTunes':
|
|
415
440
|
case 'listFineTuneEvents':
|
|
@@ -513,6 +538,26 @@ function commonCreateResponseExtraction (tags, body, openaiStore, methodName) {
|
|
|
513
538
|
openaiStore.choices = body.choices
|
|
514
539
|
}
|
|
515
540
|
|
|
541
|
+
function createResponseResponseExtraction (tags, body, openaiStore) {
|
|
542
|
+
// Extract response ID if available
|
|
543
|
+
if (body.id) {
|
|
544
|
+
tags['openai.response.id'] = body.id
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Extract status if available
|
|
548
|
+
if (body.status) {
|
|
549
|
+
tags['openai.response.status'] = body.status
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Extract model from response if available
|
|
553
|
+
if (body.model) {
|
|
554
|
+
tags['openai.response.model'] = body.model
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Store the full response for potential future use
|
|
558
|
+
openaiStore.response = body
|
|
559
|
+
}
|
|
560
|
+
|
|
516
561
|
// The server almost always responds with JSON
|
|
517
562
|
function coerceResponseBody (body, methodName) {
|
|
518
563
|
switch (methodName) {
|
|
@@ -6,6 +6,7 @@ const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
|
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
TEST_STATUS,
|
|
9
|
+
VITEST_POOL,
|
|
9
10
|
finishAllTraceSpans,
|
|
10
11
|
getTestSuitePath,
|
|
11
12
|
getTestSuiteCommonTags,
|
|
@@ -344,7 +345,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
344
345
|
finishAllTraceSpans(testSuiteSpan)
|
|
345
346
|
}
|
|
346
347
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
|
|
347
|
-
// TODO: too frequent flush - find for method in worker to decrease frequency
|
|
348
348
|
this.tracer._exporter.flush(onFinish)
|
|
349
349
|
if (this.runningTestProbe) {
|
|
350
350
|
this.removeDiProbe(this.runningTestProbe)
|
|
@@ -373,6 +373,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
373
373
|
isEarlyFlakeDetectionEnabled,
|
|
374
374
|
isEarlyFlakeDetectionFaulty,
|
|
375
375
|
isTestManagementTestsEnabled,
|
|
376
|
+
vitestPool,
|
|
376
377
|
onFinish
|
|
377
378
|
}) => {
|
|
378
379
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
@@ -394,6 +395,9 @@ class VitestPlugin extends CiPlugin {
|
|
|
394
395
|
if (isTestManagementTestsEnabled) {
|
|
395
396
|
this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
|
|
396
397
|
}
|
|
398
|
+
if (vitestPool) {
|
|
399
|
+
this.testSessionSpan.setTag(VITEST_POOL, vitestPool)
|
|
400
|
+
}
|
|
397
401
|
this.testModuleSpan.finish()
|
|
398
402
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
|
|
399
403
|
this.testSessionSpan.finish()
|
|
@@ -29,6 +29,9 @@ function getInterprocessTraceCode () {
|
|
|
29
29
|
if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) {
|
|
30
30
|
return VITEST_WORKER_TRACE_PAYLOAD_CODE
|
|
31
31
|
}
|
|
32
|
+
if (getEnvironmentVariable('DD_VITEST_WORKER')) {
|
|
33
|
+
return VITEST_WORKER_TRACE_PAYLOAD_CODE
|
|
34
|
+
}
|
|
32
35
|
return null
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -47,6 +50,9 @@ function getInterprocessLogsCode () {
|
|
|
47
50
|
if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) {
|
|
48
51
|
return VITEST_WORKER_LOGS_PAYLOAD_CODE
|
|
49
52
|
}
|
|
53
|
+
if (getEnvironmentVariable('DD_VITEST_WORKER')) {
|
|
54
|
+
return VITEST_WORKER_LOGS_PAYLOAD_CODE
|
|
55
|
+
}
|
|
50
56
|
return null
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { JSONEncoder } = require('../../encode/json-encoder')
|
|
3
3
|
const { getEnvironmentVariable } = require('../../../config-helper')
|
|
4
|
+
const log = require('../../../log')
|
|
5
|
+
const {
|
|
6
|
+
VITEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
7
|
+
VITEST_WORKER_LOGS_PAYLOAD_CODE
|
|
8
|
+
} = require('../../../plugins/util/test')
|
|
4
9
|
|
|
5
10
|
class Writer {
|
|
6
11
|
constructor (interprocessCode) {
|
|
@@ -26,24 +31,42 @@ class Writer {
|
|
|
26
31
|
_sendPayload (data, onDone = () => {}) {
|
|
27
32
|
// ## Jest
|
|
28
33
|
// Only available when `child_process` is used for the jest worker.
|
|
29
|
-
// https://github.com/facebook/jest/blob/bb39cb2c617a3334bf18daeca66bd87b7ccab28b/packages/jest-worker/README.md#experimental-worker
|
|
30
34
|
// If worker_threads is used, this will not work
|
|
31
|
-
// TODO: make
|
|
35
|
+
// TODO: make `jest` instrumentation compatible with worker_threads
|
|
36
|
+
// https://github.com/facebook/jest/blob/bb39cb2c617a3334bf18daeca66bd87b7ccab28b/packages/jest-worker/README.md#experimental-worker
|
|
32
37
|
|
|
33
38
|
// ## Cucumber
|
|
34
39
|
// This reports to the test's main process the same way test data is reported by Cucumber
|
|
35
40
|
// See cucumber code:
|
|
36
41
|
// https://github.com/cucumber/cucumber-js/blob/5ce371870b677fe3d1a14915dc535688946f734c/src/runtime/parallel/run_worker.ts#L13
|
|
37
|
-
if (process.send) { // it only works if process.send is available
|
|
38
|
-
const isVitestWorker = !!getEnvironmentVariable('TINYPOOL_WORKER_ID')
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// Old because vitest@>=4 uses `DD_VITEST_WORKER` and reports arrays just like other frameworks
|
|
44
|
+
// Before vitest@>=4, we need the `__tinypool_worker_message__` property, or tinypool will crash
|
|
45
|
+
const isVitestWorkerOld = !!getEnvironmentVariable('TINYPOOL_WORKER_ID')
|
|
46
|
+
const payload = isVitestWorkerOld
|
|
47
|
+
? { __tinypool_worker_message__: true, interprocessCode: this._interprocessCode, data }
|
|
48
|
+
: [this._interprocessCode, data]
|
|
43
49
|
|
|
50
|
+
const isVitestTestWorker =
|
|
51
|
+
this._interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE ||
|
|
52
|
+
this._interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE
|
|
53
|
+
|
|
54
|
+
if (process.send) {
|
|
44
55
|
process.send(payload, () => {
|
|
45
56
|
onDone()
|
|
46
57
|
})
|
|
58
|
+
} else if (isVitestTestWorker) { // TODO: worker_threads are only supported in vitest right now
|
|
59
|
+
const { isMainThread, parentPort } = require('worker_threads')
|
|
60
|
+
if (isMainThread) {
|
|
61
|
+
return onDone()
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
parentPort.postMessage(payload)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
log.error('Error posting message to parent port', error)
|
|
67
|
+
} finally {
|
|
68
|
+
onDone()
|
|
69
|
+
}
|
|
47
70
|
} else {
|
|
48
71
|
onDone()
|
|
49
72
|
}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const LLMObsPlugin = require('./base')
|
|
4
4
|
|
|
5
|
+
const allowedParamKeys = new Set([
|
|
6
|
+
'max_output_tokens',
|
|
7
|
+
'temperature',
|
|
8
|
+
'stream',
|
|
9
|
+
'reasoning'
|
|
10
|
+
])
|
|
11
|
+
|
|
5
12
|
function isIterable (obj) {
|
|
6
13
|
if (obj == null) {
|
|
7
14
|
return false
|
|
@@ -19,7 +26,7 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
19
26
|
const methodName = gateResource(normalizeOpenAIResourceName(resource))
|
|
20
27
|
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
21
28
|
|
|
22
|
-
const inputs = ctx.args[0] // completion, chat completion, and
|
|
29
|
+
const inputs = ctx.args[0] // completion, chat completion, embeddings, and responses take one argument
|
|
23
30
|
const operation = getOperation(methodName)
|
|
24
31
|
const kind = operation === 'embedding' ? 'embedding' : 'llm'
|
|
25
32
|
|
|
@@ -53,6 +60,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
53
60
|
this._tagChatCompletion(span, inputs, response, error)
|
|
54
61
|
} else if (operation === 'embedding') {
|
|
55
62
|
this._tagEmbedding(span, inputs, response, error)
|
|
63
|
+
} else if (operation === 'response') {
|
|
64
|
+
this.#tagResponse(span, inputs, response, error)
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
if (!error) {
|
|
@@ -75,19 +84,30 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
75
84
|
const tokenUsage = response.usage
|
|
76
85
|
|
|
77
86
|
if (tokenUsage) {
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
// Responses API uses input_tokens, Chat/Completions use prompt_tokens
|
|
88
|
+
const inputTokens = tokenUsage.input_tokens ?? tokenUsage.prompt_tokens
|
|
89
|
+
if (inputTokens !== undefined) metrics.inputTokens = inputTokens
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// Responses API uses output_tokens, Chat/Completions use completion_tokens
|
|
92
|
+
const outputTokens = tokenUsage.output_tokens ?? tokenUsage.completion_tokens
|
|
93
|
+
if (outputTokens !== undefined) metrics.outputTokens = outputTokens
|
|
83
94
|
|
|
84
95
|
const totalTokens = tokenUsage.total_tokens || (inputTokens + outputTokens)
|
|
85
|
-
if (totalTokens) metrics.totalTokens = totalTokens
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
if (totalTokens !== undefined) metrics.totalTokens = totalTokens
|
|
97
|
+
|
|
98
|
+
// Cache tokens - Responses API uses input_tokens_details, Chat/Completions use prompt_tokens_details
|
|
99
|
+
// For Responses API, always include cache tokens (even if 0)
|
|
100
|
+
// For Chat API, only include if > 0
|
|
101
|
+
if (tokenUsage.input_tokens_details) {
|
|
102
|
+
// Responses API - always include
|
|
103
|
+
const cacheReadTokens = tokenUsage.input_tokens_details.cached_tokens
|
|
104
|
+
if (cacheReadTokens !== undefined) metrics.cacheReadTokens = cacheReadTokens
|
|
105
|
+
} else if (tokenUsage.prompt_tokens_details) {
|
|
106
|
+
// Chat/Completions API - only include if > 0
|
|
107
|
+
const cacheReadTokens = tokenUsage.prompt_tokens_details.cached_tokens
|
|
108
|
+
if (cacheReadTokens) {
|
|
109
|
+
metrics.cacheReadTokens = cacheReadTokens
|
|
110
|
+
}
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
@@ -191,6 +211,183 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
191
211
|
|
|
192
212
|
this._tagger.tagMetadata(span, metadata)
|
|
193
213
|
}
|
|
214
|
+
|
|
215
|
+
#tagResponse (span, inputs, response, error) {
|
|
216
|
+
// Tag metadata - use allowlist approach for request parameters
|
|
217
|
+
|
|
218
|
+
const { input, model, ...parameters } = inputs
|
|
219
|
+
|
|
220
|
+
// Create input messages
|
|
221
|
+
const inputMessages = []
|
|
222
|
+
|
|
223
|
+
// Add system message if instructions exist
|
|
224
|
+
if (inputs.instructions) {
|
|
225
|
+
inputMessages.push({ role: 'system', content: inputs.instructions })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle input - can be string or array of mixed messages
|
|
229
|
+
if (Array.isArray(input)) {
|
|
230
|
+
for (const item of input) {
|
|
231
|
+
if (item.type === 'function_call') {
|
|
232
|
+
// Function call: convert to message with tool_calls
|
|
233
|
+
// Parse arguments if it's a JSON string
|
|
234
|
+
let parsedArgs = item.arguments
|
|
235
|
+
if (typeof parsedArgs === 'string') {
|
|
236
|
+
try {
|
|
237
|
+
parsedArgs = JSON.parse(parsedArgs)
|
|
238
|
+
} catch {
|
|
239
|
+
parsedArgs = {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
inputMessages.push({
|
|
243
|
+
role: 'assistant',
|
|
244
|
+
toolCalls: [{
|
|
245
|
+
toolId: item.call_id,
|
|
246
|
+
name: item.name,
|
|
247
|
+
arguments: parsedArgs,
|
|
248
|
+
type: item.type
|
|
249
|
+
}]
|
|
250
|
+
})
|
|
251
|
+
} else if (item.type === 'function_call_output') {
|
|
252
|
+
// Function output: convert to user message with tool_results
|
|
253
|
+
inputMessages.push({
|
|
254
|
+
role: 'user',
|
|
255
|
+
toolResults: [{
|
|
256
|
+
toolId: item.call_id,
|
|
257
|
+
result: item.output,
|
|
258
|
+
name: item.name || '',
|
|
259
|
+
type: item.type
|
|
260
|
+
}]
|
|
261
|
+
})
|
|
262
|
+
} else if (item.role && item.content) {
|
|
263
|
+
// Regular message
|
|
264
|
+
inputMessages.push({ role: item.role, content: item.content })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// Simple string input
|
|
269
|
+
inputMessages.push({ role: 'user', content: input })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (error) {
|
|
273
|
+
this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create output messages
|
|
278
|
+
const outputMessages = []
|
|
279
|
+
|
|
280
|
+
// Handle output - can be string (streaming) or array of message objects (non-streaming)
|
|
281
|
+
if (typeof response.output === 'string') {
|
|
282
|
+
// Simple text output (streaming)
|
|
283
|
+
outputMessages.push({ role: 'assistant', content: response.output })
|
|
284
|
+
} else if (Array.isArray(response.output)) {
|
|
285
|
+
// Array output - process all items to extract reasoning, messages, and tool calls
|
|
286
|
+
// Non-streaming: array of items (messages, function_calls, or reasoning)
|
|
287
|
+
for (const item of response.output) {
|
|
288
|
+
// Handle reasoning type (reasoning responses)
|
|
289
|
+
if (item.type === 'reasoning') {
|
|
290
|
+
// Extract reasoning text from summary
|
|
291
|
+
let reasoningText = ''
|
|
292
|
+
if (Array.isArray(item.summary) && item.summary.length > 0) {
|
|
293
|
+
const summaryItem = item.summary[0]
|
|
294
|
+
if (summaryItem.type === 'summary_text' && summaryItem.text) {
|
|
295
|
+
reasoningText = summaryItem.text
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
outputMessages.push({
|
|
299
|
+
role: 'reasoning',
|
|
300
|
+
content: reasoningText
|
|
301
|
+
})
|
|
302
|
+
} else if (item.type === 'function_call') {
|
|
303
|
+
// Handle function_call type (responses API tool calls)
|
|
304
|
+
let args = item.arguments
|
|
305
|
+
// Parse arguments if it's a JSON string
|
|
306
|
+
if (typeof args === 'string') {
|
|
307
|
+
try {
|
|
308
|
+
args = JSON.parse(args)
|
|
309
|
+
} catch {
|
|
310
|
+
args = {}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
outputMessages.push({
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
toolCalls: [{
|
|
316
|
+
toolId: item.call_id,
|
|
317
|
+
name: item.name,
|
|
318
|
+
arguments: args,
|
|
319
|
+
type: item.type
|
|
320
|
+
}]
|
|
321
|
+
})
|
|
322
|
+
} else {
|
|
323
|
+
// Handle regular message objects
|
|
324
|
+
const outputMsg = { role: item.role || 'assistant', content: '' }
|
|
325
|
+
|
|
326
|
+
// Extract content from message
|
|
327
|
+
if (Array.isArray(item.content)) {
|
|
328
|
+
// Content is array of content parts
|
|
329
|
+
// For responses API, text content has type 'output_text', not 'text'
|
|
330
|
+
const textParts = item.content
|
|
331
|
+
.filter(c => c.type === 'output_text')
|
|
332
|
+
.map(c => c.text)
|
|
333
|
+
outputMsg.content = textParts.join('')
|
|
334
|
+
} else if (typeof item.content === 'string') {
|
|
335
|
+
outputMsg.content = item.content
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Extract tool calls if present in message.tool_calls
|
|
339
|
+
if (Array.isArray(item.tool_calls)) {
|
|
340
|
+
outputMsg.toolCalls = item.tool_calls.map(tc => {
|
|
341
|
+
let args = tc.function?.arguments || tc.arguments
|
|
342
|
+
// Parse arguments if it's a JSON string
|
|
343
|
+
if (typeof args === 'string') {
|
|
344
|
+
try {
|
|
345
|
+
args = JSON.parse(args)
|
|
346
|
+
} catch {
|
|
347
|
+
args = {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
toolId: tc.id,
|
|
352
|
+
name: tc.function?.name || tc.name,
|
|
353
|
+
arguments: args,
|
|
354
|
+
type: tc.type || 'function_call'
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
outputMessages.push(outputMsg)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (response.output_text) {
|
|
363
|
+
// Fallback: use output_text if available (for simple non-streaming responses without reasoning/tools)
|
|
364
|
+
outputMessages.push({ role: 'assistant', content: response.output_text })
|
|
365
|
+
} else {
|
|
366
|
+
// No output
|
|
367
|
+
outputMessages.push({ role: 'assistant', content: '' })
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this._tagger.tagLLMIO(span, inputMessages, outputMessages)
|
|
371
|
+
|
|
372
|
+
const metadata = Object.entries(parameters).reduce((obj, [key, value]) => {
|
|
373
|
+
if (allowedParamKeys.has(key)) {
|
|
374
|
+
obj[key] = value
|
|
375
|
+
}
|
|
376
|
+
return obj
|
|
377
|
+
}, {})
|
|
378
|
+
|
|
379
|
+
// Add fields from response object (convert numbers to floats)
|
|
380
|
+
if (response.temperature !== undefined) metadata.temperature = Number(response.temperature)
|
|
381
|
+
if (response.top_p !== undefined) metadata.top_p = Number(response.top_p)
|
|
382
|
+
if (response.tool_choice !== undefined) metadata.tool_choice = response.tool_choice
|
|
383
|
+
if (response.truncation !== undefined) metadata.truncation = response.truncation
|
|
384
|
+
if (response.text !== undefined) metadata.text = response.text
|
|
385
|
+
if (response.usage?.output_tokens_details?.reasoning_tokens !== undefined) {
|
|
386
|
+
metadata.reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this._tagger.tagMetadata(span, metadata)
|
|
390
|
+
}
|
|
194
391
|
}
|
|
195
392
|
|
|
196
393
|
// TODO: this will be moved to the APM integration
|
|
@@ -207,13 +404,18 @@ function normalizeOpenAIResourceName (resource) {
|
|
|
207
404
|
// embeddings
|
|
208
405
|
case 'embeddings.create':
|
|
209
406
|
return 'createEmbedding'
|
|
407
|
+
|
|
408
|
+
// responses
|
|
409
|
+
case 'responses.create':
|
|
410
|
+
return 'createResponse'
|
|
411
|
+
|
|
210
412
|
default:
|
|
211
413
|
return resource
|
|
212
414
|
}
|
|
213
415
|
}
|
|
214
416
|
|
|
215
417
|
function gateResource (resource) {
|
|
216
|
-
return ['createCompletion', 'createChatCompletion', 'createEmbedding'].includes(resource)
|
|
418
|
+
return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse'].includes(resource)
|
|
217
419
|
? resource
|
|
218
420
|
: undefined
|
|
219
421
|
}
|
|
@@ -226,6 +428,8 @@ function getOperation (resource) {
|
|
|
226
428
|
return 'chat'
|
|
227
429
|
case 'createEmbedding':
|
|
228
430
|
return 'embedding'
|
|
431
|
+
case 'createResponse':
|
|
432
|
+
return 'response'
|
|
229
433
|
default:
|
|
230
434
|
// should never happen
|
|
231
435
|
return 'unknown'
|
|
@@ -224,7 +224,8 @@ class LLMObsSpanProcessor {
|
|
|
224
224
|
continue
|
|
225
225
|
}
|
|
226
226
|
if (value !== null && typeof value === 'object') {
|
|
227
|
-
|
|
227
|
+
carrier[key] = Array.isArray(value) ? [] : {}
|
|
228
|
+
add(value, carrier[key])
|
|
228
229
|
} else {
|
|
229
230
|
carrier[key] = value
|
|
230
231
|
}
|
|
@@ -292,11 +292,17 @@ class LLMObsTagger {
|
|
|
292
292
|
continue
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const { result, toolId, type } = toolResult
|
|
295
|
+
const { result, toolId, name = '', type } = toolResult
|
|
296
296
|
const toolResultObj = {}
|
|
297
297
|
|
|
298
298
|
const condition1 = this.#tagConditionalString(result, 'Tool result', toolResultObj, 'result')
|
|
299
299
|
const condition2 = this.#tagConditionalString(toolId, 'Tool ID', toolResultObj, 'tool_id')
|
|
300
|
+
// name can be empty string, so always include it
|
|
301
|
+
if (typeof name === 'string') {
|
|
302
|
+
toolResultObj.name = name
|
|
303
|
+
} else {
|
|
304
|
+
this.#handleFailure(`[LLMObs] Expected tool result name to be a string, instead got "${typeof name}"`)
|
|
305
|
+
}
|
|
300
306
|
const condition3 = this.#tagConditionalString(type, 'Tool type', toolResultObj, 'type')
|
|
301
307
|
|
|
302
308
|
if (condition1 && condition2 && condition3) {
|
|
@@ -332,13 +338,13 @@ class LLMObsTagger {
|
|
|
332
338
|
const toolId = message.toolId
|
|
333
339
|
const messageObj = { content }
|
|
334
340
|
|
|
341
|
+
let condition = this.#tagConditionalString(role, 'Message role', messageObj, 'role')
|
|
342
|
+
|
|
335
343
|
const valid = typeof content === 'string'
|
|
336
344
|
if (!valid) {
|
|
337
345
|
this.#handleFailure('Message content must be a string.', 'invalid_io_messages')
|
|
338
346
|
}
|
|
339
347
|
|
|
340
|
-
let condition = this.#tagConditionalString(role, 'Message role', messageObj, 'role')
|
|
341
|
-
|
|
342
348
|
if (toolCalls) {
|
|
343
349
|
const filteredToolCalls = this.#filterToolCalls(toolCalls)
|
|
344
350
|
|
|
@@ -34,7 +34,8 @@ const {
|
|
|
34
34
|
getLibraryCapabilitiesTags,
|
|
35
35
|
getPullRequestDiff,
|
|
36
36
|
getModifiedFilesFromDiff,
|
|
37
|
-
getPullRequestBaseBranch
|
|
37
|
+
getPullRequestBaseBranch,
|
|
38
|
+
TEST_IS_TEST_FRAMEWORK_WORKER
|
|
38
39
|
} = require('./util/test')
|
|
39
40
|
const { getRepositoryRoot } = require('./util/git')
|
|
40
41
|
const Plugin = require('./plugin')
|
|
@@ -311,6 +312,7 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
311
312
|
span.parent_id = id(span.parent_id)
|
|
312
313
|
|
|
313
314
|
if (span.name?.startsWith(`${this.constructor.id}.`)) {
|
|
315
|
+
span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
|
|
314
316
|
// augment with git information (since it will not be available in the worker)
|
|
315
317
|
for (const key in this.testEnvironmentMetadata) {
|
|
316
318
|
// CAREFUL: this bypasses the metadata/metrics distinction
|
|
@@ -92,6 +92,8 @@ const CI_APP_ORIGIN = 'ciapp-test'
|
|
|
92
92
|
const JEST_TEST_RUNNER = 'test.jest.test_runner'
|
|
93
93
|
const JEST_DISPLAY_NAME = 'test.jest.display_name'
|
|
94
94
|
|
|
95
|
+
const VITEST_POOL = 'test.vitest.pool'
|
|
96
|
+
|
|
95
97
|
const CUCUMBER_IS_PARALLEL = 'test.cucumber.is_parallel'
|
|
96
98
|
const MOCHA_IS_PARALLEL = 'test.mocha.is_parallel'
|
|
97
99
|
|
|
@@ -130,6 +132,8 @@ const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90
|
|
|
130
132
|
const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100
|
|
131
133
|
const VITEST_WORKER_LOGS_PAYLOAD_CODE = 102
|
|
132
134
|
|
|
135
|
+
const TEST_IS_TEST_FRAMEWORK_WORKER = 'test.is_test_framework_worker'
|
|
136
|
+
|
|
133
137
|
// Library Capabilities Tagging
|
|
134
138
|
const DD_CAPABILITIES_TEST_IMPACT_ANALYSIS = '_dd.library_capabilities.test_impact_analysis'
|
|
135
139
|
const DD_CAPABILITIES_EARLY_FLAKE_DETECTION = '_dd.library_capabilities.early_flake_detection'
|
|
@@ -203,6 +207,7 @@ module.exports = {
|
|
|
203
207
|
TEST_FRAMEWORK_VERSION,
|
|
204
208
|
JEST_TEST_RUNNER,
|
|
205
209
|
JEST_DISPLAY_NAME,
|
|
210
|
+
VITEST_POOL,
|
|
206
211
|
CUCUMBER_IS_PARALLEL,
|
|
207
212
|
MOCHA_IS_PARALLEL,
|
|
208
213
|
TEST_TYPE,
|
|
@@ -223,6 +228,7 @@ module.exports = {
|
|
|
223
228
|
PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
|
|
224
229
|
VITEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
225
230
|
VITEST_WORKER_LOGS_PAYLOAD_CODE,
|
|
231
|
+
TEST_IS_TEST_FRAMEWORK_WORKER,
|
|
226
232
|
TEST_SOURCE_START,
|
|
227
233
|
TEST_SKIPPED_BY_ITR,
|
|
228
234
|
TEST_IS_NEW,
|
|
@@ -269,9 +269,27 @@ module.exports = { Config }
|
|
|
269
269
|
function getProfilers ({
|
|
270
270
|
DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS
|
|
271
271
|
}) {
|
|
272
|
-
// First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to
|
|
272
|
+
// First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to space + wall
|
|
273
273
|
// Use a Set to avoid duplicates
|
|
274
|
-
|
|
274
|
+
// NOTE: space profiler is very deliberately in the first position. This way
|
|
275
|
+
// when profilers are stopped sequentially one after the other to create
|
|
276
|
+
// snapshots the space profile won't include memory taken by profiles created
|
|
277
|
+
// before it in the sequence. That memory is ultimately transient and will be
|
|
278
|
+
// released when all profiles are subsequently encoded.
|
|
279
|
+
const profilers = new Set((DD_PROFILING_PROFILERS ?? 'space,wall').split(','))
|
|
280
|
+
|
|
281
|
+
let spaceExplicitlyEnabled = false
|
|
282
|
+
// Add/remove space depending on the value of DD_PROFILING_HEAP_ENABLED
|
|
283
|
+
if (DD_PROFILING_HEAP_ENABLED != null) {
|
|
284
|
+
if (isTrue(DD_PROFILING_HEAP_ENABLED)) {
|
|
285
|
+
if (!profilers.has('space')) {
|
|
286
|
+
profilers.add('space')
|
|
287
|
+
spaceExplicitlyEnabled = true
|
|
288
|
+
}
|
|
289
|
+
} else if (isFalse(DD_PROFILING_HEAP_ENABLED)) {
|
|
290
|
+
profilers.delete('space')
|
|
291
|
+
}
|
|
292
|
+
}
|
|
275
293
|
|
|
276
294
|
// Add/remove wall depending on the value of DD_PROFILING_WALLTIME_ENABLED
|
|
277
295
|
if (DD_PROFILING_WALLTIME_ENABLED != null) {
|
|
@@ -279,19 +297,23 @@ function getProfilers ({
|
|
|
279
297
|
profilers.add('wall')
|
|
280
298
|
} else if (isFalse(DD_PROFILING_WALLTIME_ENABLED)) {
|
|
281
299
|
profilers.delete('wall')
|
|
300
|
+
profilers.delete('cpu') // remove alias too
|
|
282
301
|
}
|
|
283
302
|
}
|
|
284
303
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
const profilersArray = [...profilers]
|
|
305
|
+
// If space was added through DD_PROFILING_HEAP_ENABLED, ensure it is in the
|
|
306
|
+
// first position. Basically, the only way for it not to be in the first
|
|
307
|
+
// position is if it was explicitly specified in a different position in
|
|
308
|
+
// DD_PROFILING_PROFILERS.
|
|
309
|
+
if (spaceExplicitlyEnabled) {
|
|
310
|
+
const spaceIdx = profilersArray.indexOf('space')
|
|
311
|
+
if (spaceIdx > 0) {
|
|
312
|
+
profilersArray.splice(spaceIdx, 1)
|
|
313
|
+
profilersArray.unshift('space')
|
|
291
314
|
}
|
|
292
315
|
}
|
|
293
|
-
|
|
294
|
-
return [...profilers]
|
|
316
|
+
return profilersArray
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
function getExportStrategy (name, options) {
|
|
@@ -16,6 +16,8 @@ module.exports = {
|
|
|
16
16
|
APM_TRACING_LOGS_INJECTION: 1n << 13n,
|
|
17
17
|
APM_TRACING_HTTP_HEADER_TAGS: 1n << 14n,
|
|
18
18
|
APM_TRACING_CUSTOM_TAGS: 1n << 15n,
|
|
19
|
+
ASM_PROCESSOR_OVERRIDES: 1n << 16n,
|
|
20
|
+
ASM_CUSTOM_DATA_SCANNERS: 1n << 17n,
|
|
19
21
|
ASM_EXCLUSION_DATA: 1n << 18n,
|
|
20
22
|
APM_TRACING_ENABLED: 1n << 19n,
|
|
21
23
|
ASM_RASP_SQLI: 1n << 21n,
|
|
@@ -90,6 +90,8 @@ function enableWafUpdate (appsecConfig) {
|
|
|
90
90
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true)
|
|
91
91
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true)
|
|
92
92
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true)
|
|
93
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, true)
|
|
94
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, true)
|
|
93
95
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, true)
|
|
94
96
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true)
|
|
95
97
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, true)
|
|
@@ -129,6 +131,8 @@ function disableWafUpdate () {
|
|
|
129
131
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false)
|
|
130
132
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false)
|
|
131
133
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false)
|
|
134
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, false)
|
|
135
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, false)
|
|
132
136
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, false)
|
|
133
137
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false)
|
|
134
138
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, false)
|
|
@@ -444,6 +444,7 @@
|
|
|
444
444
|
"DD_VERSION": ["A"],
|
|
445
445
|
"DD_VERTEXAI_SPAN_CHAR_LIMIT": ["A"],
|
|
446
446
|
"DD_VERTEXAI_SPAN_PROMPT_COMPLETION_SAMPLE_RATE": ["A"],
|
|
447
|
+
"DD_VITEST_WORKER": ["A"],
|
|
447
448
|
"OTEL_LOG_LEVEL": ["A"],
|
|
448
449
|
"OTEL_LOGS_EXPORTER": ["A"],
|
|
449
450
|
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": ["A"],
|