dd-trace 5.9.0 → 5.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -69,7 +69,7 @@
69
69
  "node": ">=18"
70
70
  },
71
71
  "dependencies": {
72
- "@datadog/native-appsec": "7.1.0",
72
+ "@datadog/native-appsec": "7.1.1",
73
73
  "@datadog/native-iast-rewriter": "2.3.0",
74
74
  "@datadog/native-iast-taint-tracking": "1.7.0",
75
75
  "@datadog/native-metrics": "^2.0.0",
@@ -44,11 +44,14 @@ const knownTestsCh = channel('ci:jest:known-tests')
44
44
 
45
45
  const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
46
46
 
47
+ // Message sent by jest's main process to workers to run a test suite (=test file)
48
+ // https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/types.ts#L37
49
+ const CHILD_MESSAGE_CALL = 1
47
50
  // Maximum time we'll wait for the tracer to flush
48
51
  const FLUSH_TIMEOUT = 10000
49
52
 
50
53
  let skippableSuites = []
51
- let knownTests = []
54
+ let knownTests = {}
52
55
  let isCodeCoverageEnabled = false
53
56
  let isSuitesSkippingEnabled = false
54
57
  let isUserCodeCoverageEnabled = false
@@ -73,6 +76,7 @@ const specStatusToTestStatus = {
73
76
  const asyncResources = new WeakMap()
74
77
  const originalTestFns = new WeakMap()
75
78
  const retriedTestsToNumAttempts = new Map()
79
+ const newTestsTestStatuses = new Map()
76
80
 
77
81
  // based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
78
82
  function formatJestError (errors) {
@@ -101,6 +105,13 @@ function getTestEnvironmentOptions (config) {
101
105
  return {}
102
106
  }
103
107
 
108
+ function getEfdStats (testStatuses) {
109
+ return testStatuses.reduce((acc, testStatus) => {
110
+ acc[testStatus]++
111
+ return acc
112
+ }, { pass: 0, fail: 0 })
113
+ }
114
+
104
115
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
105
116
  return class DatadogEnvironment extends BaseEnvironment {
106
117
  constructor (config, context) {
@@ -123,9 +134,12 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
123
134
  this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
124
135
 
125
136
  if (this.isEarlyFlakeDetectionEnabled) {
137
+ const hasKnownTests = !!knownTests.jest
126
138
  earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
127
139
  try {
128
- this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
140
+ this.knownTestsForThisSuite = hasKnownTests
141
+ ? (knownTests.jest[this.testSuite] || [])
142
+ : this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
129
143
  } catch (e) {
130
144
  // If there has been an error parsing the tests, we'll disable Early Flake Deteciton
131
145
  this.isEarlyFlakeDetectionEnabled = false
@@ -145,7 +159,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
145
159
  if (typeof knownTestsForSuite === 'string') {
146
160
  knownTestsForSuite = JSON.parse(knownTestsForSuite)
147
161
  }
148
- return knownTestsForSuite.jest?.[this.testSuite] || []
162
+ return knownTestsForSuite
149
163
  }
150
164
 
151
165
  // Add the `add_test` event we don't have the test object yet, so
@@ -242,6 +256,19 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
242
256
  })
243
257
  // restore in case it is retried
244
258
  event.test.fn = originalTestFns.get(event.test)
259
+ // We'll store the test statuses of the retries
260
+ if (this.isEarlyFlakeDetectionEnabled) {
261
+ const testName = getJestTestName(event.test)
262
+ const originalTestName = removeEfdStringFromTestName(testName)
263
+ const isNewTest = retriedTestsToNumAttempts.has(originalTestName)
264
+ if (isNewTest) {
265
+ if (newTestsTestStatuses.has(originalTestName)) {
266
+ newTestsTestStatuses.get(originalTestName).push(status)
267
+ } else {
268
+ newTestsTestStatuses.set(originalTestName, [status])
269
+ }
270
+ }
271
+ }
245
272
  })
246
273
  }
247
274
  if (event.name === 'test_skip' || event.name === 'test_todo') {
@@ -508,6 +535,28 @@ function cliWrapper (cli, jestVersion) {
508
535
 
509
536
  numSkippedSuites = 0
510
537
 
538
+ /**
539
+ * If Early Flake Detection (EFD) is enabled the logic is as follows:
540
+ * - If all attempts for a test are failing, the test has failed and we will let the test process fail.
541
+ * - If just a single attempt passes, we will prevent the test process from failing.
542
+ * The rationale behind is the following: you may still be able to block your CI pipeline by gating
543
+ * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
544
+ */
545
+
546
+ if (isEarlyFlakeDetectionEnabled) {
547
+ let numFailedTestsToIgnore = 0
548
+ for (const testStatuses of newTestsTestStatuses.values()) {
549
+ const { pass, fail } = getEfdStats(testStatuses)
550
+ if (pass > 0) { // as long as one passes, we'll consider the test passed
551
+ numFailedTestsToIgnore += fail
552
+ }
553
+ }
554
+ // If every test that failed was an EFD retry, we'll consider the suite passed
555
+ if (numFailedTestsToIgnore !== 0 && result.results.numFailedTests === numFailedTestsToIgnore) {
556
+ result.results.success = true
557
+ }
558
+ }
559
+
511
560
  return result
512
561
  })
513
562
 
@@ -619,7 +668,6 @@ function configureTestEnvironment (readConfigsResult) {
619
668
  // because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions`
620
669
  configs.forEach(config => {
621
670
  config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled
622
- config.testEnvironmentOptions._ddKnownTests = knownTests
623
671
  })
624
672
 
625
673
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
@@ -795,6 +843,38 @@ addHook({
795
843
  file: 'build/workers/ChildProcessWorker.js'
796
844
  }, (childProcessWorker) => {
797
845
  const ChildProcessWorker = childProcessWorker.default
846
+ shimmer.wrap(ChildProcessWorker.prototype, 'send', send => function (request) {
847
+ if (!isEarlyFlakeDetectionEnabled) {
848
+ return send.apply(this, arguments)
849
+ }
850
+ const [type] = request
851
+ // eslint-disable-next-line
852
+ // https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/workers/ChildProcessWorker.ts#L424
853
+ if (type === CHILD_MESSAGE_CALL) {
854
+ // This is the message that the main process sends to the worker to run a test suite (=test file).
855
+ // In here we modify the config.testEnvironmentOptions to include the known tests for the suite.
856
+ // This way the suite only knows about the tests that are part of it.
857
+ const args = request[request.length - 1]
858
+ if (args.length > 1) {
859
+ return send.apply(this, arguments)
860
+ }
861
+ if (!args[0]?.config) {
862
+ return send.apply(this, arguments)
863
+ }
864
+ const [{ globalConfig, config, path: testSuiteAbsolutePath }] = args
865
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, globalConfig.rootDir || process.cwd())
866
+ const suiteKnownTests = knownTests.jest?.[testSuite] || []
867
+ args[0].config = {
868
+ ...config,
869
+ testEnvironmentOptions: {
870
+ ...config.testEnvironmentOptions,
871
+ _ddKnownTests: suiteKnownTests
872
+ }
873
+ }
874
+ }
875
+
876
+ return send.apply(this, arguments)
877
+ })
798
878
  shimmer.wrap(ChildProcessWorker.prototype, '_onMessage', _onMessage => function () {
799
879
  const [code, data] = arguments[0]
800
880
  if (code === JEST_WORKER_TRACE_PAYLOAD_CODE) { // datadog trace payload
@@ -68,7 +68,10 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
68
68
  const result = send.apply(this, arguments)
69
69
 
70
70
  result.then(
71
- innerAsyncResource.bind(() => producerFinishCh.publish(undefined)),
71
+ innerAsyncResource.bind(res => {
72
+ producerFinishCh.publish(undefined)
73
+ producerCommitCh.publish(res)
74
+ }),
72
75
  innerAsyncResource.bind(err => {
73
76
  if (err) {
74
77
  producerErrorCh.publish(err)
@@ -77,12 +80,6 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
77
80
  })
78
81
  )
79
82
 
80
- result.then(res => {
81
- if (producerCommitCh.hasSubscribers) {
82
- producerCommitCh.publish(res)
83
- }
84
- })
85
-
86
83
  return result
87
84
  } catch (e) {
88
85
  producerErrorCh.publish(e)
@@ -21,7 +21,7 @@ function finish (err) {
21
21
  finishChannel.publish(undefined)
22
22
  }
23
23
 
24
- addHook({ name: 'oracledb', versions: ['5'] }, oracledb => {
24
+ addHook({ name: 'oracledb', versions: ['>=5'] }, oracledb => {
25
25
  shimmer.wrap(oracledb.Connection.prototype, 'execute', execute => {
26
26
  return function wrappedExecute (dbQuery, ...args) {
27
27
  if (!startChannel.hasSubscribers) {
@@ -20,11 +20,15 @@ const EXCLUDED_LOCATIONS = getNodeModulesPaths(
20
20
  'pusher/lib/utils.js',
21
21
  'redlock/dist/cjs',
22
22
  'sqreen/lib/package-reader/index.js',
23
- 'ws/lib/websocket-server.js'
23
+ 'ws/lib/websocket-server.js',
24
+ 'google-gax/build/src/grpc.js',
25
+ 'cookie-signature/index.js'
24
26
  )
25
27
 
26
28
  const EXCLUDED_PATHS_FROM_STACK = [
27
- path.join('node_modules', 'object-hash', path.sep)
29
+ path.join('node_modules', 'object-hash', path.sep),
30
+ path.join('node_modules', 'aws-sdk', 'lib', 'util.js'),
31
+ path.join('node_modules', 'keygrip', path.sep)
28
32
  ]
29
33
  class WeakHashAnalyzer extends Analyzer {
30
34
  constructor () {
@@ -114,6 +114,10 @@ function getCommitsToUpload ({ url, repositoryUrl, latestCommits, isEvpProxy, ev
114
114
 
115
115
  const commitsToUpload = getCommitsRevList(alreadySeenCommits, commitsToInclude)
116
116
 
117
+ if (commitsToUpload === null) {
118
+ return callback(new Error('git rev-list failed'))
119
+ }
120
+
117
121
  callback(null, commitsToUpload)
118
122
  })
119
123
  }
@@ -252,9 +256,8 @@ function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryU
252
256
  return callback(new Error('Repository URL is empty'))
253
257
  }
254
258
 
255
- const latestCommits = getLatestCommits()
259
+ let latestCommits = getLatestCommits()
256
260
  log.debug(`There were ${latestCommits.length} commits since last month.`)
257
- const [headCommit] = latestCommits
258
261
 
259
262
  const getOnFinishGetCommitsToUpload = (hasCheckedShallow) => (err, commitsToUpload) => {
260
263
  if (err) {
@@ -268,6 +271,7 @@ function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryU
268
271
 
269
272
  // If it has already unshallowed or the clone is not shallow, we move on
270
273
  if (hasCheckedShallow || !isShallowRepository()) {
274
+ const [headCommit] = latestCommits
271
275
  return generateAndUploadPackFiles({
272
276
  url,
273
277
  isEvpProxy,
@@ -280,6 +284,9 @@ function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryU
280
284
  // Otherwise we unshallow and get commits to upload again
281
285
  log.debug('It is shallow clone, unshallowing...')
282
286
  unshallowRepository()
287
+
288
+ // The latest commits change after unshallowing
289
+ latestCommits = getLatestCommits()
283
290
  getCommitsToUpload({
284
291
  url,
285
292
  repositoryUrl,
@@ -27,6 +27,7 @@ const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private
27
27
  const defaultWafObfuscatorKeyRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?)key)|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization'
28
28
  // eslint-disable-next-line max-len
29
29
  const defaultWafObfuscatorValueRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}'
30
+ const runtimeId = uuid()
30
31
 
31
32
  function maybeFile (filepath) {
32
33
  if (!filepath) return
@@ -291,7 +292,7 @@ class Config {
291
292
  service: this.service,
292
293
  env: this.env,
293
294
  version: this.version,
294
- 'runtime-id': uuid()
295
+ 'runtime-id': runtimeId
295
296
  })
296
297
 
297
298
  if (this.isCiVisibility) {
@@ -823,6 +824,7 @@ class Config {
823
824
  : undefined
824
825
 
825
826
  tagger.add(tags, options.tracing_tags)
827
+ if (Object.keys(tags).length) tags['runtime-id'] = runtimeId
826
828
 
827
829
  this._setUnit(opts, 'sampleRate', options.tracing_sampling_rate)
828
830
  this._setBoolean(opts, 'logInjection', options.log_injection_enabled)
@@ -28,7 +28,7 @@ const {
28
28
  const { filterSensitiveInfoFromRepository } = require('./url')
29
29
  const { storage } = require('../../../../datadog-core')
30
30
 
31
- const GIT_REV_LIST_MAX_BUFFER = 8 * 1024 * 1024 // 8MB
31
+ const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB
32
32
 
33
33
  function sanitizedExec (
34
34
  cmd,
@@ -53,11 +53,15 @@ function sanitizedExec (
53
53
  distributionMetric(durationMetric.name, durationMetric.tags, Date.now() - startTime)
54
54
  }
55
55
  return result
56
- } catch (e) {
56
+ } catch (err) {
57
57
  if (errorMetric) {
58
- incrementCountMetric(errorMetric.name, { ...errorMetric.tags, exitCode: e.status })
58
+ incrementCountMetric(errorMetric.name, {
59
+ ...errorMetric.tags,
60
+ errorType: err.code,
61
+ exitCode: err.status || err.errno
62
+ })
59
63
  }
60
- log.error(e)
64
+ log.error(err)
61
65
  return ''
62
66
  } finally {
63
67
  storage.enterWith(store)
@@ -129,7 +133,10 @@ function unshallowRepository () {
129
133
  } catch (err) {
130
134
  // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail.
131
135
  log.error(err)
132
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', exitCode: err.status })
136
+ incrementCountMetric(
137
+ TELEMETRY_GIT_COMMAND_ERRORS,
138
+ { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
139
+ )
133
140
  const upstreamRemote = sanitizedExec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
134
141
  try {
135
142
  cp.execFileSync('git', [
@@ -139,7 +146,10 @@ function unshallowRepository () {
139
146
  } catch (err) {
140
147
  // If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail.
141
148
  log.error(err)
142
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', exitCode: err.status })
149
+ incrementCountMetric(
150
+ TELEMETRY_GIT_COMMAND_ERRORS,
151
+ { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
152
+ )
143
153
  // We use sanitizedExec here because if this last option fails, we'll give up.
144
154
  sanitizedExec(
145
155
  'git',
@@ -175,13 +185,16 @@ function getLatestCommits () {
175
185
  return result
176
186
  } catch (err) {
177
187
  log.error(`Get latest commits failed: ${err.message}`)
178
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_local_commits', errorType: err.status })
188
+ incrementCountMetric(
189
+ TELEMETRY_GIT_COMMAND_ERRORS,
190
+ { command: 'get_local_commits', errorType: err.status }
191
+ )
179
192
  return []
180
193
  }
181
194
  }
182
195
 
183
196
  function getCommitsRevList (commitsToExclude, commitsToInclude) {
184
- let result = []
197
+ let result = null
185
198
 
186
199
  const commitsToExcludeString = commitsToExclude.map(commit => `^${commit}`)
187
200
 
@@ -205,7 +218,10 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) {
205
218
  .filter(commit => commit)
206
219
  } catch (err) {
207
220
  log.error(`Get commits to upload failed: ${err.message}`)
208
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_objects', errorType: err.status })
221
+ incrementCountMetric(
222
+ TELEMETRY_GIT_COMMAND_ERRORS,
223
+ { command: 'get_objects', errorType: err.code, exitCode: err.status || err.errno } // err.status might be null
224
+ )
209
225
  }
210
226
  distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_objects' }, Date.now() - startTime)
211
227
  return result
@@ -245,7 +261,10 @@ function generatePackFilesForCommits (commitsToUpload) {
245
261
  result = execGitPackObjects(temporaryPath)
246
262
  } catch (err) {
247
263
  log.error(err)
248
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', errorType: err.status })
264
+ incrementCountMetric(
265
+ TELEMETRY_GIT_COMMAND_ERRORS,
266
+ { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code }
267
+ )
249
268
  /**
250
269
  * The generation of pack files in the temporary folder (from `os.tmpdir()`)
251
270
  * sometimes fails in certain CI setups with the error message
@@ -262,7 +281,10 @@ function generatePackFilesForCommits (commitsToUpload) {
262
281
  result = execGitPackObjects(cwdPath)
263
282
  } catch (err) {
264
283
  log.error(err)
265
- incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', errorType: err.status })
284
+ incrementCountMetric(
285
+ TELEMETRY_GIT_COMMAND_ERRORS,
286
+ { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code }
287
+ )
266
288
  }
267
289
  }
268
290
  distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'pack_objects' }, Date.now() - startTime)