dd-trace 5.22.0 → 5.23.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 (89) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +20 -8
  3. package/package.json +9 -3
  4. package/packages/datadog-instrumentations/src/cucumber.js +290 -53
  5. package/packages/datadog-instrumentations/src/jest.js +3 -1
  6. package/packages/datadog-instrumentations/src/kafkajs.js +67 -31
  7. package/packages/datadog-instrumentations/src/microgateway-core.js +3 -1
  8. package/packages/datadog-instrumentations/src/mocha/main.js +139 -54
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -15
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +29 -1
  11. package/packages/datadog-instrumentations/src/openai.js +4 -2
  12. package/packages/datadog-instrumentations/src/pg.js +59 -4
  13. package/packages/datadog-instrumentations/src/vitest.js +184 -9
  14. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +33 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  17. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  19. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  20. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +36 -6
  21. package/packages/datadog-plugin-cypress/src/support.js +4 -1
  22. package/packages/datadog-plugin-http/src/client.js +1 -42
  23. package/packages/datadog-plugin-http2/src/client.js +1 -26
  24. package/packages/datadog-plugin-jest/src/index.js +17 -1
  25. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +20 -0
  26. package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -2
  27. package/packages/datadog-plugin-kafkajs/src/index.js +3 -1
  28. package/packages/datadog-plugin-mocha/src/index.js +18 -0
  29. package/packages/datadog-plugin-openai/src/index.js +27 -18
  30. package/packages/datadog-plugin-playwright/src/index.js +9 -0
  31. package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
  32. package/packages/datadog-plugin-vitest/src/index.js +68 -3
  33. package/packages/dd-trace/src/appsec/addresses.js +3 -1
  34. package/packages/dd-trace/src/appsec/channels.js +4 -2
  35. package/packages/dd-trace/src/appsec/rasp/index.js +103 -0
  36. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +86 -0
  37. package/packages/dd-trace/src/appsec/rasp/ssrf.js +37 -0
  38. package/packages/dd-trace/src/appsec/rasp/utils.js +63 -0
  39. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  40. package/packages/dd-trace/src/appsec/remote_config/index.js +16 -7
  41. package/packages/dd-trace/src/appsec/remote_config/manager.js +89 -51
  42. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +33 -14
  43. package/packages/dd-trace/src/appsec/waf/waf_manager.js +2 -1
  44. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -0
  46. package/packages/dd-trace/src/config.js +61 -10
  47. package/packages/dd-trace/src/constants.js +11 -1
  48. package/packages/dd-trace/src/data_streams_context.js +3 -0
  49. package/packages/dd-trace/src/datastreams/fnv.js +23 -0
  50. package/packages/dd-trace/src/datastreams/pathway.js +12 -5
  51. package/packages/dd-trace/src/datastreams/processor.js +35 -0
  52. package/packages/dd-trace/src/datastreams/schemas/schema.js +8 -0
  53. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +125 -0
  54. package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +29 -0
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +24 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/index.js +57 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +23 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +164 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/send.js +28 -0
  60. package/packages/dd-trace/src/debugger/devtools_client/session.js +7 -0
  61. package/packages/dd-trace/src/debugger/devtools_client/state.js +47 -0
  62. package/packages/dd-trace/src/debugger/devtools_client/status.js +109 -0
  63. package/packages/dd-trace/src/debugger/index.js +92 -0
  64. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +29 -2
  65. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  66. package/packages/dd-trace/src/payload-tagging/config/aws.json +30 -0
  67. package/packages/dd-trace/src/payload-tagging/config/index.js +30 -0
  68. package/packages/dd-trace/src/payload-tagging/index.js +93 -0
  69. package/packages/dd-trace/src/payload-tagging/tagging.js +83 -0
  70. package/packages/dd-trace/src/plugin_manager.js +11 -10
  71. package/packages/dd-trace/src/plugins/ci_plugin.js +33 -8
  72. package/packages/dd-trace/src/plugins/util/env.js +5 -2
  73. package/packages/dd-trace/src/plugins/util/test.js +26 -2
  74. package/packages/dd-trace/src/profiling/config.js +5 -0
  75. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  76. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns.js +13 -0
  77. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookup.js +16 -0
  78. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookupservice.js +16 -0
  79. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_resolve.js +24 -0
  80. package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_reverse.js +16 -0
  81. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +48 -0
  82. package/packages/dd-trace/src/profiling/profilers/event_plugins/net.js +24 -0
  83. package/packages/dd-trace/src/profiling/profilers/events.js +108 -32
  84. package/packages/dd-trace/src/profiling/profilers/shared.js +5 -0
  85. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  86. package/packages/dd-trace/src/profiling/ssi-heuristics.js +10 -2
  87. package/packages/dd-trace/src/proxy.js +10 -3
  88. package/packages/dd-trace/src/span_stats.js +4 -2
  89. package/packages/dd-trace/src/appsec/rasp.js +0 -176
@@ -58,7 +58,7 @@ class HttpClientPlugin extends ClientPlugin {
58
58
  span._spanContext._trace.record = false
59
59
  }
60
60
 
61
- if (this.shouldInjectTraceHeaders(options, uri)) {
61
+ if (this.config.propagationFilter(uri)) {
62
62
  this.tracer.inject(span, HTTP_HEADERS, options.headers)
63
63
  }
64
64
 
@@ -71,18 +71,6 @@ class HttpClientPlugin extends ClientPlugin {
71
71
  return message.currentStore
72
72
  }
73
73
 
74
- shouldInjectTraceHeaders (options, uri) {
75
- if (hasAmazonSignature(options) && !this.config.enablePropagationWithAmazonHeaders) {
76
- return false
77
- }
78
-
79
- if (!this.config.propagationFilter(uri)) {
80
- return false
81
- }
82
-
83
- return true
84
- }
85
-
86
74
  bindAsyncStart ({ parentStore }) {
87
75
  return parentStore
88
76
  }
@@ -212,31 +200,6 @@ function getHooks (config) {
212
200
  return { request }
213
201
  }
214
202
 
215
- function hasAmazonSignature (options) {
216
- if (!options) {
217
- return false
218
- }
219
-
220
- if (options.headers) {
221
- const headers = Object.keys(options.headers)
222
- .reduce((prev, next) => Object.assign(prev, {
223
- [next.toLowerCase()]: options.headers[next]
224
- }), {})
225
-
226
- if (headers['x-amz-signature']) {
227
- return true
228
- }
229
-
230
- if ([].concat(headers.authorization).some(startsWith('AWS4-HMAC-SHA256'))) {
231
- return true
232
- }
233
- }
234
-
235
- const search = options.search || options.path
236
-
237
- return search && search.toLowerCase().indexOf('x-amz-signature=') !== -1
238
- }
239
-
240
203
  function extractSessionDetails (options) {
241
204
  if (typeof options === 'string') {
242
205
  return new URL(options).host
@@ -248,8 +211,4 @@ function extractSessionDetails (options) {
248
211
  return { host, port }
249
212
  }
250
213
 
251
- function startsWith (searchString) {
252
- return value => String(value).startsWith(searchString)
253
- }
254
-
255
214
  module.exports = HttpClientPlugin
@@ -62,9 +62,7 @@ class Http2ClientPlugin extends ClientPlugin {
62
62
 
63
63
  addHeaderTags(span, headers, HTTP_REQUEST_HEADERS, this.config)
64
64
 
65
- if (!hasAmazonSignature(headers, path)) {
66
- this.tracer.inject(span, HTTP_HEADERS, headers)
67
- }
65
+ this.tracer.inject(span, HTTP_HEADERS, headers)
68
66
 
69
67
  message.parentStore = store
70
68
  message.currentStore = { ...store, span }
@@ -134,29 +132,6 @@ function extractSessionDetails (authority, options) {
134
132
  return { protocol, port, host }
135
133
  }
136
134
 
137
- function hasAmazonSignature (headers, path) {
138
- if (headers) {
139
- headers = Object.keys(headers)
140
- .reduce((prev, next) => Object.assign(prev, {
141
- [next.toLowerCase()]: headers[next]
142
- }), {})
143
-
144
- if (headers['x-amz-signature']) {
145
- return true
146
- }
147
-
148
- if ([].concat(headers.authorization).some(startsWith('AWS4-HMAC-SHA256'))) {
149
- return true
150
- }
151
- }
152
-
153
- return path && path.toLowerCase().indexOf('x-amz-signature=') !== -1
154
- }
155
-
156
- function startsWith (searchString) {
157
- return value => String(value).startsWith(searchString)
158
- }
159
-
160
135
  function getStatusValidator (config) {
161
136
  if (typeof config.validateStatus === 'function') {
162
137
  return config.validateStatus
@@ -158,7 +158,13 @@ class JestPlugin extends CiPlugin {
158
158
  })
159
159
  })
160
160
 
161
- this.addSub('ci:jest:test-suite:start', ({ testSuite, testEnvironmentOptions, frameworkVersion, displayName }) => {
161
+ this.addSub('ci:jest:test-suite:start', ({
162
+ testSuite,
163
+ testSourceFile,
164
+ testEnvironmentOptions,
165
+ frameworkVersion,
166
+ displayName
167
+ }) => {
162
168
  const {
163
169
  _ddTestSessionId: testSessionId,
164
170
  _ddTestCommand: testCommand,
@@ -196,6 +202,16 @@ class JestPlugin extends CiPlugin {
196
202
  if (displayName) {
197
203
  testSuiteMetadata[JEST_DISPLAY_NAME] = displayName
198
204
  }
205
+ if (testSourceFile) {
206
+ testSuiteMetadata[TEST_SOURCE_FILE] = testSourceFile
207
+ // Test suite is the whole test file, so we can use the first line as the start
208
+ testSuiteMetadata[TEST_SOURCE_START] = 1
209
+ }
210
+
211
+ const codeOwners = this.getCodeOwners(testSuiteMetadata)
212
+ if (codeOwners) {
213
+ testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
214
+ }
199
215
 
200
216
  this.testSuiteSpan = this.tracer.startSpan('jest.test_suite', {
201
217
  childOf: testSessionSpanContext,
@@ -0,0 +1,20 @@
1
+ const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
2
+ const { getMessageSize } = require('../../dd-trace/src/datastreams/processor')
3
+
4
+ class KafkajsBatchConsumerPlugin extends ConsumerPlugin {
5
+ static get id () { return 'kafkajs' }
6
+ static get operation () { return 'consume-batch' }
7
+
8
+ start ({ topic, partition, messages, groupId }) {
9
+ if (!this.config.dsmEnabled) return
10
+ for (const message of messages) {
11
+ if (!message || !message.headers) continue
12
+ const payloadSize = getMessageSize(message)
13
+ this.tracer.decodeDataStreamsContext(message.headers)
14
+ this.tracer
15
+ .setCheckpoint(['direction:in', `group:${groupId}`, `topic:${topic}`, 'type:kafka'], null, payloadSize)
16
+ }
17
+ }
18
+ }
19
+
20
+ module.exports = KafkajsBatchConsumerPlugin
@@ -2,7 +2,6 @@
2
2
 
3
3
  const dc = require('dc-polyfill')
4
4
  const { getMessageSize } = require('../../dd-trace/src/datastreams/processor')
5
- const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway')
6
5
  const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
7
6
 
8
7
  const afterStartCh = dc.channel('dd-trace:kafkajs:consumer:afterStart')
@@ -78,7 +77,7 @@ class KafkajsConsumerPlugin extends ConsumerPlugin {
78
77
  'kafka.partition': partition
79
78
  }
80
79
  })
81
- if (this.config.dsmEnabled && message?.headers && DsmPathwayCodec.contextExists(message.headers)) {
80
+ if (this.config.dsmEnabled && message?.headers) {
82
81
  const payloadSize = getMessageSize(message)
83
82
  this.tracer.decodeDataStreamsContext(message.headers)
84
83
  this.tracer
@@ -2,6 +2,7 @@
2
2
 
3
3
  const ProducerPlugin = require('./producer')
4
4
  const ConsumerPlugin = require('./consumer')
5
+ const BatchConsumerPlugin = require('./batch-consumer')
5
6
  const CompositePlugin = require('../../dd-trace/src/plugins/composite')
6
7
 
7
8
  class KafkajsPlugin extends CompositePlugin {
@@ -9,7 +10,8 @@ class KafkajsPlugin extends CompositePlugin {
9
10
  static get plugins () {
10
11
  return {
11
12
  producer: ProducerPlugin,
12
- consumer: ConsumerPlugin
13
+ consumer: ConsumerPlugin,
14
+ batchConsumer: BatchConsumerPlugin
13
15
  }
14
16
  }
15
17
  }
@@ -21,6 +21,7 @@ const {
21
21
  TEST_IS_NEW,
22
22
  TEST_IS_RETRY,
23
23
  TEST_EARLY_FLAKE_ENABLED,
24
+ TEST_EARLY_FLAKE_ABORT_REASON,
24
25
  TEST_SESSION_ID,
25
26
  TEST_MODULE_ID,
26
27
  TEST_MODULE,
@@ -124,6 +125,19 @@ class MochaPlugin extends CiPlugin {
124
125
  testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
125
126
  this.telemetry.count(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' })
126
127
  }
128
+ if (this.repositoryRoot !== this.sourceRoot && !!this.repositoryRoot) {
129
+ testSuiteMetadata[TEST_SOURCE_FILE] = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
130
+ } else {
131
+ testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
132
+ }
133
+ if (testSuiteMetadata[TEST_SOURCE_FILE]) {
134
+ testSuiteMetadata[TEST_SOURCE_START] = 1
135
+ }
136
+
137
+ const codeOwners = this.getCodeOwners(testSuiteMetadata)
138
+ if (codeOwners) {
139
+ testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
140
+ }
127
141
 
128
142
  const testSuiteSpan = this.tracer.startSpan('mocha.test_suite', {
129
143
  childOf: this.testModuleSpan,
@@ -267,6 +281,7 @@ class MochaPlugin extends CiPlugin {
267
281
  hasUnskippableSuites,
268
282
  error,
269
283
  isEarlyFlakeDetectionEnabled,
284
+ isEarlyFlakeDetectionFaulty,
270
285
  isParallel
271
286
  }) => {
272
287
  if (this.testSessionSpan) {
@@ -301,6 +316,9 @@ class MochaPlugin extends CiPlugin {
301
316
  if (isEarlyFlakeDetectionEnabled) {
302
317
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
303
318
  }
319
+ if (isEarlyFlakeDetectionFaulty) {
320
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
321
+ }
304
322
 
305
323
  this.testModuleSpan.finish()
306
324
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
@@ -276,25 +276,34 @@ class OpenApiPlugin extends TracingPlugin {
276
276
  const completionTokens = spanTags['openai.response.usage.completion_tokens']
277
277
  const completionTokensEstimated = spanTags['openai.response.usage.completion_tokens_estimated']
278
278
 
279
+ const totalTokens = spanTags['openai.response.usage.total_tokens']
280
+
279
281
  if (!error) {
280
- if (promptTokensEstimated) {
281
- this.metrics.distribution(
282
- 'openai.tokens.prompt', promptTokens, [...tags, 'openai.estimated:true'])
283
- } else {
284
- this.metrics.distribution('openai.tokens.prompt', promptTokens, tags)
282
+ if (promptTokens != null) {
283
+ if (promptTokensEstimated) {
284
+ this.metrics.distribution(
285
+ 'openai.tokens.prompt', promptTokens, [...tags, 'openai.estimated:true'])
286
+ } else {
287
+ this.metrics.distribution('openai.tokens.prompt', promptTokens, tags)
288
+ }
285
289
  }
286
- if (completionTokensEstimated) {
287
- this.metrics.distribution(
288
- 'openai.tokens.completion', completionTokens, [...tags, 'openai.estimated:true'])
289
- } else {
290
- this.metrics.distribution('openai.tokens.completion', completionTokens, tags)
290
+
291
+ if (completionTokens != null) {
292
+ if (completionTokensEstimated) {
293
+ this.metrics.distribution(
294
+ 'openai.tokens.completion', completionTokens, [...tags, 'openai.estimated:true'])
295
+ } else {
296
+ this.metrics.distribution('openai.tokens.completion', completionTokens, tags)
297
+ }
291
298
  }
292
299
 
293
- if (promptTokensEstimated || completionTokensEstimated) {
294
- this.metrics.distribution(
295
- 'openai.tokens.total', promptTokens + completionTokens, [...tags, 'openai.estimated:true'])
296
- } else {
297
- this.metrics.distribution('openai.tokens.total', promptTokens + completionTokens, tags)
300
+ if (totalTokens != null) {
301
+ if (promptTokensEstimated || completionTokensEstimated) {
302
+ this.metrics.distribution(
303
+ 'openai.tokens.total', totalTokens, [...tags, 'openai.estimated:true'])
304
+ } else {
305
+ this.metrics.distribution('openai.tokens.total', totalTokens, tags)
306
+ }
298
307
  }
299
308
  }
300
309
 
@@ -777,9 +786,9 @@ function usageExtraction (tags, body, methodName, openaiStore) {
777
786
  if (completionEstimated) tags['openai.response.usage.completion_tokens_estimated'] = true
778
787
  }
779
788
 
780
- if (promptTokens) tags['openai.response.usage.prompt_tokens'] = promptTokens
781
- if (completionTokens) tags['openai.response.usage.completion_tokens'] = completionTokens
782
- if (totalTokens) tags['openai.response.usage.total_tokens'] = totalTokens
789
+ if (promptTokens != null) tags['openai.response.usage.prompt_tokens'] = promptTokens
790
+ if (completionTokens != null) tags['openai.response.usage.completion_tokens'] = completionTokens
791
+ if (totalTokens != null) tags['openai.response.usage.total_tokens'] = totalTokens
783
792
  }
784
793
 
785
794
  function truncateApiKey (apiKey) {
@@ -69,6 +69,7 @@ class PlaywrightPlugin extends CiPlugin {
69
69
  this.addSub('ci:playwright:test-suite:start', (testSuiteAbsolutePath) => {
70
70
  const store = storage.getStore()
71
71
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
72
+ const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
72
73
 
73
74
  const testSuiteMetadata = getTestSuiteCommonTags(
74
75
  this.command,
@@ -76,6 +77,14 @@ class PlaywrightPlugin extends CiPlugin {
76
77
  testSuite,
77
78
  'playwright'
78
79
  )
80
+ if (testSourceFile) {
81
+ testSuiteMetadata[TEST_SOURCE_FILE] = testSourceFile
82
+ testSuiteMetadata[TEST_SOURCE_START] = 1
83
+ }
84
+ const codeOwners = this.getCodeOwners(testSuiteMetadata)
85
+ if (codeOwners) {
86
+ testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
87
+ }
79
88
 
80
89
  const testSuiteSpan = this.tracer.startSpan('playwright.test_suite', {
81
90
  childOf: this.testModuleSpan,
@@ -3,7 +3,6 @@
3
3
  const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
4
4
  const { storage } = require('../../datadog-core')
5
5
  const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor')
6
- const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway')
7
6
 
8
7
  class RheaConsumerPlugin extends ConsumerPlugin {
9
8
  static get id () { return 'rhea' }
@@ -34,8 +33,7 @@ class RheaConsumerPlugin extends ConsumerPlugin {
34
33
 
35
34
  if (
36
35
  this.config.dsmEnabled &&
37
- msgObj?.message?.delivery_annotations &&
38
- DsmPathwayCodec.contextExists(msgObj.message.delivery_annotations)
36
+ msgObj?.message?.delivery_annotations
39
37
  ) {
40
38
  const payloadSize = getAmqpMessageSize(
41
39
  { headers: msgObj.message.delivery_annotations, content: msgObj.message.body }
@@ -6,10 +6,18 @@ const {
6
6
  finishAllTraceSpans,
7
7
  getTestSuitePath,
8
8
  getTestSuiteCommonTags,
9
+ getTestSessionName,
10
+ getIsFaultyEarlyFlakeDetection,
9
11
  TEST_SOURCE_FILE,
10
12
  TEST_IS_RETRY,
11
13
  TEST_CODE_COVERAGE_LINES_PCT,
12
- TEST_CODE_OWNERS
14
+ TEST_CODE_OWNERS,
15
+ TEST_LEVEL_EVENT_TYPES,
16
+ TEST_SESSION_NAME,
17
+ TEST_SOURCE_START,
18
+ TEST_IS_NEW,
19
+ TEST_EARLY_FLAKE_ENABLED,
20
+ TEST_EARLY_FLAKE_ABORT_REASON
13
21
  } = require('../../dd-trace/src/plugins/util/test')
14
22
  const { COMPONENT } = require('../../dd-trace/src/constants')
15
23
  const {
@@ -33,7 +41,26 @@ class VitestPlugin extends CiPlugin {
33
41
 
34
42
  this.taskToFinishTime = new WeakMap()
35
43
 
36
- this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry }) => {
44
+ this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
45
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
46
+ const testsForThisTestSuite = knownTests[testSuite] || []
47
+ onDone(!testsForThisTestSuite.includes(testName))
48
+ })
49
+
50
+ this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
51
+ knownTests,
52
+ testFilepaths,
53
+ onDone
54
+ }) => {
55
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
56
+ testFilepaths.map(testFilepath => getTestSuitePath(testFilepath, this.repositoryRoot)),
57
+ knownTests,
58
+ this.libraryConfig.earlyFlakeDetectionFaultyThreshold
59
+ )
60
+ onDone(isFaulty)
61
+ })
62
+
63
+ this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry, isNew }) => {
37
64
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
38
65
  const store = storage.getStore()
39
66
 
@@ -43,6 +70,9 @@ class VitestPlugin extends CiPlugin {
43
70
  if (isRetry) {
44
71
  extraTags[TEST_IS_RETRY] = 'true'
45
72
  }
73
+ if (isNew) {
74
+ extraTags[TEST_IS_NEW] = 'true'
75
+ }
46
76
 
47
77
  const span = this.startTestSpan(
48
78
  testName,
@@ -110,6 +140,7 @@ class VitestPlugin extends CiPlugin {
110
140
  this.testSuiteSpan,
111
141
  {
112
142
  [TEST_SOURCE_FILE]: testSuite,
143
+ [TEST_SOURCE_START]: 1, // we can't get the proper start line in vitest
113
144
  [TEST_STATUS]: 'skip'
114
145
  }
115
146
  )
@@ -120,12 +151,25 @@ class VitestPlugin extends CiPlugin {
120
151
  })
121
152
 
122
153
  this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
154
+ this.command = process.env.DD_CIVISIBILITY_TEST_COMMAND
123
155
  this.frameworkVersion = frameworkVersion
124
156
  const testSessionSpanContext = this.tracer.extract('text_map', {
125
157
  'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
126
158
  'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
127
159
  })
128
160
 
161
+ // test suites run in a different process, so they also need to init the metadata dictionary
162
+ const testSessionName = getTestSessionName(this.config, this.command, this.testEnvironmentMetadata)
163
+ const metadataTags = {}
164
+ for (const testLevel of TEST_LEVEL_EVENT_TYPES) {
165
+ metadataTags[testLevel] = {
166
+ [TEST_SESSION_NAME]: testSessionName
167
+ }
168
+ }
169
+ if (this.tracer._exporter.setMetadataTags) {
170
+ this.tracer._exporter.setMetadataTags(metadataTags)
171
+ }
172
+
129
173
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
130
174
  const testSuiteMetadata = getTestSuiteCommonTags(
131
175
  this.command,
@@ -133,6 +177,14 @@ class VitestPlugin extends CiPlugin {
133
177
  testSuite,
134
178
  'vitest'
135
179
  )
180
+ testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
181
+ testSuiteMetadata[TEST_SOURCE_START] = 1
182
+
183
+ const codeOwners = this.getCodeOwners(testSuiteMetadata)
184
+ if (codeOwners) {
185
+ testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
186
+ }
187
+
136
188
  const testSuiteSpan = this.tracer.startSpan('vitest.test_suite', {
137
189
  childOf: testSessionSpanContext,
138
190
  tags: {
@@ -169,7 +221,14 @@ class VitestPlugin extends CiPlugin {
169
221
  }
170
222
  })
171
223
 
172
- this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => {
224
+ this.addSub('ci:vitest:session:finish', ({
225
+ status,
226
+ error,
227
+ testCodeCoverageLinesTotal,
228
+ isEarlyFlakeDetectionEnabled,
229
+ isEarlyFlakeDetectionFaulty,
230
+ onFinish
231
+ }) => {
173
232
  this.testSessionSpan.setTag(TEST_STATUS, status)
174
233
  this.testModuleSpan.setTag(TEST_STATUS, status)
175
234
  if (error) {
@@ -180,6 +239,12 @@ class VitestPlugin extends CiPlugin {
180
239
  this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
181
240
  this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
182
241
  }
242
+ if (isEarlyFlakeDetectionEnabled) {
243
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
244
+ }
245
+ if (isEarlyFlakeDetectionFaulty) {
246
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
247
+ }
183
248
  this.testModuleSpan.finish()
184
249
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
185
250
  this.testSessionSpan.finish()
@@ -22,5 +22,7 @@ module.exports = {
22
22
  USER_ID: 'usr.id',
23
23
  WAF_CONTEXT_PROCESSOR: 'waf.context.processor',
24
24
 
25
- HTTP_OUTGOING_URL: 'server.io.net.url'
25
+ HTTP_OUTGOING_URL: 'server.io.net.url',
26
+ DB_STATEMENT: 'server.db.statement',
27
+ DB_SYSTEM: 'server.db.system'
26
28
  }
@@ -21,6 +21,8 @@ module.exports = {
21
21
  responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
22
22
  httpClientRequestStart: dc.channel('apm:http:client:request:start'),
23
23
  responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
24
- setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
25
-
24
+ setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
25
+ pgQueryStart: dc.channel('apm:pg:query:start'),
26
+ pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
27
+ wafRunFinished: dc.channel('datadog:waf:run:finish')
26
28
  }
@@ -0,0 +1,103 @@
1
+ 'use strict'
2
+
3
+ const web = require('../../plugins/util/web')
4
+ const { setUncaughtExceptionCaptureCallbackStart } = require('../channels')
5
+ const { block } = require('../blocking')
6
+ const ssrf = require('./ssrf')
7
+ const sqli = require('./sql_injection')
8
+
9
+ const { DatadogRaspAbortError } = require('./utils')
10
+
11
+ function removeAllListeners (emitter, event) {
12
+ const listeners = emitter.listeners(event)
13
+ emitter.removeAllListeners(event)
14
+
15
+ let cleaned = false
16
+ return function () {
17
+ if (cleaned === true) {
18
+ return
19
+ }
20
+ cleaned = true
21
+
22
+ for (let i = 0; i < listeners.length; ++i) {
23
+ emitter.on(event, listeners[i])
24
+ }
25
+ }
26
+ }
27
+
28
+ function findDatadogRaspAbortError (err, deep = 10) {
29
+ if (err instanceof DatadogRaspAbortError) {
30
+ return err
31
+ }
32
+
33
+ if (err.cause && deep > 0) {
34
+ return findDatadogRaspAbortError(err.cause, deep - 1)
35
+ }
36
+ }
37
+
38
+ function handleUncaughtExceptionMonitor (err) {
39
+ const abortError = findDatadogRaspAbortError(err)
40
+ if (!abortError) return
41
+
42
+ const { req, res, blockingAction } = abortError
43
+ block(req, res, web.root(req), null, blockingAction)
44
+
45
+ if (!process.hasUncaughtExceptionCaptureCallback()) {
46
+ const cleanUp = removeAllListeners(process, 'uncaughtException')
47
+ const handler = () => {
48
+ process.removeListener('uncaughtException', handler)
49
+ }
50
+
51
+ setTimeout(() => {
52
+ process.removeListener('uncaughtException', handler)
53
+ cleanUp()
54
+ })
55
+
56
+ process.on('uncaughtException', handler)
57
+ } else {
58
+ // uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
59
+ let previousCb
60
+ const cb = ({ currentCallback, abortController }) => {
61
+ setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb)
62
+ if (!currentCallback) {
63
+ abortController.abort()
64
+ return
65
+ }
66
+
67
+ previousCb = currentCallback
68
+ }
69
+
70
+ setUncaughtExceptionCaptureCallbackStart.subscribe(cb)
71
+
72
+ process.setUncaughtExceptionCaptureCallback(null)
73
+
74
+ // For some reason, previous callback was defined before the instrumentation
75
+ // We can not restore it, so we let the app decide
76
+ if (previousCb) {
77
+ process.setUncaughtExceptionCaptureCallback(() => {
78
+ process.setUncaughtExceptionCaptureCallback(null)
79
+ process.setUncaughtExceptionCaptureCallback(previousCb)
80
+ })
81
+ }
82
+ }
83
+ }
84
+
85
+ function enable (config) {
86
+ ssrf.enable(config)
87
+ sqli.enable(config)
88
+
89
+ process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
90
+ }
91
+
92
+ function disable () {
93
+ ssrf.disable()
94
+ sqli.disable()
95
+
96
+ process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
97
+ }
98
+
99
+ module.exports = {
100
+ enable,
101
+ disable,
102
+ handleUncaughtExceptionMonitor // exported only for testing purpose
103
+ }