dd-trace 5.2.0 → 5.3.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 (68) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +1 -32
  3. package/ci/init.js +1 -4
  4. package/index.d.ts +21 -0
  5. package/package.json +6 -5
  6. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  7. package/packages/datadog-instrumentations/src/child_process.js +150 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +12 -12
  9. package/packages/datadog-instrumentations/src/express.js +20 -0
  10. package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -2
  12. package/packages/datadog-instrumentations/src/jest.js +147 -10
  13. package/packages/datadog-instrumentations/src/mocha.js +3 -3
  14. package/packages/datadog-instrumentations/src/mongoose.js +23 -10
  15. package/packages/datadog-instrumentations/src/next.js +17 -3
  16. package/packages/datadog-instrumentations/src/playwright.js +41 -9
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +10 -1
  18. package/packages/datadog-plugin-amqplib/src/producer.js +14 -1
  19. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +107 -1
  20. package/packages/datadog-plugin-child_process/src/index.js +91 -0
  21. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +125 -0
  22. package/packages/datadog-plugin-cucumber/src/index.js +16 -11
  23. package/packages/datadog-plugin-cypress/src/plugin.js +25 -12
  24. package/packages/datadog-plugin-grpc/src/client.js +16 -2
  25. package/packages/datadog-plugin-http/src/client.js +1 -1
  26. package/packages/datadog-plugin-jest/src/index.js +47 -6
  27. package/packages/datadog-plugin-mocha/src/index.js +14 -5
  28. package/packages/datadog-plugin-playwright/src/index.js +19 -5
  29. package/packages/datadog-plugin-rhea/src/consumer.js +11 -1
  30. package/packages/datadog-plugin-rhea/src/producer.js +11 -0
  31. package/packages/dd-trace/src/appsec/addresses.js +2 -0
  32. package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
  33. package/packages/dd-trace/src/appsec/channels.js +2 -1
  34. package/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js +1 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +7 -28
  36. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +10 -6
  37. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -1
  38. package/packages/dd-trace/src/appsec/index.js +17 -2
  39. package/packages/dd-trace/src/appsec/rule_manager.js +2 -2
  40. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
  41. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +25 -6
  42. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +2 -0
  43. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +83 -41
  44. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +30 -8
  45. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -1
  46. package/packages/dd-trace/src/ci-visibility/{intelligent-test-runner/get-itr-configuration.js → requests/get-library-configuration.js} +18 -6
  47. package/packages/dd-trace/src/config.js +22 -9
  48. package/packages/dd-trace/src/datastreams/processor.js +6 -0
  49. package/packages/dd-trace/src/datastreams/writer.js +2 -5
  50. package/packages/dd-trace/src/dogstatsd.js +3 -5
  51. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
  52. package/packages/dd-trace/src/exporters/common/request.js +21 -3
  53. package/packages/dd-trace/src/format.js +25 -1
  54. package/packages/dd-trace/src/noop/span.js +1 -0
  55. package/packages/dd-trace/src/opentelemetry/span.js +9 -2
  56. package/packages/dd-trace/src/opentracing/span.js +38 -0
  57. package/packages/dd-trace/src/opentracing/span_context.js +12 -6
  58. package/packages/dd-trace/src/opentracing/tracer.js +2 -1
  59. package/packages/dd-trace/src/plugins/ci_plugin.js +24 -8
  60. package/packages/dd-trace/src/plugins/index.js +1 -0
  61. package/packages/dd-trace/src/plugins/util/git.js +6 -0
  62. package/packages/dd-trace/src/plugins/util/test.js +36 -7
  63. package/packages/dd-trace/src/profiling/config.js +22 -22
  64. package/packages/dd-trace/src/proxy.js +31 -23
  65. package/packages/dd-trace/src/span_processor.js +5 -1
  66. package/packages/dd-trace/src/telemetry/index.js +3 -0
  67. package/packages/datadog-instrumentations/src/child-process.js +0 -29
  68. package/packages/dd-trace/src/plugins/util/exec.js +0 -34
@@ -37,11 +37,16 @@ const testRunFinishCh = channel('ci:jest:test:finish')
37
37
  const testErrCh = channel('ci:jest:test:err')
38
38
 
39
39
  const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
40
- const jestItrConfigurationCh = channel('ci:jest:itr-configuration')
40
+ const libraryConfigurationCh = channel('ci:jest:library-configuration')
41
+ const knownTestsCh = channel('ci:jest:known-tests')
41
42
 
42
43
  const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
43
44
 
45
+ // Maximum time we'll wait for the tracer to flush
46
+ const FLUSH_TIMEOUT = 10000
47
+
44
48
  let skippableSuites = []
49
+ let knownTests = []
45
50
  let isCodeCoverageEnabled = false
46
51
  let isSuitesSkippingEnabled = false
47
52
  let isUserCodeCoverageEnabled = false
@@ -49,6 +54,11 @@ let isSuitesSkipped = false
49
54
  let numSkippedSuites = 0
50
55
  let hasUnskippableSuites = false
51
56
  let hasForcedToRunSuites = false
57
+ let isEarlyFlakeDetectionEnabled = false
58
+ let earlyFlakeDetectionNumRetries = 0
59
+
60
+ const EFD_STRING = "Retried by Datadog's Early Flake Detection"
61
+ const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g')
52
62
 
53
63
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
54
64
 
@@ -62,6 +72,7 @@ const specStatusToTestStatus = {
62
72
 
63
73
  const asyncResources = new WeakMap()
64
74
  const originalTestFns = new WeakMap()
75
+ const retriedTestsToNumAttempts = new Map()
65
76
 
66
77
  // based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
67
78
  function formatJestError (errors) {
@@ -90,6 +101,14 @@ function getTestEnvironmentOptions (config) {
90
101
  return {}
91
102
  }
92
103
 
104
+ function getEfdTestName (testName, numAttempt) {
105
+ return `${EFD_STRING} (#${numAttempt}): ${testName}`
106
+ }
107
+
108
+ function removeEfdTestName (testName) {
109
+ return testName.replace(EFD_TEST_NAME_REGEX, '')
110
+ }
111
+
93
112
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
94
113
  return class DatadogEnvironment extends BaseEnvironment {
95
114
  constructor (config, context) {
@@ -97,10 +116,43 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
97
116
  const rootDir = config.globalConfig ? config.globalConfig.rootDir : config.rootDir
98
117
  this.rootDir = rootDir
99
118
  this.testSuite = getTestSuitePath(context.testPath, rootDir)
119
+ this.testFileAbsolutePath = context.testPath
100
120
  this.nameToParams = {}
101
121
  this.global._ddtrace = global._ddtrace
102
122
 
103
123
  this.testEnvironmentOptions = getTestEnvironmentOptions(config)
124
+
125
+ this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
126
+
127
+ if (this.isEarlyFlakeDetectionEnabled) {
128
+ earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
129
+ try {
130
+ this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
131
+ } catch (e) {
132
+ // If there has been an error parsing the tests, we'll disable Early Flake Deteciton
133
+ this.isEarlyFlakeDetectionEnabled = false
134
+ }
135
+ }
136
+ }
137
+
138
+ // Function that receives a list of known tests for a test service and
139
+ // returns the ones that belong to the current suite
140
+ getKnownTestsForSuite (knownTests) {
141
+ let knownTestsForSuite = knownTests
142
+ // If jest runs in band, the known tests are not serialized, so they're an array.
143
+ if (!Array.isArray(knownTests)) {
144
+ knownTestsForSuite = JSON.parse(knownTestsForSuite)
145
+ }
146
+ return knownTestsForSuite
147
+ .filter(test => test.includes(this.testSuite))
148
+ .map(test => test.replace(`jest.${this.testSuite}.`, '').trim())
149
+ }
150
+
151
+ // Add the `add_test` event we don't have the test object yet, so
152
+ // we use its describe block to get the full name
153
+ getTestNameFromAddTestEvent (event, state) {
154
+ const describeSuffix = getJestTestName(state.currentDescribeBlock)
155
+ return removeEfdTestName(`${describeSuffix} ${event.testName}`).trim()
104
156
  }
105
157
 
106
158
  async handleTestEvent (event, state) {
@@ -124,23 +176,56 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
124
176
  }
125
177
  }
126
178
  if (event.name === 'test_start') {
179
+ let isNewTest = false
180
+ let numEfdRetry = null
127
181
  const testParameters = getTestParametersString(this.nameToParams, event.test.name)
128
182
  // Async resource for this test is created here
129
183
  // It is used later on by the test_done handler
130
184
  const asyncResource = new AsyncResource('bound-anonymous-fn')
131
185
  asyncResources.set(event.test, asyncResource)
186
+ const testName = getJestTestName(event.test)
187
+
188
+ if (this.isEarlyFlakeDetectionEnabled) {
189
+ const originalTestName = removeEfdTestName(testName)
190
+ isNewTest = retriedTestsToNumAttempts.has(originalTestName)
191
+ if (isNewTest) {
192
+ numEfdRetry = retriedTestsToNumAttempts.get(originalTestName)
193
+ retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1)
194
+ }
195
+ }
196
+
132
197
  asyncResource.runInAsyncScope(() => {
133
198
  testStartCh.publish({
134
- name: getJestTestName(event.test),
199
+ name: removeEfdTestName(testName),
135
200
  suite: this.testSuite,
201
+ testFileAbsolutePath: this.testFileAbsolutePath,
136
202
  runner: 'jest-circus',
137
203
  testParameters,
138
- frameworkVersion: jestVersion
204
+ frameworkVersion: jestVersion,
205
+ isNew: isNewTest,
206
+ isEfdRetry: numEfdRetry > 0
139
207
  })
140
208
  originalTestFns.set(event.test, event.test.fn)
141
209
  event.test.fn = asyncResource.bind(event.test.fn)
142
210
  })
143
211
  }
212
+ if (event.name === 'add_test') {
213
+ if (this.isEarlyFlakeDetectionEnabled) {
214
+ const testName = this.getTestNameFromAddTestEvent(event, state)
215
+ const isNew = !this.knownTestsForThisSuite?.includes(testName)
216
+ const isSkipped = event.mode === 'todo' || event.mode === 'skip'
217
+ if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testName)) {
218
+ retriedTestsToNumAttempts.set(testName, 0)
219
+ for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
220
+ if (this.global.test) {
221
+ this.global.test(getEfdTestName(event.testName, retryIndex), event.fn, event.timeout)
222
+ } else {
223
+ log.error('Early flake detection could not retry test because global.test is undefined')
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
144
229
  if (event.name === 'test_done') {
145
230
  const asyncResource = asyncResources.get(event.test)
146
231
  asyncResource.runInAsyncScope(() => {
@@ -164,6 +249,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
164
249
  testSkippedCh.publish({
165
250
  name: getJestTestName(event.test),
166
251
  suite: this.testSuite,
252
+ testFileAbsolutePath: this.testFileAbsolutePath,
167
253
  runner: 'jest-circus',
168
254
  frameworkVersion: jestVersion,
169
255
  testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
@@ -206,7 +292,7 @@ addHook({
206
292
  }
207
293
  // TODO: could we get the rootDir from each test?
208
294
  const [test] = shardedTests
209
- const rootDir = test && test.context && test.context.config && test.context.config.rootDir
295
+ const rootDir = test?.context?.config?.rootDir
210
296
 
211
297
  const jestSuitesToRun = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
212
298
 
@@ -234,24 +320,45 @@ function cliWrapper (cli, jestVersion) {
234
320
  const configurationPromise = new Promise((resolve) => {
235
321
  onDone = resolve
236
322
  })
237
- if (!jestItrConfigurationCh.hasSubscribers) {
323
+ if (!libraryConfigurationCh.hasSubscribers) {
238
324
  return runCLI.apply(this, arguments)
239
325
  }
240
326
 
241
327
  sessionAsyncResource.runInAsyncScope(() => {
242
- jestItrConfigurationCh.publish({ onDone })
328
+ libraryConfigurationCh.publish({ onDone })
243
329
  })
244
330
 
245
331
  try {
246
- const { err, itrConfig } = await configurationPromise
332
+ const { err, libraryConfig } = await configurationPromise
247
333
  if (!err) {
248
- isCodeCoverageEnabled = itrConfig.isCodeCoverageEnabled
249
- isSuitesSkippingEnabled = itrConfig.isSuitesSkippingEnabled
334
+ isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
335
+ isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
336
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
337
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
250
338
  }
251
339
  } catch (err) {
252
340
  log.error(err)
253
341
  }
254
342
 
343
+ if (isEarlyFlakeDetectionEnabled) {
344
+ const knownTestsPromise = new Promise((resolve) => {
345
+ onDone = resolve
346
+ })
347
+
348
+ sessionAsyncResource.runInAsyncScope(() => {
349
+ knownTestsCh.publish({ onDone })
350
+ })
351
+
352
+ try {
353
+ const { err, knownTests: receivedKnownTests } = await knownTestsPromise
354
+ if (!err) {
355
+ knownTests = receivedKnownTests
356
+ }
357
+ } catch (err) {
358
+ log.error(err)
359
+ }
360
+ }
361
+
255
362
  if (isSuitesSkippingEnabled) {
256
363
  const skippableSuitesPromise = new Promise((resolve) => {
257
364
  onDone = resolve
@@ -311,6 +418,21 @@ function cliWrapper (cli, jestVersion) {
311
418
  status = 'fail'
312
419
  error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
313
420
  }
421
+ let timeoutId
422
+
423
+ // Pass the resolve callback to defer it to DC listener
424
+ const flushPromise = new Promise((resolve) => {
425
+ onDone = () => {
426
+ clearTimeout(timeoutId)
427
+ resolve()
428
+ }
429
+ })
430
+
431
+ const timeoutPromise = new Promise((resolve) => {
432
+ timeoutId = setTimeout(() => {
433
+ resolve('timeout')
434
+ }, FLUSH_TIMEOUT).unref()
435
+ })
314
436
 
315
437
  sessionAsyncResource.runInAsyncScope(() => {
316
438
  testSessionFinishCh.publish({
@@ -322,9 +444,16 @@ function cliWrapper (cli, jestVersion) {
322
444
  numSkippedSuites,
323
445
  hasUnskippableSuites,
324
446
  hasForcedToRunSuites,
325
- error
447
+ error,
448
+ isEarlyFlakeDetectionEnabled,
449
+ onDone
326
450
  })
327
451
  })
452
+ const waitingResult = await Promise.race([flushPromise, timeoutPromise])
453
+
454
+ if (waitingResult === 'timeout') {
455
+ log.error('Timeout waiting for the tracer to flush')
456
+ }
328
457
 
329
458
  numSkippedSuites = 0
330
459
 
@@ -438,10 +567,15 @@ function configureTestEnvironment (readConfigsResult) {
438
567
  // because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions`
439
568
  configs.forEach(config => {
440
569
  config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled
570
+ config.testEnvironmentOptions._ddKnownTests = knownTests
441
571
  })
442
572
 
443
573
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
444
574
 
575
+ if (readConfigsResult.globalConfig.forceExit) {
576
+ log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
577
+ }
578
+
445
579
  if (isCodeCoverageEnabled) {
446
580
  const globalConfig = {
447
581
  ...readConfigsResult.globalConfig,
@@ -498,6 +632,9 @@ addHook({
498
632
  _ddForcedToRun,
499
633
  _ddUnskippable,
500
634
  _ddItrCorrelationId,
635
+ _ddKnownTests,
636
+ _ddIsEarlyFlakeDetectionEnabled,
637
+ _ddEarlyFlakeDetectionNumRetries,
501
638
  ...restOfTestEnvironmentOptions
502
639
  } = testEnvironmentOptions
503
640
 
@@ -20,7 +20,7 @@ const skipCh = channel('ci:mocha:test:skip')
20
20
  const testFinishCh = channel('ci:mocha:test:finish')
21
21
  const parameterizedTestCh = channel('ci:mocha:test:parameterize')
22
22
 
23
- const itrConfigurationCh = channel('ci:mocha:itr-configuration')
23
+ const libraryConfigurationCh = channel('ci:mocha:library-configuration')
24
24
  const skippableSuitesCh = channel('ci:mocha:test-suite:skippable')
25
25
 
26
26
  const testSessionStartCh = channel('ci:mocha:session:start')
@@ -384,7 +384,7 @@ addHook({
384
384
  return run.apply(this, arguments)
385
385
  }
386
386
 
387
- if (!itrConfigurationCh.hasSubscribers || this.isWorker) {
387
+ if (!libraryConfigurationCh.hasSubscribers || this.isWorker) {
388
388
  if (this.isWorker) {
389
389
  isWorker = true
390
390
  }
@@ -439,7 +439,7 @@ addHook({
439
439
  }
440
440
 
441
441
  mochaRunAsyncResource.runInAsyncScope(() => {
442
- itrConfigurationCh.publish({
442
+ libraryConfigurationCh.publish({
443
443
  onDone: mochaRunAsyncResource.bind(onReceivedConfiguration)
444
444
  })
445
445
  })
@@ -79,21 +79,26 @@ addHook({
79
79
  })
80
80
 
81
81
  let callbackWrapped = false
82
- const lastArgumentIndex = arguments.length - 1
83
82
 
84
- if (typeof arguments[lastArgumentIndex] === 'function') {
85
- // is a callback, wrap it to execute finish()
86
- shimmer.wrap(arguments, lastArgumentIndex, originalCb => {
87
- return function () {
88
- finish()
83
+ const wrapCallbackIfExist = (args) => {
84
+ const lastArgumentIndex = args.length - 1
89
85
 
90
- return originalCb.apply(this, arguments)
91
- }
92
- })
86
+ if (typeof args[lastArgumentIndex] === 'function') {
87
+ // is a callback, wrap it to execute finish()
88
+ shimmer.wrap(args, lastArgumentIndex, originalCb => {
89
+ return function () {
90
+ finish()
91
+
92
+ return originalCb.apply(this, arguments)
93
+ }
94
+ })
93
95
 
94
- callbackWrapped = true
96
+ callbackWrapped = true
97
+ }
95
98
  }
96
99
 
100
+ wrapCallbackIfExist(arguments)
101
+
97
102
  return asyncResource.runInAsyncScope(() => {
98
103
  startCh.publish({
99
104
  filters,
@@ -106,8 +111,16 @@ addHook({
106
111
  if (!callbackWrapped) {
107
112
  shimmer.wrap(res, 'exec', originalExec => {
108
113
  return function wrappedExec () {
114
+ if (!callbackWrapped) {
115
+ wrapCallbackIfExist(arguments)
116
+ }
117
+
109
118
  const execResult = originalExec.apply(this, arguments)
110
119
 
120
+ if (callbackWrapped || typeof execResult?.then !== 'function') {
121
+ return execResult
122
+ }
123
+
111
124
  // wrap them method, wrap resolve and reject methods
112
125
  shimmer.wrap(execResult, 'then', originalThen => {
113
126
  return function wrappedThen () {
@@ -290,9 +290,23 @@ addHook({
290
290
  shimmer.massWrap(request.NextRequest.prototype, ['text', 'json'], function (originalMethod) {
291
291
  return async function wrappedJson () {
292
292
  const body = await originalMethod.apply(this, arguments)
293
- bodyParsedChannel.publish({
294
- body
295
- })
293
+
294
+ bodyParsedChannel.publish({ body })
295
+
296
+ return body
297
+ }
298
+ })
299
+
300
+ shimmer.wrap(request.NextRequest.prototype, 'formData', function (originalFormData) {
301
+ return async function wrappedFormData () {
302
+ const body = await originalFormData.apply(this, arguments)
303
+
304
+ let normalizedBody = body
305
+ if (typeof body.entries === 'function') {
306
+ normalizedBody = Object.fromEntries(body.entries())
307
+ }
308
+ bodyParsedChannel.publish({ body: normalizedBody })
309
+
296
310
  return body
297
311
  }
298
312
  })
@@ -73,14 +73,41 @@ function getRootDir (playwrightRunner) {
73
73
  if (playwrightRunner._configDir) {
74
74
  return playwrightRunner._configDir
75
75
  }
76
- if (playwrightRunner._config && playwrightRunner._config.config) {
77
- return playwrightRunner._config.config.rootDir
76
+ if (playwrightRunner._config) {
77
+ return playwrightRunner._config.config?.rootDir || process.cwd()
78
78
  }
79
79
  return process.cwd()
80
80
  }
81
81
 
82
- function testBeginHandler (test) {
83
- const { _requireFile: testSuiteAbsolutePath, title: testName, _type, location: { line: testSourceLine } } = test
82
+ function getProjectsFromRunner (runner) {
83
+ const config = getPlaywrightConfig(runner)
84
+ return config.projects?.map(({ project }) => project)
85
+ }
86
+
87
+ function getProjectsFromDispatcher (dispatcher) {
88
+ const newConfig = dispatcher._config?.config?.projects
89
+ if (newConfig) {
90
+ return newConfig
91
+ }
92
+ // old
93
+ return dispatcher._loader?.fullConfig()?.projects
94
+ }
95
+
96
+ function getBrowserNameFromProjects (projects, projectId) {
97
+ if (!projects) {
98
+ return null
99
+ }
100
+ return projects.find(project =>
101
+ project.__projectId === projectId || project._id === projectId
102
+ )?.name
103
+ }
104
+
105
+ function testBeginHandler (test, browserName) {
106
+ const {
107
+ _requireFile: testSuiteAbsolutePath,
108
+ title: testName, _type,
109
+ location: { line: testSourceLine }
110
+ } = test
84
111
 
85
112
  if (_type === 'beforeAll' || _type === 'afterAll') {
86
113
  return
@@ -100,7 +127,7 @@ function testBeginHandler (test) {
100
127
  const testAsyncResource = new AsyncResource('bound-anonymous-fn')
101
128
  testToAr.set(test, testAsyncResource)
102
129
  testAsyncResource.runInAsyncScope(() => {
103
- testStartCh.publish({ testName, testSuiteAbsolutePath, testSourceLine })
130
+ testStartCh.publish({ testName, testSuiteAbsolutePath, testSourceLine, browserName })
104
131
  })
105
132
  }
106
133
 
@@ -166,11 +193,12 @@ function dispatcherHook (dispatcherExport) {
166
193
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
167
194
  const dispatcher = this
168
195
  const worker = createWorker.apply(this, arguments)
169
-
170
196
  worker.process.on('message', ({ method, params }) => {
171
197
  if (method === 'testBegin') {
172
198
  const { test } = dispatcher._testById.get(params.testId)
173
- testBeginHandler(test)
199
+ const projects = getProjectsFromDispatcher(dispatcher)
200
+ const browser = getBrowserNameFromProjects(projects, test._projectId)
201
+ testBeginHandler(test, browser)
174
202
  } else if (method === 'testEnd') {
175
203
  const { test } = dispatcher._testById.get(params.testId)
176
204
 
@@ -203,7 +231,9 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
203
231
 
204
232
  worker.on('testBegin', ({ testId }) => {
205
233
  const test = getTestByTestId(dispatcher, testId)
206
- testBeginHandler(test)
234
+ const projects = getProjectsFromDispatcher(dispatcher)
235
+ const browser = getBrowserNameFromProjects(projects, test._projectId)
236
+ testBeginHandler(test, browser)
207
237
  })
208
238
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
209
239
  const test = getTestByTestId(dispatcher, testId)
@@ -226,6 +256,7 @@ function runnerHook (runnerExport, playwrightVersion) {
226
256
  testSessionAsyncResource.runInAsyncScope(() => {
227
257
  testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
228
258
  })
259
+ const projects = getProjectsFromRunner(this)
229
260
 
230
261
  const runAllTestsReturn = await runAllTests.apply(this, arguments)
231
262
 
@@ -234,7 +265,8 @@ function runnerHook (runnerExport, playwrightVersion) {
234
265
  // there were tests that did not go through `testBegin` or `testEnd`,
235
266
  // because they were skipped
236
267
  tests.forEach(test => {
237
- testBeginHandler(test)
268
+ const browser = getBrowserNameFromProjects(projects, test._projectId)
269
+ testBeginHandler(test, browser)
238
270
  testEndHandler(test, [], 'skip')
239
271
  })
240
272
  })
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { TEXT_MAP } = require('../../../ext/formats')
4
4
  const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
5
+ const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor')
5
6
  const { getResourceName } = require('./util')
6
7
 
7
8
  class AmqplibConsumerPlugin extends ConsumerPlugin {
@@ -13,7 +14,7 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
13
14
 
14
15
  const childOf = extract(this.tracer, message)
15
16
 
16
- this.startSpan({
17
+ const span = this.startSpan({
17
18
  childOf,
18
19
  resource: getResourceName(method, fields),
19
20
  type: 'worker',
@@ -26,6 +27,14 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
26
27
  'amqp.destination': fields.destination
27
28
  }
28
29
  })
30
+
31
+ if (this.config.dsmEnabled && message) {
32
+ const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
33
+ const queue = fields.queue ?? fields.routingKey
34
+ this.tracer.decodeDataStreamsContext(message.properties.headers[CONTEXT_PROPAGATION_KEY])
35
+ this.tracer
36
+ .setCheckpoint(['direction:in', `topic:${queue}`, 'type:rabbitmq'], span, payloadSize)
37
+ }
29
38
  }
30
39
  }
31
40
 
@@ -3,13 +3,15 @@
3
3
  const { TEXT_MAP } = require('../../../ext/formats')
4
4
  const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
5
5
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
6
+ const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway')
7
+ const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor')
6
8
  const { getResourceName } = require('./util')
7
9
 
8
10
  class AmqplibProducerPlugin extends ProducerPlugin {
9
11
  static get id () { return 'amqplib' }
10
12
  static get operation () { return 'command' }
11
13
 
12
- start ({ channel = {}, method, fields }) {
14
+ start ({ channel = {}, method, fields, message }) {
13
15
  if (method !== 'basic.publish') return
14
16
 
15
17
  const stream = (channel.connection && channel.connection.stream) || {}
@@ -30,6 +32,17 @@ class AmqplibProducerPlugin extends ProducerPlugin {
30
32
  fields.headers = fields.headers || {}
31
33
 
32
34
  this.tracer.inject(span, TEXT_MAP, fields.headers)
35
+
36
+ if (this.config.dsmEnabled) {
37
+ const hasRoutingKey = fields.routingKey != null
38
+ const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
39
+ const dataStreamsContext = this.tracer
40
+ .setCheckpoint(
41
+ ['direction:out', `exchange:${fields.exchange}`, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
42
+ , span, payloadSize)
43
+ const pathwayCtx = encodePathwayContext(dataStreamsContext)
44
+ fields.headers[CONTEXT_PROPAGATION_KEY] = pathwayCtx
45
+ }
33
46
  }
34
47
  }
35
48
 
@@ -1,15 +1,69 @@
1
1
  'use strict'
2
2
  const {
3
- CONTEXT_PROPAGATION_KEY
3
+ CONTEXT_PROPAGATION_KEY,
4
+ getSizeOrZero
4
5
  } = require('../../../dd-trace/src/datastreams/processor')
5
6
  const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
6
7
  const log = require('../../../dd-trace/src/log')
7
8
  const BaseAwsSdkPlugin = require('../base')
9
+ const { storage } = require('../../../datadog-core')
8
10
 
9
11
  class Kinesis extends BaseAwsSdkPlugin {
10
12
  static get id () { return 'kinesis' }
11
13
  static get peerServicePrecursors () { return ['streamname'] }
12
14
 
15
+ constructor (...args) {
16
+ super(...args)
17
+
18
+ // TODO(bengl) Find a way to create the response span tags without this WeakMap being populated
19
+ // in the base class
20
+ this.requestTags = new WeakMap()
21
+
22
+ this.addSub('apm:aws:response:start:kinesis', obj => {
23
+ const { request, response } = obj
24
+ const store = storage.getStore()
25
+ const plugin = this
26
+
27
+ // if we have either of these operations, we want to store the streamName param
28
+ // since it is not typically available during get/put records requests
29
+ if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
30
+ this.storeStreamName(request.params, request.operation, store)
31
+ return
32
+ }
33
+
34
+ if (request.operation === 'getRecords') {
35
+ let span
36
+ const responseExtraction = this.responseExtract(request.params, request.operation, response)
37
+ if (responseExtraction && responseExtraction.maybeChildOf) {
38
+ obj.needsFinish = true
39
+ const options = {
40
+ childOf: responseExtraction.maybeChildOf,
41
+ tags: Object.assign(
42
+ {},
43
+ this.requestTags.get(request) || {},
44
+ { 'span.kind': 'server' }
45
+ )
46
+ }
47
+ span = plugin.tracer.startSpan('aws.response', options)
48
+ this.enter(span, store)
49
+ }
50
+
51
+ // get the stream name that should have been stored previously
52
+ const { streamName } = storage.getStore()
53
+
54
+ // extract DSM context after as we might not have a parent-child but may have a DSM context
55
+ this.responseExtractDSMContext(
56
+ request.operation, response, span ?? null, streamName
57
+ )
58
+ }
59
+ })
60
+
61
+ this.addSub('apm:aws:response:finish:kinesis', err => {
62
+ const { span } = storage.getStore()
63
+ this.finish(span, null, err)
64
+ })
65
+ }
66
+
13
67
  generateTags (params, operation, response) {
14
68
  if (!params || !params.StreamName) return {}
15
69
 
@@ -20,6 +74,58 @@ class Kinesis extends BaseAwsSdkPlugin {
20
74
  }
21
75
  }
22
76
 
77
+ storeStreamName (params, operation, store) {
78
+ if (!operation || (operation !== 'getShardIterator' && operation !== 'listShards')) return
79
+ if (!params || !params.StreamName) return
80
+
81
+ const streamName = params.StreamName
82
+ storage.enterWith({ ...store, streamName })
83
+ }
84
+
85
+ responseExtract (params, operation, response) {
86
+ if (operation !== 'getRecords') return
87
+ if (params.Limit && params.Limit !== 1) return
88
+ if (!response || !response.Records || !response.Records[0]) return
89
+
90
+ const record = response.Records[0]
91
+
92
+ try {
93
+ const decodedData = JSON.parse(Buffer.from(record.Data).toString())
94
+
95
+ return {
96
+ maybeChildOf: this.tracer.extract('text_map', decodedData._datadog),
97
+ parsedAttributes: decodedData._datadog
98
+ }
99
+ } catch (e) {
100
+ log.error(e)
101
+ }
102
+ }
103
+
104
+ responseExtractDSMContext (operation, response, span, streamName) {
105
+ if (!this.config.dsmEnabled) return
106
+ if (operation !== 'getRecords') return
107
+ if (!response || !response.Records || !response.Records[0]) return
108
+
109
+ // we only want to set the payloadSize on the span if we have one message, not repeatedly
110
+ span = response.Records.length > 1 ? null : span
111
+
112
+ response.Records.forEach(record => {
113
+ const parsedAttributes = JSON.parse(Buffer.from(record.Data).toString())
114
+
115
+ if (
116
+ parsedAttributes &&
117
+ parsedAttributes._datadog &&
118
+ parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY] &&
119
+ streamName
120
+ ) {
121
+ const payloadSize = getSizeOrZero(record.Data)
122
+ this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY]))
123
+ this.tracer
124
+ .setCheckpoint(['direction:in', `topic:${streamName}`, 'type:kinesis'], span, payloadSize)
125
+ }
126
+ })
127
+ }
128
+
23
129
  // AWS-SDK will b64 kinesis payloads
24
130
  // or will accept an already b64 encoded payload
25
131
  // This method handles both