dd-trace 5.17.0 → 5.18.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.
Files changed (63) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ext/exporters.d.ts +1 -1
  3. package/index.d.ts +47 -1
  4. package/init.js +40 -1
  5. package/initialize.mjs +8 -5
  6. package/package.json +24 -20
  7. package/packages/datadog-core/src/storage/index.js +1 -10
  8. package/packages/datadog-esbuild/index.js +5 -1
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +76 -34
  11. package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
  12. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  13. package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
  14. package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
  15. package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
  16. package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
  17. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
  18. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  19. package/packages/datadog-instrumentations/src/vitest.js +303 -0
  20. package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
  21. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  22. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
  23. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
  24. package/packages/datadog-plugin-child_process/src/index.js +1 -1
  25. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  26. package/packages/datadog-plugin-mocha/src/index.js +25 -4
  27. package/packages/datadog-plugin-openai/src/index.js +52 -30
  28. package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
  29. package/packages/datadog-plugin-vitest/src/index.js +156 -0
  30. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
  31. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
  32. package/packages/dd-trace/src/appsec/index.js +1 -1
  33. package/packages/dd-trace/src/appsec/rasp.js +32 -5
  34. package/packages/dd-trace/src/appsec/recommended.json +208 -3
  35. package/packages/dd-trace/src/appsec/reporter.js +64 -20
  36. package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
  37. package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
  38. package/packages/dd-trace/src/appsec/standalone.js +130 -0
  39. package/packages/dd-trace/src/appsec/telemetry.js +33 -1
  40. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  41. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
  42. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  43. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  44. package/packages/dd-trace/src/config.js +110 -40
  45. package/packages/dd-trace/src/constants.js +3 -1
  46. package/packages/dd-trace/src/datastreams/processor.js +2 -1
  47. package/packages/dd-trace/src/exporters/agent/index.js +2 -2
  48. package/packages/dd-trace/src/format.js +1 -0
  49. package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
  50. package/packages/dd-trace/src/opentracing/span.js +4 -1
  51. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  52. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
  53. package/packages/dd-trace/src/plugins/index.js +2 -0
  54. package/packages/dd-trace/src/plugins/util/test.js +5 -1
  55. package/packages/dd-trace/src/priority_sampler.js +2 -5
  56. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  57. package/packages/dd-trace/src/proxy.js +3 -1
  58. package/packages/dd-trace/src/rate_limiter.js +2 -2
  59. package/packages/dd-trace/src/span_stats.js +4 -3
  60. package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
  61. package/packages/dd-trace/src/tracer.js +2 -2
  62. package/packages/dd-trace/src/util.js +6 -1
  63. package/packages/datadog-core/src/storage/async_hooks.js +0 -49
@@ -7,6 +7,7 @@ const Hook = require('./hook')
7
7
  const requirePackageJson = require('../../../dd-trace/src/require-package-json')
8
8
  const log = require('../../../dd-trace/src/log')
9
9
  const checkRequireCache = require('../check_require_cache')
10
+ const telemetry = require('../../../dd-trace/src/telemetry/init-telemetry')
10
11
 
11
12
  const {
12
13
  DD_TRACE_DISABLED_INSTRUMENTATIONS = '',
@@ -35,22 +36,38 @@ if (DD_TRACE_DEBUG && DD_TRACE_DEBUG.toLowerCase() !== 'false') {
35
36
  setImmediate(checkRequireCache.checkForPotentialConflicts)
36
37
  }
37
38
 
39
+ const seenCombo = new Set()
40
+
38
41
  // TODO: make this more efficient
39
42
  for (const packageName of names) {
40
43
  if (disabledInstrumentations.has(packageName)) continue
41
44
 
42
- Hook([packageName], (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
45
+ const hookOptions = {}
46
+
47
+ let hook = hooks[packageName]
48
+
49
+ if (typeof hook === 'object') {
50
+ hookOptions.internals = hook.esmFirst
51
+ hook = hook.fn
52
+ }
53
+
54
+ Hook([packageName], hookOptions, (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
43
55
  moduleName = moduleName.replace(pathSepExpr, '/')
44
56
 
45
57
  // This executes the integration file thus adding its entries to `instrumentations`
46
- hooks[packageName]()
58
+ hook()
47
59
 
48
60
  if (!instrumentations[packageName]) {
49
61
  return moduleExports
50
62
  }
51
63
 
52
- for (const { name, file, versions, hook } of instrumentations[packageName]) {
64
+ const namesAndSuccesses = {}
65
+ for (const { name, file, versions, hook, filePattern } of instrumentations[packageName]) {
66
+ let fullFilePattern = filePattern
53
67
  const fullFilename = filename(name, file)
68
+ if (fullFilePattern) {
69
+ fullFilePattern = filename(name, fullFilePattern)
70
+ }
54
71
 
55
72
  // Create a WeakMap associated with the hook function so that patches on the same moduleExport only happens once
56
73
  // for example by instrumenting both dns and node:dns double the spans would be created
@@ -58,13 +75,29 @@ for (const packageName of names) {
58
75
  if (!hook[HOOK_SYMBOL]) {
59
76
  hook[HOOK_SYMBOL] = new WeakMap()
60
77
  }
78
+ let matchesFile = false
79
+
80
+ matchesFile = moduleName === fullFilename
61
81
 
62
- if (moduleName === fullFilename) {
82
+ if (fullFilePattern) {
83
+ // Some libraries include a hash in their filenames when installed,
84
+ // so our instrumentation has to include a '.*' to match them for more than a single version.
85
+ matchesFile = matchesFile || new RegExp(fullFilePattern).test(moduleName)
86
+ }
87
+
88
+ if (matchesFile) {
63
89
  const version = moduleVersion || getVersion(moduleBaseDir)
90
+ if (!Object.hasOwnProperty(namesAndSuccesses, name)) {
91
+ namesAndSuccesses[name] = {
92
+ success: false,
93
+ version
94
+ }
95
+ }
64
96
 
65
97
  if (matchVersion(version, versions)) {
66
98
  // Check if the hook already has a set moduleExport
67
99
  if (hook[HOOK_SYMBOL].has(moduleExports)) {
100
+ namesAndSuccesses[name].success = true
68
101
  return moduleExports
69
102
  }
70
103
 
@@ -76,11 +109,29 @@ for (const packageName of names) {
76
109
  // Set the moduleExports in the hooks weakmap
77
110
  hook[HOOK_SYMBOL].set(moduleExports, name)
78
111
  } catch (e) {
79
- log.error(e)
112
+ log.info('Error during ddtrace instrumentation of application, aborting.')
113
+ log.info(e)
114
+ telemetry('error', [
115
+ `error_type:${e.constructor.name}`,
116
+ `integration:${name}`,
117
+ `integration_version:${version}`
118
+ ])
80
119
  }
120
+ namesAndSuccesses[name].success = true
81
121
  }
82
122
  }
83
123
  }
124
+ for (const name of Object.keys(namesAndSuccesses)) {
125
+ const { success, version } = namesAndSuccesses[name]
126
+ if (!success && !seenCombo.has(`${name}@${version}`)) {
127
+ telemetry('abort.integration', [
128
+ `integration:${name}`,
129
+ `integration_version:${version}`
130
+ ])
131
+ log.info(`Found incompatible integration version: ${name}@${version}`)
132
+ seenCombo.add(`${name}@${version}`)
133
+ }
134
+ }
84
135
 
85
136
  return moduleExports
86
137
  })
@@ -21,6 +21,7 @@ const {
21
21
  runnableWrapper,
22
22
  getOnTestHandler,
23
23
  getOnTestEndHandler,
24
+ getOnTestRetryHandler,
24
25
  getOnHookEndHandler,
25
26
  getOnFailHandler,
26
27
  getOnPendingHandler,
@@ -37,10 +38,12 @@ let isSuitesSkipped = false
37
38
  let skippedSuites = []
38
39
  let isEarlyFlakeDetectionEnabled = false
39
40
  let isSuitesSkippingEnabled = false
41
+ let isFlakyTestRetriesEnabled = false
40
42
  let earlyFlakeDetectionNumRetries = 0
41
43
  let knownTests = []
42
44
  let itrCorrelationId = ''
43
45
  let isForcedToRun = false
46
+ const config = {}
44
47
 
45
48
  // We'll preserve the original coverage here
46
49
  const originalCoverageMap = createCoverageMap()
@@ -227,6 +230,12 @@ addHook({
227
230
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
228
231
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
229
232
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
233
+ isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
234
+
235
+ config.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
236
+ config.isSuitesSkippingEnabled = isSuitesSkippingEnabled
237
+ config.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
238
+ config.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
230
239
 
231
240
  if (isEarlyFlakeDetectionEnabled) {
232
241
  knownTestsCh.publish({
@@ -317,6 +326,8 @@ addHook({
317
326
 
318
327
  this.on('test end', getOnTestEndHandler())
319
328
 
329
+ this.on('retry', getOnTestRetryHandler())
330
+
320
331
  // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
321
332
  this.on('hook end', getOnHookEndHandler())
322
333
 
@@ -401,7 +412,7 @@ addHook({
401
412
  name: 'mocha',
402
413
  versions: ['>=5.2.0'],
403
414
  file: 'lib/runnable.js'
404
- }, runnableWrapper)
415
+ }, (runnablePackage) => runnableWrapper(runnablePackage, config))
405
416
 
406
417
  // Only used in parallel mode (--parallel flag is passed)
407
418
  // Used to generate suite events and receive test payloads from workers
@@ -3,7 +3,8 @@
3
3
  const {
4
4
  getTestSuitePath,
5
5
  removeEfdStringFromTestName,
6
- addEfdStringToTestName
6
+ addEfdStringToTestName,
7
+ NUM_FAILED_TEST_RETRIES
7
8
  } = require('../../../dd-trace/src/plugins/util/test')
8
9
  const { channel, AsyncResource } = require('../helpers/instrument')
9
10
  const shimmer = require('../../../datadog-shimmer')
@@ -11,6 +12,8 @@ const shimmer = require('../../../datadog-shimmer')
11
12
  // test channels
12
13
  const testStartCh = channel('ci:mocha:test:start')
13
14
  const testFinishCh = channel('ci:mocha:test:finish')
15
+ // after a test has failed, we'll publish to this channel
16
+ const testRetryCh = channel('ci:mocha:test:retry')
14
17
  const errorCh = channel('ci:mocha:test:error')
15
18
  const skipCh = channel('ci:mocha:test:skip')
16
19
 
@@ -70,6 +73,10 @@ function isMochaRetry (test) {
70
73
  return test._currentRetry !== undefined && test._currentRetry !== 0
71
74
  }
72
75
 
76
+ function isLastRetry (test) {
77
+ return test._currentRetry === test._retries
78
+ }
79
+
73
80
  function getTestFullName (test) {
74
81
  return `mocha.${getTestSuitePath(test.file, process.cwd())}.${removeEfdStringFromTestName(test.fullTitle())}`
75
82
  }
@@ -84,22 +91,34 @@ function getTestStatus (test) {
84
91
  return 'pass'
85
92
  }
86
93
 
87
- function getTestAsyncResource (test) {
94
+ function getTestToArKey (test) {
88
95
  if (!test.fn) {
89
- return testToAr.get(test)
96
+ return test
90
97
  }
91
98
  if (!wrappedFunctions.has(test.fn)) {
92
- return testToAr.get(test.fn)
99
+ return test.fn
93
100
  }
94
101
  const originalFn = originalFns.get(test.fn)
95
- return testToAr.get(originalFn)
102
+ return originalFn
103
+ }
104
+
105
+ function getTestAsyncResource (test) {
106
+ const key = getTestToArKey(test)
107
+ return testToAr.get(key)
96
108
  }
97
109
 
98
- function runnableWrapper (RunnablePackage) {
110
+ function runnableWrapper (RunnablePackage, libraryConfig) {
99
111
  shimmer.wrap(RunnablePackage.prototype, 'run', run => function () {
100
112
  if (!testStartCh.hasSubscribers) {
101
113
  return run.apply(this, arguments)
102
114
  }
115
+ // Flaky test retries does not work in parallel mode
116
+ if (libraryConfig?.isFlakyTestRetriesEnabled) {
117
+ this.retries(NUM_FAILED_TEST_RETRIES)
118
+ }
119
+ // The reason why the wrapping logic is here is because we need to cover
120
+ // `afterEach` and `beforeEach` hooks as well.
121
+ // It can't be done in `getOnTestHandler` because it's only called for tests.
103
122
  const isBeforeEach = this.parent._beforeEach.includes(this)
104
123
  const isAfterEach = this.parent._afterEach.includes(this)
105
124
 
@@ -135,11 +154,16 @@ function runnableWrapper (RunnablePackage) {
135
154
 
136
155
  function getOnTestHandler (isMain, newTests) {
137
156
  return function (test) {
138
- if (isMochaRetry(test)) {
139
- return
140
- }
141
157
  const testStartLine = testToStartLine.get(test)
142
158
  const asyncResource = new AsyncResource('bound-anonymous-fn')
159
+
160
+ // This may be a retry. If this is the case, `test.fn` is already wrapped,
161
+ // so we need to restore it.
162
+ if (wrappedFunctions.has(test.fn)) {
163
+ const originalFn = originalFns.get(test.fn)
164
+ test.fn = originalFn
165
+ wrappedFunctions.delete(test.fn)
166
+ }
143
167
  testToAr.set(test.fn, asyncResource)
144
168
 
145
169
  const {
@@ -186,7 +210,7 @@ function getOnTestEndHandler () {
186
210
  // if there are afterEach to be run, we don't finish the test yet
187
211
  if (asyncResource && !test.parent._afterEach.length) {
188
212
  asyncResource.runInAsyncScope(() => {
189
- testFinishCh.publish(status)
213
+ testFinishCh.publish({ status, hasBeenRetried: isMochaRetry(test) })
190
214
  })
191
215
  }
192
216
  }
@@ -197,12 +221,17 @@ function getOnHookEndHandler () {
197
221
  const test = hook.ctx.currentTest
198
222
  if (test && hook.parent._afterEach.includes(hook)) { // only if it's an afterEach
199
223
  const isLastAfterEach = hook.parent._afterEach.indexOf(hook) === hook.parent._afterEach.length - 1
224
+ if (test._retries > 0 && !isLastRetry(test)) {
225
+ return
226
+ }
200
227
  if (isLastAfterEach) {
201
228
  const status = getTestStatus(test)
202
229
  const asyncResource = getTestAsyncResource(test)
203
- asyncResource.runInAsyncScope(() => {
204
- testFinishCh.publish(status)
205
- })
230
+ if (asyncResource) {
231
+ asyncResource.runInAsyncScope(() => {
232
+ testFinishCh.publish({ status, hasBeenRetried: isMochaRetry(test) })
233
+ })
234
+ }
206
235
  }
207
236
  }
208
237
  }
@@ -226,7 +255,7 @@ function getOnFailHandler (isMain) {
226
255
  err.message = `${testOrHook.fullTitle()}: ${err.message}`
227
256
  errorCh.publish(err)
228
257
  // if it's a hook and it has failed, 'test end' will not be called
229
- testFinishCh.publish('fail')
258
+ testFinishCh.publish({ status: 'fail', hasBeenRetried: isMochaRetry(test) })
230
259
  } else {
231
260
  errorCh.publish(err)
232
261
  }
@@ -250,6 +279,20 @@ function getOnFailHandler (isMain) {
250
279
  }
251
280
  }
252
281
 
282
+ function getOnTestRetryHandler () {
283
+ return function (test) {
284
+ const asyncResource = getTestAsyncResource(test)
285
+ if (asyncResource) {
286
+ const isFirstAttempt = test._currentRetry === 0
287
+ asyncResource.runInAsyncScope(() => {
288
+ testRetryCh.publish(isFirstAttempt)
289
+ })
290
+ }
291
+ const key = getTestToArKey(test)
292
+ testToAr.delete(key)
293
+ }
294
+ }
295
+
253
296
  function getOnPendingHandler () {
254
297
  return function (test) {
255
298
  const testStartLine = testToStartLine.get(test)
@@ -299,6 +342,7 @@ module.exports = {
299
342
  testToStartLine,
300
343
  getOnTestHandler,
301
344
  getOnTestEndHandler,
345
+ getOnTestRetryHandler,
302
346
  getOnHookEndHandler,
303
347
  getOnFailHandler,
304
348
  getOnPendingHandler,
@@ -49,3 +49,4 @@ addHook({
49
49
  versions: ['>=5.2.0'],
50
50
  file: 'lib/runnable.js'
51
51
  }, runnableWrapper)
52
+ // TODO: parallel mode does not support flaky test retries, so no library config is passed.
@@ -249,7 +249,7 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
249
249
  testAsyncResource.runInAsyncScope(() => {
250
250
  testFinishCh.publish({
251
251
  testStatus,
252
- steps: testResult.steps,
252
+ steps: testResult?.steps || [],
253
253
  error,
254
254
  extraTags: annotationTags,
255
255
  isNew: test._ddIsNew,
@@ -0,0 +1,303 @@
1
+ const { addHook, channel, AsyncResource } = require('./helpers/instrument')
2
+ const shimmer = require('../../datadog-shimmer')
3
+
4
+ // test hooks
5
+ const testStartCh = channel('ci:vitest:test:start')
6
+ const testFinishTimeCh = channel('ci:vitest:test:finish-time')
7
+ const testPassCh = channel('ci:vitest:test:pass')
8
+ const testErrorCh = channel('ci:vitest:test:error')
9
+ const testSkipCh = channel('ci:vitest:test:skip')
10
+
11
+ // test suite hooks
12
+ const testSuiteStartCh = channel('ci:vitest:test-suite:start')
13
+ const testSuiteFinishCh = channel('ci:vitest:test-suite:finish')
14
+ const testSuiteErrorCh = channel('ci:vitest:test-suite:error')
15
+
16
+ // test session hooks
17
+ const testSessionStartCh = channel('ci:vitest:session:start')
18
+ const testSessionFinishCh = channel('ci:vitest:session:finish')
19
+
20
+ const taskToAsync = new WeakMap()
21
+
22
+ const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
23
+
24
+ function isReporterPackage (vitestPackage) {
25
+ return vitestPackage.B?.name === 'BaseSequencer'
26
+ }
27
+
28
+ // from 2.0.0
29
+ function isReporterPackageNew (vitestPackage) {
30
+ return vitestPackage.e?.name === 'BaseSequencer'
31
+ }
32
+
33
+ function getSessionStatus (state) {
34
+ if (state.getCountOfFailedTests() > 0) {
35
+ return 'fail'
36
+ }
37
+ if (state.pathsSet.size === 0) {
38
+ return 'skip'
39
+ }
40
+ return 'pass'
41
+ }
42
+
43
+ // eslint-disable-next-line
44
+ // From https://github.com/vitest-dev/vitest/blob/51c04e2f44d91322b334f8ccbcdb368facc3f8ec/packages/runner/src/run.ts#L243-L250
45
+ function getVitestTestStatus (test, retryCount) {
46
+ if (test.result.state !== 'fail') {
47
+ if (!test.repeats) {
48
+ return 'pass'
49
+ } else if (test.repeats && (test.retry ?? 0) === retryCount) {
50
+ return 'pass'
51
+ }
52
+ }
53
+ return 'fail'
54
+ }
55
+
56
+ function getTypeTasks (fileTasks, type = 'test') {
57
+ const typeTasks = []
58
+
59
+ function getTasks (tasks) {
60
+ for (const task of tasks) {
61
+ if (task.type === type) {
62
+ typeTasks.push(task)
63
+ } else if (task.tasks) {
64
+ getTasks(task.tasks)
65
+ }
66
+ }
67
+ }
68
+
69
+ getTasks(fileTasks)
70
+
71
+ return typeTasks
72
+ }
73
+
74
+ function getTestName (task) {
75
+ let testName = task.name
76
+ let currentTask = task.suite
77
+
78
+ while (currentTask) {
79
+ if (currentTask.name) {
80
+ testName = `${currentTask.name} ${testName}`
81
+ }
82
+ currentTask = currentTask.suite
83
+ }
84
+
85
+ return testName
86
+ }
87
+
88
+ function getSortWrapper (sort) {
89
+ return async function () {
90
+ if (!testSessionFinishCh.hasSubscribers) {
91
+ return sort.apply(this, arguments)
92
+ }
93
+ shimmer.wrap(this.ctx, 'exit', exit => async function () {
94
+ let onFinish
95
+
96
+ const flushPromise = new Promise(resolve => {
97
+ onFinish = resolve
98
+ })
99
+ const failedSuites = this.state.getFailedFilepaths()
100
+ let error
101
+ if (failedSuites.length) {
102
+ error = new Error(`Test suites failed: ${failedSuites.length}.`)
103
+ }
104
+
105
+ sessionAsyncResource.runInAsyncScope(() => {
106
+ testSessionFinishCh.publish({
107
+ status: getSessionStatus(this.state),
108
+ onFinish,
109
+ error
110
+ })
111
+ })
112
+
113
+ await flushPromise
114
+
115
+ return exit.apply(this, arguments)
116
+ })
117
+
118
+ return sort.apply(this, arguments)
119
+ }
120
+ }
121
+
122
+ addHook({
123
+ name: 'vitest',
124
+ versions: ['>=1.6.0'],
125
+ file: 'dist/runners.js'
126
+ }, (vitestPackage) => {
127
+ const { VitestTestRunner } = vitestPackage
128
+ // test start (only tests that are not marked as skip or todo)
129
+ shimmer.wrap(VitestTestRunner.prototype, 'onBeforeTryTask', onBeforeTryTask => async function (task) {
130
+ if (!testStartCh.hasSubscribers) {
131
+ return onBeforeTryTask.apply(this, arguments)
132
+ }
133
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
134
+ taskToAsync.set(task, asyncResource)
135
+
136
+ asyncResource.runInAsyncScope(() => {
137
+ testStartCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
138
+ })
139
+ return onBeforeTryTask.apply(this, arguments)
140
+ })
141
+
142
+ // test finish (only passed tests)
143
+ shimmer.wrap(VitestTestRunner.prototype, 'onAfterTryTask', onAfterTryTask =>
144
+ async function (task, { retry: retryCount }) {
145
+ if (!testFinishTimeCh.hasSubscribers) {
146
+ return onAfterTryTask.apply(this, arguments)
147
+ }
148
+ const result = await onAfterTryTask.apply(this, arguments)
149
+
150
+ const status = getVitestTestStatus(task, retryCount)
151
+ const asyncResource = taskToAsync.get(task)
152
+
153
+ if (asyncResource) {
154
+ // We don't finish here because the test might fail in a later hook
155
+ asyncResource.runInAsyncScope(() => {
156
+ testFinishTimeCh.publish({ status, task })
157
+ })
158
+ }
159
+
160
+ return result
161
+ })
162
+
163
+ return vitestPackage
164
+ })
165
+
166
+ addHook({
167
+ name: 'vitest',
168
+ versions: ['>=2.0.0'],
169
+ filePattern: 'dist/vendor/index.*'
170
+ }, (vitestPackage) => {
171
+ // there are multiple index* files so we have to check the exported values
172
+ if (isReporterPackageNew(vitestPackage)) {
173
+ shimmer.wrap(vitestPackage.e.prototype, 'sort', getSortWrapper)
174
+ }
175
+
176
+ return vitestPackage
177
+ })
178
+
179
+ addHook({
180
+ name: 'vitest',
181
+ versions: ['>=1.6.0'],
182
+ filePattern: 'dist/vendor/index.*'
183
+ }, (vitestPackage) => {
184
+ // there are multiple index* files so we have to check the exported values
185
+ if (isReporterPackage(vitestPackage)) {
186
+ shimmer.wrap(vitestPackage.B.prototype, 'sort', getSortWrapper)
187
+ }
188
+
189
+ return vitestPackage
190
+ })
191
+
192
+ // Can't specify file because compiled vitest includes hashes in their files
193
+ addHook({
194
+ name: 'vitest',
195
+ versions: ['>=1.6.0'],
196
+ filePattern: 'dist/vendor/cac.*'
197
+ }, (vitestPackage, frameworkVersion) => {
198
+ shimmer.wrap(vitestPackage, 'c', oldCreateCli => function () {
199
+ if (!testSessionStartCh.hasSubscribers) {
200
+ return oldCreateCli.apply(this, arguments)
201
+ }
202
+ sessionAsyncResource.runInAsyncScope(() => {
203
+ const processArgv = process.argv.slice(2).join(' ')
204
+ testSessionStartCh.publish({ command: `vitest ${processArgv}`, frameworkVersion })
205
+ })
206
+ return oldCreateCli.apply(this, arguments)
207
+ })
208
+
209
+ return vitestPackage
210
+ })
211
+
212
+ // test suite start and finish
213
+ // only relevant for workers
214
+ addHook({
215
+ name: '@vitest/runner',
216
+ versions: ['>=1.6.0'],
217
+ file: 'dist/index.js'
218
+ }, vitestPackage => {
219
+ shimmer.wrap(vitestPackage, 'startTests', startTests => async function (testPath) {
220
+ let testSuiteError = null
221
+ if (!testSuiteStartCh.hasSubscribers) {
222
+ return startTests.apply(this, arguments)
223
+ }
224
+
225
+ const testSuiteAsyncResource = new AsyncResource('bound-anonymous-fn')
226
+ testSuiteAsyncResource.runInAsyncScope(() => {
227
+ testSuiteStartCh.publish(testPath[0])
228
+ })
229
+ const startTestsResponse = await startTests.apply(this, arguments)
230
+
231
+ let onFinish = null
232
+ const onFinishPromise = new Promise(resolve => {
233
+ onFinish = resolve
234
+ })
235
+
236
+ const testTasks = getTypeTasks(startTestsResponse[0].tasks)
237
+
238
+ testTasks.forEach(task => {
239
+ const testAsyncResource = taskToAsync.get(task)
240
+ const { result } = task
241
+
242
+ if (result) {
243
+ const { state, duration, errors } = result
244
+ if (state === 'skip') { // programmatic skip
245
+ testSkipCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
246
+ } else if (state === 'pass') {
247
+ if (testAsyncResource) {
248
+ testAsyncResource.runInAsyncScope(() => {
249
+ testPassCh.publish({ task })
250
+ })
251
+ }
252
+ } else if (state === 'fail') {
253
+ // If it's failing, we have no accurate finish time, so we have to use `duration`
254
+ let testError
255
+
256
+ if (errors?.length) {
257
+ testError = errors[0]
258
+ }
259
+
260
+ if (testAsyncResource) {
261
+ testAsyncResource.runInAsyncScope(() => {
262
+ testErrorCh.publish({ duration, error: testError })
263
+ })
264
+ }
265
+ if (errors?.length) {
266
+ testSuiteError = testError // we store the error to bubble it up to the suite
267
+ }
268
+ }
269
+ } else { // test.skip or test.todo
270
+ testSkipCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
271
+ }
272
+ })
273
+
274
+ const testSuiteResult = startTestsResponse[0].result
275
+
276
+ if (testSuiteResult.errors?.length) { // Errors from root level hooks
277
+ testSuiteError = testSuiteResult.errors[0]
278
+ } else if (testSuiteResult.state === 'fail') { // Errors from `describe` level hooks
279
+ const suiteTasks = getTypeTasks(startTestsResponse[0].tasks, 'suite')
280
+ const failedSuites = suiteTasks.filter(task => task.result?.state === 'fail')
281
+ if (failedSuites.length && failedSuites[0].result?.errors?.length) {
282
+ testSuiteError = failedSuites[0].result.errors[0]
283
+ }
284
+ }
285
+
286
+ if (testSuiteError) {
287
+ testSuiteAsyncResource.runInAsyncScope(() => {
288
+ testSuiteErrorCh.publish({ error: testSuiteError })
289
+ })
290
+ }
291
+
292
+ testSuiteAsyncResource.runInAsyncScope(() => {
293
+ testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish })
294
+ })
295
+
296
+ // TODO: fix too frequent flushes
297
+ await onFinishPromise
298
+
299
+ return startTestsResponse
300
+ })
301
+
302
+ return vitestPackage
303
+ })
@@ -64,11 +64,17 @@ class BaseAwsSdkPlugin extends ClientPlugin {
64
64
  span.setTag('region', region)
65
65
  })
66
66
 
67
- this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ({ response }) => {
67
+ this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ({ response, cbExists = false }) => {
68
68
  const store = storage.getStore()
69
69
  if (!store) return
70
70
  const { span } = store
71
71
  if (!span) return
72
+ // try to extract DSM context from response if no callback exists as extraction normally happens in CB
73
+ if (!cbExists && this.serviceIdentifier === 'sqs') {
74
+ const params = response.request.params
75
+ const operation = response.request.operation
76
+ this.responseExtractDSMContext(operation, params, response.data, span)
77
+ }
72
78
  this.addResponseTags(span, response)
73
79
  this.finish(span, response, response.error)
74
80
  })
@@ -159,6 +165,7 @@ function normalizeConfig (config, serviceIdentifier) {
159
165
 
160
166
  return Object.assign({}, config, specificConfig, {
161
167
  splitByAwsService: config.splitByAwsService !== false,
168
+ batchPropagationEnabled: config.batchPropagationEnabled !== false,
162
169
  hooks
163
170
  })
164
171
  }
@@ -52,7 +52,7 @@ class Kinesis extends BaseAwsSdkPlugin {
52
52
 
53
53
  // extract DSM context after as we might not have a parent-child but may have a DSM context
54
54
  this.responseExtractDSMContext(
55
- request.operation, response, span || null, streamName
55
+ request.operation, request.params, response, span || null, { streamName }
56
56
  )
57
57
  }
58
58
  })
@@ -100,7 +100,8 @@ class Kinesis extends BaseAwsSdkPlugin {
100
100
  }
101
101
  }
102
102
 
103
- responseExtractDSMContext (operation, response, span, streamName) {
103
+ responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
104
+ const { streamName } = kwargs
104
105
  if (!this.config.dsmEnabled) return
105
106
  if (operation !== 'getRecords') return
106
107
  if (!response || !response.Records || !response.Records[0]) return
@@ -151,7 +152,12 @@ class Kinesis extends BaseAwsSdkPlugin {
151
152
  case 'putRecords':
152
153
  stream = params.StreamArn ? params.StreamArn : (params.StreamName ? params.StreamName : '')
153
154
  for (let i = 0; i < params.Records.length; i++) {
154
- this.injectToMessage(span, params.Records[i], stream, i === 0)
155
+ this.injectToMessage(
156
+ span,
157
+ params.Records[i],
158
+ stream,
159
+ i === 0 || (this.config.kinesis && this.config.kinesis.batchPropagationEnabled)
160
+ )
155
161
  }
156
162
  }
157
163
  }