dd-trace 5.92.0 → 5.94.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/package.json +15 -11
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +23 -0
- package/packages/datadog-instrumentations/src/jest.js +118 -32
- package/packages/datadog-instrumentations/src/mocha/main.js +6 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +89 -5
- package/packages/datadog-instrumentations/src/playwright.js +10 -0
- package/packages/datadog-instrumentations/src/vitest.js +119 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -0
- package/packages/datadog-plugin-jest/src/index.js +6 -0
- package/packages/datadog-plugin-mocha/src/index.js +11 -0
- package/packages/datadog-plugin-playwright/src/index.js +9 -0
- package/packages/datadog-plugin-vitest/src/index.js +9 -0
- package/packages/datadog-webpack/index.js +187 -0
- package/packages/datadog-webpack/src/loader.js +27 -0
- package/packages/datadog-webpack/src/log.js +32 -0
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +103 -32
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +10 -21
- package/packages/dd-trace/src/config/supported-configurations.json +2 -2
- package/packages/dd-trace/src/crashtracking/index.js +7 -1
- package/packages/dd-trace/src/exporters/common/docker.js +1 -0
- package/packages/dd-trace/src/exporters/common/request.js +26 -17
- package/packages/dd-trace/src/opentracing/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +10 -7
- package/packages/dd-trace/src/plugins/util/test.js +76 -0
- package/packages/dd-trace/src/priority_sampler.js +6 -3
- package/packages/dd-trace/src/profiling/profiler.js +78 -47
- package/packages/dd-trace/src/profiling/profilers/wall.js +35 -28
- package/packages/dd-trace/src/proxy.js +4 -3
- package/packages/dd-trace/src/tracer_metadata.js +10 -1
- package/webpack.js +3 -0
|
@@ -17,6 +17,36 @@ const {
|
|
|
17
17
|
|
|
18
18
|
const { getNumFromKnownTests } = require('../../plugins/util/test')
|
|
19
19
|
|
|
20
|
+
const MAX_KNOWN_TESTS_PAGES = 10_000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Deep-merges page tests into aggregate.
|
|
24
|
+
* Structure: { module: { suite: [testName, ...] } }
|
|
25
|
+
*/
|
|
26
|
+
function mergeKnownTests (aggregate, page) {
|
|
27
|
+
if (!page) return aggregate
|
|
28
|
+
if (!aggregate) return page
|
|
29
|
+
|
|
30
|
+
for (const [moduleName, suites] of Object.entries(page)) {
|
|
31
|
+
if (!suites) continue
|
|
32
|
+
|
|
33
|
+
if (!aggregate[moduleName]) {
|
|
34
|
+
aggregate[moduleName] = suites
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [suiteName, tests] of Object.entries(suites)) {
|
|
39
|
+
if (!tests || tests.length === 0) continue
|
|
40
|
+
|
|
41
|
+
aggregate[moduleName][suiteName] = aggregate[moduleName][suiteName]
|
|
42
|
+
? [...aggregate[moduleName][suiteName], ...tests]
|
|
43
|
+
: tests
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return aggregate
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
function getKnownTests ({
|
|
21
51
|
url,
|
|
22
52
|
isEvpProxy,
|
|
@@ -59,53 +89,94 @@ function getKnownTests ({
|
|
|
59
89
|
options.headers['dd-api-key'] = apiKey
|
|
60
90
|
}
|
|
61
91
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'os.architecture': osArchitecture,
|
|
71
|
-
'runtime.name': runtimeName,
|
|
72
|
-
'runtime.version': runtimeVersion,
|
|
73
|
-
custom,
|
|
74
|
-
},
|
|
75
|
-
service,
|
|
76
|
-
env,
|
|
77
|
-
repository_url: repositoryUrl,
|
|
78
|
-
sha,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
})
|
|
92
|
+
const configurations = {
|
|
93
|
+
'os.platform': osPlatform,
|
|
94
|
+
'os.version': osVersion,
|
|
95
|
+
'os.architecture': osArchitecture,
|
|
96
|
+
'runtime.name': runtimeName,
|
|
97
|
+
'runtime.version': runtimeVersion,
|
|
98
|
+
custom,
|
|
99
|
+
}
|
|
82
100
|
|
|
83
101
|
incrementCountMetric(TELEMETRY_KNOWN_TESTS)
|
|
84
102
|
|
|
85
103
|
const startTime = Date.now()
|
|
104
|
+
let aggregateTests = null
|
|
105
|
+
let totalResponseBytes = 0
|
|
106
|
+
let pageNumber = 0
|
|
107
|
+
|
|
108
|
+
function fetchPage (pageState) {
|
|
109
|
+
pageNumber++
|
|
110
|
+
|
|
111
|
+
if (pageNumber > MAX_KNOWN_TESTS_PAGES) {
|
|
112
|
+
log.error('Known tests pagination exceeded maximum of %d pages. Aborting.', MAX_KNOWN_TESTS_PAGES)
|
|
113
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
114
|
+
return done(new Error(`Known tests pagination exceeded maximum of ${MAX_KNOWN_TESTS_PAGES} pages`))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pageInfo = pageState ? { page_state: pageState } : {}
|
|
118
|
+
|
|
119
|
+
const data = JSON.stringify({
|
|
120
|
+
data: {
|
|
121
|
+
id: id().toString(10),
|
|
122
|
+
type: 'ci_app_libraries_tests_request',
|
|
123
|
+
attributes: {
|
|
124
|
+
configurations,
|
|
125
|
+
service,
|
|
126
|
+
env,
|
|
127
|
+
repository_url: repositoryUrl,
|
|
128
|
+
sha,
|
|
129
|
+
page_info: pageInfo,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
request(data, options, (err, res, statusCode) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
137
|
+
incrementCountMetric(TELEMETRY_KNOWN_TESTS_ERRORS, { statusCode })
|
|
138
|
+
return done(err)
|
|
139
|
+
}
|
|
86
140
|
|
|
87
|
-
request(data, options, (err, res, statusCode) => {
|
|
88
|
-
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
89
|
-
if (err) {
|
|
90
|
-
incrementCountMetric(TELEMETRY_KNOWN_TESTS_ERRORS, { statusCode })
|
|
91
|
-
done(err)
|
|
92
|
-
} else {
|
|
93
141
|
try {
|
|
94
|
-
|
|
142
|
+
totalResponseBytes += res.length
|
|
143
|
+
|
|
144
|
+
const { data: { attributes } } = JSON.parse(res)
|
|
145
|
+
const { tests: pageTests, page_info: responsePageInfo } = attributes
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
aggregateTests = mergeKnownTests(aggregateTests, pageTests)
|
|
148
|
+
|
|
149
|
+
// Check if there are more pages
|
|
150
|
+
if (responsePageInfo && responsePageInfo.has_next) {
|
|
151
|
+
if (!responsePageInfo.cursor) {
|
|
152
|
+
log.error(
|
|
153
|
+
'Known tests response has has_next=true but no cursor on page %d. Aborting pagination.', pageNumber
|
|
154
|
+
)
|
|
155
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
156
|
+
return done(new Error('Known tests pagination: has_next=true but no cursor'))
|
|
157
|
+
}
|
|
158
|
+
return fetchPage(responsePageInfo.cursor)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Done — no more pages
|
|
162
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
163
|
+
|
|
164
|
+
const numTests = getNumFromKnownTests(aggregateTests)
|
|
97
165
|
|
|
98
166
|
distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, {}, numTests)
|
|
99
|
-
distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES, {},
|
|
167
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES, {}, totalResponseBytes)
|
|
100
168
|
|
|
101
169
|
log.debug('Number of received known tests:', numTests)
|
|
102
170
|
|
|
103
|
-
done(null,
|
|
171
|
+
done(null, aggregateTests)
|
|
104
172
|
} catch (err) {
|
|
173
|
+
distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime)
|
|
105
174
|
done(err)
|
|
106
175
|
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fetchPage(null)
|
|
109
180
|
}
|
|
110
181
|
|
|
111
182
|
module.exports = { getKnownTests }
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
const { JSONEncoder } = require('../../encode/json-encoder')
|
|
3
3
|
const { getEnvironmentVariable } = require('../../../config/helper')
|
|
4
4
|
const log = require('../../../log')
|
|
5
|
-
const {
|
|
6
|
-
VITEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
7
|
-
VITEST_WORKER_LOGS_PAYLOAD_CODE,
|
|
8
|
-
} = require('../../../plugins/util/test')
|
|
9
5
|
|
|
10
6
|
class Writer {
|
|
11
7
|
constructor (interprocessCode) {
|
|
@@ -29,12 +25,6 @@ class Writer {
|
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
_sendPayload (data, onDone = () => {}) {
|
|
32
|
-
// ## Jest
|
|
33
|
-
// Only available when `child_process` is used for the jest worker.
|
|
34
|
-
// If worker_threads is used, this will not work
|
|
35
|
-
// TODO: make `jest` instrumentation compatible with worker_threads
|
|
36
|
-
// https://github.com/facebook/jest/blob/bb39cb2c617a3334bf18daeca66bd87b7ccab28b/packages/jest-worker/README.md#experimental-worker
|
|
37
|
-
|
|
38
28
|
// ## Cucumber
|
|
39
29
|
// This reports to the test's main process the same way test data is reported by Cucumber
|
|
40
30
|
// See cucumber code:
|
|
@@ -47,19 +37,17 @@ class Writer {
|
|
|
47
37
|
? { __tinypool_worker_message__: true, interprocessCode: this._interprocessCode, data }
|
|
48
38
|
: [this._interprocessCode, data]
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
this._interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE ||
|
|
52
|
-
this._interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE
|
|
53
|
-
|
|
40
|
+
// child_process workers (jest default, cucumber)
|
|
54
41
|
if (process.send) {
|
|
55
42
|
process.send(payload, () => {
|
|
56
43
|
onDone()
|
|
57
44
|
})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// worker_threads (jest --workerThreads, vitest)
|
|
49
|
+
const { isMainThread, parentPort } = require('node:worker_threads')
|
|
50
|
+
if (!isMainThread && parentPort) {
|
|
63
51
|
try {
|
|
64
52
|
parentPort.postMessage(payload)
|
|
65
53
|
} catch (error) {
|
|
@@ -67,9 +55,10 @@ class Writer {
|
|
|
67
55
|
} finally {
|
|
68
56
|
onDone()
|
|
69
57
|
}
|
|
70
|
-
|
|
71
|
-
onDone()
|
|
58
|
+
return
|
|
72
59
|
}
|
|
60
|
+
|
|
61
|
+
onDone()
|
|
73
62
|
}
|
|
74
63
|
}
|
|
75
64
|
|
|
@@ -744,9 +744,9 @@
|
|
|
744
744
|
],
|
|
745
745
|
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
|
|
746
746
|
{
|
|
747
|
-
"implementation": "
|
|
747
|
+
"implementation": "B",
|
|
748
748
|
"type": "boolean",
|
|
749
|
-
"default": "
|
|
749
|
+
"default": "true",
|
|
750
750
|
"configurationNames": [
|
|
751
751
|
"propagateProcessTags.enabled"
|
|
752
752
|
]
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { existsSync } = require('node:fs')
|
|
3
4
|
const { isMainThread } = require('worker_threads')
|
|
4
5
|
const log = require('../log')
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// libdatadog v29 crashtracker segfaults during init on ARM64 musl (Alpine).
|
|
8
|
+
// The segfault bypasses JS try/catch so we must avoid loading it entirely.
|
|
9
|
+
// See: https://github.com/DataDog/libdatadog-nodejs/issues/114
|
|
10
|
+
const isArm64Musl = process.arch === 'arm64' && existsSync('/etc/alpine-release')
|
|
11
|
+
|
|
12
|
+
if (isMainThread && !isArm64Musl) {
|
|
7
13
|
try {
|
|
8
14
|
module.exports = require('./crashtracker')
|
|
9
15
|
} catch (e) {
|
|
@@ -14,9 +14,9 @@ const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
|
|
|
14
14
|
const docker = require('./docker')
|
|
15
15
|
const { httpAgent, httpsAgent } = require('./agents')
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const maxActiveBufferSize = 1024 * 1024 * 64
|
|
18
18
|
|
|
19
|
-
let
|
|
19
|
+
let activeBufferSize = 0
|
|
20
20
|
|
|
21
21
|
function parseUrl (urlObjOrString) {
|
|
22
22
|
if (urlObjOrString !== null && typeof urlObjOrString === 'object') return urlToHttpOptions(urlObjOrString)
|
|
@@ -50,7 +50,22 @@ function request (data, options, callback) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
if (data instanceof Readable) {
|
|
54
|
+
const chunks = []
|
|
55
|
+
|
|
56
|
+
data
|
|
57
|
+
.on('data', (data) => {
|
|
58
|
+
chunks.push(data)
|
|
59
|
+
})
|
|
60
|
+
.on('end', () => {
|
|
61
|
+
request(Buffer.concat(chunks), options, callback)
|
|
62
|
+
})
|
|
63
|
+
.on('error', (err) => {
|
|
64
|
+
callback(err)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
}
|
|
54
69
|
|
|
55
70
|
// The timeout should be kept low to avoid excessive queueing.
|
|
56
71
|
const timeout = options.timeout || 2000
|
|
@@ -58,12 +73,10 @@ function request (data, options, callback) {
|
|
|
58
73
|
const client = isSecure ? https : http
|
|
59
74
|
let dataArray = data
|
|
60
75
|
|
|
61
|
-
if (!
|
|
62
|
-
|
|
63
|
-
dataArray = [data]
|
|
64
|
-
}
|
|
65
|
-
options.headers['Content-Length'] = byteLength(dataArray)
|
|
76
|
+
if (!Array.isArray(data)) {
|
|
77
|
+
dataArray = [data]
|
|
66
78
|
}
|
|
79
|
+
options.headers['Content-Length'] = byteLength(dataArray)
|
|
67
80
|
|
|
68
81
|
docker.inject(options.headers)
|
|
69
82
|
|
|
@@ -126,14 +139,14 @@ function request (data, options, callback) {
|
|
|
126
139
|
return callback(null)
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
activeBufferSize += options.headers['Content-Length'] ?? 0
|
|
130
143
|
|
|
131
144
|
storage('legacy').run({ noop: true }, () => {
|
|
132
145
|
let finished = false
|
|
133
146
|
const finalize = () => {
|
|
134
147
|
if (finished) return
|
|
135
148
|
finished = true
|
|
136
|
-
|
|
149
|
+
activeBufferSize -= options.headers['Content-Length'] ?? 0
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
const req = client.request(options, (res) => onResponse(res, finalize))
|
|
@@ -158,12 +171,8 @@ function request (data, options, callback) {
|
|
|
158
171
|
}
|
|
159
172
|
})
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} else {
|
|
164
|
-
for (const buffer of dataArray) req.write(buffer)
|
|
165
|
-
req.end()
|
|
166
|
-
}
|
|
174
|
+
for (const buffer of dataArray) req.write(buffer)
|
|
175
|
+
req.end()
|
|
167
176
|
})
|
|
168
177
|
}
|
|
169
178
|
|
|
@@ -183,7 +192,7 @@ function byteLength (data) {
|
|
|
183
192
|
|
|
184
193
|
Object.defineProperty(request, 'writable', {
|
|
185
194
|
get () {
|
|
186
|
-
return
|
|
195
|
+
return activeBufferSize < maxActiveBufferSize
|
|
187
196
|
},
|
|
188
197
|
})
|
|
189
198
|
|
|
@@ -35,6 +35,7 @@ const integrationCounters = {
|
|
|
35
35
|
|
|
36
36
|
const startCh = channel('dd-trace:span:start')
|
|
37
37
|
const finishCh = channel('dd-trace:span:finish')
|
|
38
|
+
const tagsUpdateCh = channel('dd-trace:span:tags:update')
|
|
38
39
|
|
|
39
40
|
function getIntegrationCounter (event, integration) {
|
|
40
41
|
const counters = integrationCounters[event]
|
|
@@ -399,6 +400,10 @@ class DatadogSpan {
|
|
|
399
400
|
tagger.add(this._spanContext._tags, keyValuePairs)
|
|
400
401
|
|
|
401
402
|
this._prioritySampler.sample(this, false)
|
|
403
|
+
|
|
404
|
+
if (tagsUpdateCh.hasSubscribers) {
|
|
405
|
+
tagsUpdateCh.publish(this)
|
|
406
|
+
}
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
|
|
@@ -18,13 +18,6 @@ const TEST_OPTIMIZATION_PLUGINS = new Set([
|
|
|
18
18
|
|
|
19
19
|
const loadChannel = channel('dd-trace:instrumentation:load')
|
|
20
20
|
|
|
21
|
-
// instrument everything that needs Plugin System V2 instrumentation
|
|
22
|
-
require('../../datadog-instrumentations')
|
|
23
|
-
if (getEnvironmentVariable('AWS_LAMBDA_FUNCTION_NAME') !== undefined) {
|
|
24
|
-
// instrument lambda environment
|
|
25
|
-
require('./lambda')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
const DD_TRACE_DISABLED_PLUGINS = getValueFromEnvSources('DD_TRACE_DISABLED_PLUGINS')
|
|
29
22
|
|
|
30
23
|
const disabledPlugins = new Set(
|
|
@@ -35,10 +28,20 @@ const disabledPlugins = new Set(
|
|
|
35
28
|
|
|
36
29
|
const pluginClasses = {}
|
|
37
30
|
|
|
31
|
+
// Subscribe before requiring instrumentations so that loadChannel events fired
|
|
32
|
+
// during instrumentation initialization (e.g. re-requires in bundler contexts)
|
|
33
|
+
// are captured and populate pluginClasses correctly.
|
|
38
34
|
loadChannel.subscribe(({ name }) => {
|
|
39
35
|
maybeEnable(plugins[name])
|
|
40
36
|
})
|
|
41
37
|
|
|
38
|
+
// instrument everything that needs Plugin System V2 instrumentation
|
|
39
|
+
require('../../datadog-instrumentations')
|
|
40
|
+
if (getEnvironmentVariable('AWS_LAMBDA_FUNCTION_NAME') !== undefined) {
|
|
41
|
+
// instrument lambda environment
|
|
42
|
+
require('./lambda')
|
|
43
|
+
}
|
|
44
|
+
|
|
42
45
|
function maybeEnable (Plugin) {
|
|
43
46
|
if (!Plugin || typeof Plugin !== 'function') return
|
|
44
47
|
if (!pluginClasses[Plugin.id]) {
|
|
@@ -88,8 +88,24 @@ const TEST_EARLY_FLAKE_ABORT_REASON = 'test.early_flake.abort_reason'
|
|
|
88
88
|
const TEST_RETRY_REASON = 'test.retry_reason'
|
|
89
89
|
const TEST_HAS_FAILED_ALL_RETRIES = 'test.has_failed_all_retries'
|
|
90
90
|
const TEST_IS_MODIFIED = 'test.is_modified'
|
|
91
|
+
const TEST_HAS_DYNAMIC_NAME = '_dd.has_dynamic_name'
|
|
91
92
|
const CI_APP_ORIGIN = 'ciapp-test'
|
|
92
93
|
|
|
94
|
+
// Matches patterns that are almost certainly runtime-generated values in test names:
|
|
95
|
+
// - Unix timestamps in ms (13 digits, years ~2020-2090) or s (10 digits)
|
|
96
|
+
// - UUIDs (8-4-4-4-12 hex)
|
|
97
|
+
// - ISO 8601 dates (2024-03-23) or date-times (2024-03-23T14:30)
|
|
98
|
+
// - Random ports on localhost, 127.0.0.1, or 0.0.0.0
|
|
99
|
+
// - Math.random() float values (10+ decimal digits after 0.)
|
|
100
|
+
const DYNAMIC_NAME_RE = new RegExp(
|
|
101
|
+
String.raw`\b1[6-9]\d{8,11}\b|` +
|
|
102
|
+
String.raw`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|` +
|
|
103
|
+
String.raw`\b\d{4}-\d{2}-\d{2}|` +
|
|
104
|
+
String.raw`(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d{4,5}\b|` +
|
|
105
|
+
String.raw`\b0\.\d{10,}`,
|
|
106
|
+
'i'
|
|
107
|
+
)
|
|
108
|
+
|
|
93
109
|
const JEST_TEST_RUNNER = 'test.jest.test_runner'
|
|
94
110
|
const JEST_DISPLAY_NAME = 'test.jest.display_name'
|
|
95
111
|
|
|
@@ -260,6 +276,7 @@ module.exports = {
|
|
|
260
276
|
TEST_RETRY_REASON,
|
|
261
277
|
TEST_HAS_FAILED_ALL_RETRIES,
|
|
262
278
|
TEST_IS_MODIFIED,
|
|
279
|
+
TEST_HAS_DYNAMIC_NAME,
|
|
263
280
|
getTestEnvironmentMetadata,
|
|
264
281
|
getTestParametersString,
|
|
265
282
|
finishAllTraceSpans,
|
|
@@ -337,6 +354,9 @@ module.exports = {
|
|
|
337
354
|
POSSIBLE_BASE_BRANCHES,
|
|
338
355
|
GIT_COMMIT_SHA,
|
|
339
356
|
GIT_REPOSITORY_URL,
|
|
357
|
+
DYNAMIC_NAME_RE,
|
|
358
|
+
collectDynamicNamesFromTraces,
|
|
359
|
+
logDynamicNamesWarning,
|
|
340
360
|
}
|
|
341
361
|
|
|
342
362
|
// Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
|
|
@@ -1178,6 +1198,62 @@ function getModifiedFilesFromDiff (diff) {
|
|
|
1178
1198
|
return result
|
|
1179
1199
|
}
|
|
1180
1200
|
|
|
1201
|
+
/**
|
|
1202
|
+
* Scans serialized worker trace payloads for tests tagged with TEST_HAS_DYNAMIC_NAME
|
|
1203
|
+
* and populates the provided Set. Silently ignores parse errors.
|
|
1204
|
+
*
|
|
1205
|
+
* @param {string} data - JSON-serialized traces from a worker
|
|
1206
|
+
* @param {Set<string>} newTestsWithDynamicNames - Set to populate with "suite › name" strings
|
|
1207
|
+
*/
|
|
1208
|
+
function collectDynamicNamesFromTraces (data, newTestsWithDynamicNames) {
|
|
1209
|
+
try {
|
|
1210
|
+
const traces = JSON.parse(data)
|
|
1211
|
+
for (const trace of traces) {
|
|
1212
|
+
for (const span of trace) {
|
|
1213
|
+
if (span.meta?.[TEST_HAS_DYNAMIC_NAME] === 'true') {
|
|
1214
|
+
const suite = span.meta[TEST_SUITE]
|
|
1215
|
+
const name = span.meta[TEST_NAME]
|
|
1216
|
+
if (suite && name) {
|
|
1217
|
+
newTestsWithDynamicNames.add(`${suite} › ${name}`)
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
} catch {
|
|
1223
|
+
// ignore parse errors
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Logs a "Datadog Test Optimization" warning about new tests with dynamic names.
|
|
1229
|
+
* Clears the Set after logging. No-op if the Set is empty.
|
|
1230
|
+
*
|
|
1231
|
+
* @param {Set<string>} newTestsWithDynamicNames
|
|
1232
|
+
*/
|
|
1233
|
+
function logDynamicNamesWarning (newTestsWithDynamicNames) {
|
|
1234
|
+
if (newTestsWithDynamicNames.size === 0) return
|
|
1235
|
+
|
|
1236
|
+
const MAX_SHOWN = 10
|
|
1237
|
+
const names = [...newTestsWithDynamicNames]
|
|
1238
|
+
const shown = names.slice(0, MAX_SHOWN)
|
|
1239
|
+
const more = names.length - shown.length
|
|
1240
|
+
const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
|
|
1241
|
+
const nameList = shown.map(n => ` • ${n}`).join('\n') + moreSuffix
|
|
1242
|
+
|
|
1243
|
+
const line = '-'.repeat(50)
|
|
1244
|
+
// eslint-disable-next-line no-console -- Intentional user-facing session summary
|
|
1245
|
+
console.warn(
|
|
1246
|
+
`\n${line}\nDatadog Test Optimization\n${line}\n` +
|
|
1247
|
+
`${newTestsWithDynamicNames.size} test(s) detected as new but their names contain ` +
|
|
1248
|
+
'dynamic data (timestamps, UUIDs, etc.).\n' +
|
|
1249
|
+
'Tests with changing names are always treated as new on every run, ' +
|
|
1250
|
+
'causing unnecessary Early Flake Detection retries and preventing correct new test detection.\n' +
|
|
1251
|
+
'Consider using stable, deterministic test names.\n\n' +
|
|
1252
|
+
`${nameList}\n`
|
|
1253
|
+
)
|
|
1254
|
+
newTestsWithDynamicNames.clear()
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1181
1257
|
function isModifiedTest (testPath, testStartLine, testEndLine, modifiedFiles, testFramework) {
|
|
1182
1258
|
if (modifiedFiles === undefined) {
|
|
1183
1259
|
return false
|
|
@@ -37,13 +37,16 @@ const {
|
|
|
37
37
|
const DEFAULT_KEY = 'service:,env:'
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Formats a sampling rate as a string with up to 6
|
|
40
|
+
* Formats a sampling rate as a string with up to 6 decimal digits and no trailing zeros.
|
|
41
41
|
*
|
|
42
42
|
* @param {number} rate
|
|
43
|
-
* @returns {string}
|
|
44
43
|
*/
|
|
45
44
|
function formatKnuthRate (rate) {
|
|
46
|
-
|
|
45
|
+
const string = Number(rate).toFixed(6)
|
|
46
|
+
for (let i = string.length - 1; i > 0; i--) {
|
|
47
|
+
if (string[i] === '0') continue
|
|
48
|
+
return string.slice(0, i + (string[i] === '.' ? 0 : 1))
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const defaultSampler = new Sampler(AUTO_KEEP)
|