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
@@ -59,7 +59,12 @@ class Sns extends BaseAwsSdkPlugin {
59
59
  break
60
60
  case 'publishBatch':
61
61
  for (let i = 0; i < params.PublishBatchRequestEntries.length; i++) {
62
- this.injectToMessage(span, params.PublishBatchRequestEntries[i], params.TopicArn, i === 0)
62
+ this.injectToMessage(
63
+ span,
64
+ params.PublishBatchRequestEntries[i],
65
+ params.TopicArn,
66
+ i === 0 || (this.config.sns && this.config.sns.batchPropagationEnabled)
67
+ )
63
68
  }
64
69
  break
65
70
  }
@@ -23,7 +23,7 @@ class Sqs extends BaseAwsSdkPlugin {
23
23
  const plugin = this
24
24
  const contextExtraction = this.responseExtract(request.params, request.operation, response)
25
25
  let span
26
- let parsedMessageAttributes
26
+ let parsedMessageAttributes = null
27
27
  if (contextExtraction && contextExtraction.datadogContext) {
28
28
  obj.needsFinish = true
29
29
  const options = {
@@ -39,8 +39,9 @@ class Sqs extends BaseAwsSdkPlugin {
39
39
  this.enter(span, store)
40
40
  }
41
41
  // extract DSM context after as we might not have a parent-child but may have a DSM context
42
+
42
43
  this.responseExtractDSMContext(
43
- request.operation, request.params, response, span || null, parsedMessageAttributes || null
44
+ request.operation, request.params, response, span || null, { parsedMessageAttributes }
44
45
  )
45
46
  })
46
47
 
@@ -165,7 +166,8 @@ class Sqs extends BaseAwsSdkPlugin {
165
166
  }
166
167
  }
167
168
 
168
- responseExtractDSMContext (operation, params, response, span, parsedAttributes) {
169
+ responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
170
+ let { parsedAttributes } = kwargs
169
171
  if (!this.config.dsmEnabled) return
170
172
  if (operation !== 'receiveMessage') return
171
173
  if (!response || !response.Messages || !response.Messages[0]) return
@@ -188,7 +190,7 @@ class Sqs extends BaseAwsSdkPlugin {
188
190
  // SQS to SQS
189
191
  }
190
192
  }
191
- if (message.MessageAttributes && message.MessageAttributes._datadog) {
193
+ if (!parsedAttributes && message.MessageAttributes && message.MessageAttributes._datadog) {
192
194
  parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
193
195
  }
194
196
  }
@@ -216,7 +218,23 @@ class Sqs extends BaseAwsSdkPlugin {
216
218
  break
217
219
  case 'sendMessageBatch':
218
220
  for (let i = 0; i < params.Entries.length; i++) {
219
- this.injectToMessage(span, params.Entries[i], params.QueueUrl, i === 0)
221
+ this.injectToMessage(
222
+ span,
223
+ params.Entries[i],
224
+ params.QueueUrl,
225
+ i === 0 || (this.config.sqs && this.config.sqs.batchPropagationEnabled)
226
+ )
227
+ }
228
+ break
229
+ case 'receiveMessage':
230
+ if (!params.MessageAttributeNames) {
231
+ params.MessageAttributeNames = ['_datadog']
232
+ } else if (
233
+ !params.MessageAttributeNames.includes('_datadog') &&
234
+ !params.MessageAttributeNames.includes('.*') &&
235
+ !params.MessageAttributeNames.includes('All')
236
+ ) {
237
+ params.MessageAttributeNames.push('_datadog')
220
238
  }
221
239
  break
222
240
  }
@@ -54,7 +54,7 @@ class ChildProcessPlugin extends TracingPlugin {
54
54
  }
55
55
 
56
56
  this.startSpan('command_execution', {
57
- service: this.config.service,
57
+ service: this.config.service || this._tracerConfig.service,
58
58
  resource: (shell === true) ? 'sh' : cmdFields[0],
59
59
  type: 'system',
60
60
  meta
@@ -195,6 +195,17 @@ class CucumberPlugin extends CiPlugin {
195
195
  this.enter(testSpan, store)
196
196
  })
197
197
 
198
+ this.addSub('ci:cucumber:test:retry', (isFlakyRetry) => {
199
+ const store = storage.getStore()
200
+ const span = store.span
201
+ if (isFlakyRetry) {
202
+ span.setTag(TEST_IS_RETRY, 'true')
203
+ }
204
+ span.setTag(TEST_STATUS, 'fail')
205
+ span.finish()
206
+ finishAllTraceSpans(span)
207
+ })
208
+
198
209
  this.addSub('ci:cucumber:test-step:start', ({ resource }) => {
199
210
  const store = storage.getStore()
200
211
  const childOf = store ? store.span : store
@@ -239,7 +250,15 @@ class CucumberPlugin extends CiPlugin {
239
250
  })
240
251
  })
241
252
 
242
- this.addSub('ci:cucumber:test:finish', ({ isStep, status, skipReason, errorMessage, isNew, isEfdRetry }) => {
253
+ this.addSub('ci:cucumber:test:finish', ({
254
+ isStep,
255
+ status,
256
+ skipReason,
257
+ errorMessage,
258
+ isNew,
259
+ isEfdRetry,
260
+ isFlakyRetry
261
+ }) => {
243
262
  const span = storage.getStore().span
244
263
  const statusTag = isStep ? 'step.status' : TEST_STATUS
245
264
 
@@ -260,6 +279,10 @@ class CucumberPlugin extends CiPlugin {
260
279
  span.setTag(ERROR_MESSAGE, errorMessage)
261
280
  }
262
281
 
282
+ if (isFlakyRetry > 0) {
283
+ span.setTag(TEST_IS_RETRY, 'true')
284
+ }
285
+
263
286
  span.finish()
264
287
  if (!isStep) {
265
288
  this.telemetry.ciVisEvent(
@@ -175,13 +175,15 @@ class MochaPlugin extends CiPlugin {
175
175
  this.tracer._exporter.flush()
176
176
  })
177
177
 
178
- this.addSub('ci:mocha:test:finish', (status) => {
178
+ this.addSub('ci:mocha:test:finish', ({ status, hasBeenRetried }) => {
179
179
  const store = storage.getStore()
180
180
  const span = store?.span
181
181
 
182
182
  if (span) {
183
183
  span.setTag(TEST_STATUS, status)
184
-
184
+ if (hasBeenRetried) {
185
+ span.setTag(TEST_IS_RETRY, 'true')
186
+ }
185
187
  span.finish()
186
188
  this.telemetry.ciVisEvent(
187
189
  TELEMETRY_EVENT_FINISHED,
@@ -204,8 +206,8 @@ class MochaPlugin extends CiPlugin {
204
206
 
205
207
  this.addSub('ci:mocha:test:error', (err) => {
206
208
  const store = storage.getStore()
207
- if (err && store && store.span) {
208
- const span = store.span
209
+ const span = store?.span
210
+ if (err && span) {
209
211
  if (err.constructor.name === 'Pending' && !this.forbidPending) {
210
212
  span.setTag(TEST_STATUS, 'skip')
211
213
  } else {
@@ -215,6 +217,25 @@ class MochaPlugin extends CiPlugin {
215
217
  }
216
218
  })
217
219
 
220
+ this.addSub('ci:mocha:test:retry', (isFirstAttempt) => {
221
+ const store = storage.getStore()
222
+ const span = store?.span
223
+ if (span) {
224
+ span.setTag(TEST_STATUS, 'fail')
225
+ if (!isFirstAttempt) {
226
+ span.setTag(TEST_IS_RETRY, 'true')
227
+ }
228
+
229
+ span.finish()
230
+ this.telemetry.ciVisEvent(
231
+ TELEMETRY_EVENT_FINISHED,
232
+ 'test',
233
+ { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] }
234
+ )
235
+ finishAllTraceSpans(span)
236
+ }
237
+ })
238
+
218
239
  this.addSub('ci:mocha:test:parameterize', ({ title, params }) => {
219
240
  this._testTitleToParams[title] = params
220
241
  })
@@ -7,6 +7,7 @@ const { storage } = require('../../datadog-core')
7
7
  const services = require('./services')
8
8
  const Sampler = require('../../dd-trace/src/sampler')
9
9
  const { MEASURED } = require('../../../ext/tags')
10
+ const { estimateTokens } = require('./token-estimator')
10
11
 
11
12
  // String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
12
13
  const RE_NEWLINE = /\n/g
@@ -15,14 +16,17 @@ const RE_TAB = /\t/g
15
16
  // TODO: In the future we should refactor config.js to make it requirable
16
17
  let MAX_TEXT_LEN = 128
17
18
 
18
- let encodingForModel
19
- try {
20
- // eslint-disable-next-line import/no-extraneous-dependencies
21
- encodingForModel = require('tiktoken').encoding_for_model
22
- } catch {
23
- // we will use token count estimations in this case
19
+ function safeRequire (path) {
20
+ try {
21
+ // eslint-disable-next-line import/no-extraneous-dependencies
22
+ return require(path)
23
+ } catch {
24
+ return null
25
+ }
24
26
  }
25
27
 
28
+ const encodingForModel = safeRequire('tiktoken')?.encoding_for_model
29
+
26
30
  class OpenApiPlugin extends TracingPlugin {
27
31
  static get id () { return 'openai' }
28
32
  static get operation () { return 'request' }
@@ -305,6 +309,7 @@ class OpenApiPlugin extends TracingPlugin {
305
309
  }
306
310
 
307
311
  sendLog (methodName, span, tags, store, error) {
312
+ if (!store) return
308
313
  if (!Object.keys(store).length) return
309
314
  if (!this.sampler.isSampled()) return
310
315
 
@@ -325,9 +330,22 @@ function countPromptTokens (methodName, payload, model) {
325
330
  const messages = payload.messages
326
331
  for (const message of messages) {
327
332
  const content = message.content
328
- const { tokens, estimated } = countTokens(content, model)
329
- promptTokens += tokens
330
- promptEstimated = estimated
333
+ if (typeof content === 'string') {
334
+ const { tokens, estimated } = countTokens(content, model)
335
+ promptTokens += tokens
336
+ promptEstimated = estimated
337
+ } else if (Array.isArray(content)) {
338
+ for (const c of content) {
339
+ if (c.type === 'text') {
340
+ const { tokens, estimated } = countTokens(c.text, model)
341
+ promptTokens += tokens
342
+ promptEstimated = estimated
343
+ }
344
+ // unsupported token computation for image_url
345
+ // as even though URL is a string, its true token count
346
+ // is based on the image itself, something onerous to do client-side
347
+ }
348
+ }
331
349
  }
332
350
  } else if (methodName === 'completions.create') {
333
351
  let prompt = payload.prompt
@@ -382,25 +400,6 @@ function countTokens (content, model) {
382
400
  }
383
401
  }
384
402
 
385
- // If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
386
- // Approximate using the following assumptions:
387
- // * English text
388
- // * 1 token ~= 4 chars
389
- // * 1 token ~= ¾ words
390
- function estimateTokens (content) {
391
- let estimatedTokens = 0
392
- if (typeof content === 'string') {
393
- const estimation1 = content.length / 4
394
-
395
- const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
396
- const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
397
- estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
398
- } else if (Array.isArray(content) && typeof content[0] === 'number') {
399
- estimatedTokens = content.length
400
- }
401
- return estimatedTokens
402
- }
403
-
404
403
  function createEditRequestExtraction (tags, payload, store) {
405
404
  const instruction = payload.instruction
406
405
  tags['openai.request.instruction'] = instruction
@@ -418,7 +417,7 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
418
417
  store.messages = payload.messages
419
418
  for (let i = 0; i < payload.messages.length; i++) {
420
419
  const message = payload.messages[i]
421
- tags[`openai.request.messages.${i}.content`] = truncateText(message.content)
420
+ tagChatCompletionRequestContent(message.content, i, tags)
422
421
  tags[`openai.request.messages.${i}.role`] = message.role
423
422
  tags[`openai.request.messages.${i}.name`] = message.name
424
423
  tags[`openai.request.messages.${i}.finish_reason`] = message.finish_reason
@@ -707,7 +706,7 @@ function commonCreateResponseExtraction (tags, body, store, methodName) {
707
706
  for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
708
707
  const choice = body.choices[choiceIdx]
709
708
 
710
- // logprobs can be nullm and we still want to tag it as 'returned' even when set to 'null'
709
+ // logprobs can be null and we still want to tag it as 'returned' even when set to 'null'
711
710
  const specifiesLogProb = Object.keys(choice).indexOf('logprobs') !== -1
712
711
 
713
712
  tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
@@ -781,6 +780,7 @@ function truncateApiKey (apiKey) {
781
780
  */
782
781
  function truncateText (text) {
783
782
  if (!text) return
783
+ if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
784
784
 
785
785
  text = text
786
786
  .replace(RE_NEWLINE, '\\n')
@@ -793,6 +793,28 @@ function truncateText (text) {
793
793
  return text
794
794
  }
795
795
 
796
+ function tagChatCompletionRequestContent (contents, messageIdx, tags) {
797
+ if (typeof contents === 'string') {
798
+ tags[`openai.request.messages.${messageIdx}.content`] = contents
799
+ } else if (Array.isArray(contents)) {
800
+ // content can also be an array of objects
801
+ // which represent text input or image url
802
+ for (const contentIdx in contents) {
803
+ const content = contents[contentIdx]
804
+ const type = content.type
805
+ tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.type`] = content.type
806
+ if (type === 'text') {
807
+ tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.text`] = truncateText(content.text)
808
+ } else if (type === 'image_url') {
809
+ tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.image_url.url`] =
810
+ truncateText(content.image_url.url)
811
+ }
812
+ // unsupported type otherwise, won't be tagged
813
+ }
814
+ }
815
+ // unsupported type otherwise, won't be tagged
816
+ }
817
+
796
818
  // The server almost always responds with JSON
797
819
  function coerceResponseBody (body, methodName) {
798
820
  switch (methodName) {
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ // If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
4
+ // Approximate using the following assumptions:
5
+ // * English text
6
+ // * 1 token ~= 4 chars
7
+ // * 1 token ~= ¾ words
8
+ module.exports.estimateTokens = function (content) {
9
+ let estimatedTokens = 0
10
+ if (typeof content === 'string') {
11
+ const estimation1 = content.length / 4
12
+
13
+ const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
14
+ const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
15
+ estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
16
+ } else if (Array.isArray(content) && typeof content[0] === 'number') {
17
+ estimatedTokens = content.length
18
+ }
19
+ return estimatedTokens
20
+ }
@@ -0,0 +1,156 @@
1
+ const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
2
+ const { storage } = require('../../datadog-core')
3
+
4
+ const {
5
+ TEST_STATUS,
6
+ finishAllTraceSpans,
7
+ getTestSuitePath,
8
+ getTestSuiteCommonTags,
9
+ TEST_SOURCE_FILE
10
+ } = require('../../dd-trace/src/plugins/util/test')
11
+ const { COMPONENT } = require('../../dd-trace/src/constants')
12
+
13
+ // Milliseconds that we subtract from the error test duration
14
+ // so that they do not overlap with the following test
15
+ // This is because there's some loss of resolution.
16
+ const MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION = 5
17
+
18
+ class VitestPlugin extends CiPlugin {
19
+ static get id () {
20
+ return 'vitest'
21
+ }
22
+
23
+ constructor (...args) {
24
+ super(...args)
25
+
26
+ this.taskToFinishTime = new WeakMap()
27
+
28
+ this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath }) => {
29
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
30
+ const store = storage.getStore()
31
+ const span = this.startTestSpan(
32
+ testName,
33
+ testSuite,
34
+ this.testSuiteSpan,
35
+ {
36
+ [TEST_SOURCE_FILE]: testSuite
37
+ }
38
+ )
39
+
40
+ this.enter(span, store)
41
+ })
42
+
43
+ this.addSub('ci:vitest:test:finish-time', ({ status, task }) => {
44
+ const store = storage.getStore()
45
+ const span = store?.span
46
+
47
+ // we store the finish time to finish at a later hook
48
+ // this is because the test might fail at a `afterEach` hook
49
+ if (span) {
50
+ span.setTag(TEST_STATUS, status)
51
+ this.taskToFinishTime.set(task, span._getTime())
52
+ }
53
+ })
54
+
55
+ this.addSub('ci:vitest:test:pass', ({ task }) => {
56
+ const store = storage.getStore()
57
+ const span = store?.span
58
+
59
+ if (span) {
60
+ span.setTag(TEST_STATUS, 'pass')
61
+ span.finish(this.taskToFinishTime.get(task))
62
+ finishAllTraceSpans(span)
63
+ }
64
+ })
65
+
66
+ this.addSub('ci:vitest:test:error', ({ duration, error }) => {
67
+ const store = storage.getStore()
68
+ const span = store?.span
69
+
70
+ if (span) {
71
+ span.setTag(TEST_STATUS, 'fail')
72
+
73
+ if (error) {
74
+ span.setTag('error', error)
75
+ }
76
+ span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
77
+ finishAllTraceSpans(span)
78
+ }
79
+ })
80
+
81
+ this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => {
82
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
83
+ this.startTestSpan(
84
+ testName,
85
+ testSuite,
86
+ this.testSuiteSpan,
87
+ {
88
+ [TEST_SOURCE_FILE]: testSuite,
89
+ [TEST_STATUS]: 'skip'
90
+ }
91
+ ).finish()
92
+ })
93
+
94
+ this.addSub('ci:vitest:test-suite:start', (testSuiteAbsolutePath) => {
95
+ const testSessionSpanContext = this.tracer.extract('text_map', {
96
+ 'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
97
+ 'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
98
+ })
99
+
100
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
101
+ const testSuiteMetadata = getTestSuiteCommonTags(
102
+ this.command,
103
+ this.frameworkVersion,
104
+ testSuite,
105
+ 'vitest'
106
+ )
107
+ const testSuiteSpan = this.tracer.startSpan('vitest.test_suite', {
108
+ childOf: testSessionSpanContext,
109
+ tags: {
110
+ [COMPONENT]: this.constructor.id,
111
+ ...this.testEnvironmentMetadata,
112
+ ...testSuiteMetadata
113
+ }
114
+ })
115
+ const store = storage.getStore()
116
+ this.enter(testSuiteSpan, store)
117
+ this.testSuiteSpan = testSuiteSpan
118
+ })
119
+
120
+ this.addSub('ci:vitest:test-suite:finish', ({ status, onFinish }) => {
121
+ const store = storage.getStore()
122
+ const span = store?.span
123
+ if (span) {
124
+ span.setTag(TEST_STATUS, status)
125
+ span.finish()
126
+ finishAllTraceSpans(span)
127
+ }
128
+ // TODO: too frequent flush - find for method in worker to decrease frequency
129
+ this.tracer._exporter.flush(onFinish)
130
+ })
131
+
132
+ this.addSub('ci:vitest:test-suite:error', ({ error }) => {
133
+ const store = storage.getStore()
134
+ const span = store?.span
135
+ if (span && error) {
136
+ span.setTag('error', error)
137
+ span.setTag(TEST_STATUS, 'fail')
138
+ }
139
+ })
140
+
141
+ this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => {
142
+ this.testSessionSpan.setTag(TEST_STATUS, status)
143
+ this.testModuleSpan.setTag(TEST_STATUS, status)
144
+ if (error) {
145
+ this.testModuleSpan.setTag('error', error)
146
+ this.testSessionSpan.setTag('error', error)
147
+ }
148
+ this.testModuleSpan.finish()
149
+ this.testSessionSpan.finish()
150
+ finishAllTraceSpans(this.testSessionSpan)
151
+ this.tracer._exporter.flush(onFinish)
152
+ })
153
+ }
154
+ }
155
+
156
+ module.exports = VitestPlugin
@@ -3,6 +3,7 @@
3
3
  const path = require('path')
4
4
  const process = require('process')
5
5
  const { calculateDDBasePath } = require('../../util')
6
+ const { getCallSiteList } = require('../stack_trace')
6
7
  const pathLine = {
7
8
  getFirstNonDDPathAndLine,
8
9
  getNodeModulesPaths,
@@ -24,24 +25,6 @@ const EXCLUDED_PATH_PREFIXES = [
24
25
  'async_hooks'
25
26
  ]
26
27
 
27
- function getCallSiteInfo () {
28
- const previousPrepareStackTrace = Error.prepareStackTrace
29
- const previousStackTraceLimit = Error.stackTraceLimit
30
- let callsiteList
31
- Error.stackTraceLimit = 100
32
- try {
33
- Error.prepareStackTrace = function (_, callsites) {
34
- callsiteList = callsites
35
- }
36
- const e = new Error()
37
- e.stack
38
- } finally {
39
- Error.prepareStackTrace = previousPrepareStackTrace
40
- Error.stackTraceLimit = previousStackTraceLimit
41
- }
42
- return callsiteList
43
- }
44
-
45
28
  function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPaths) {
46
29
  if (callsites) {
47
30
  for (let i = 0; i < callsites.length; i++) {
@@ -91,7 +74,7 @@ function isExcluded (callsite, externallyExcludedPaths) {
91
74
  }
92
75
 
93
76
  function getFirstNonDDPathAndLine (externallyExcludedPaths) {
94
- return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo(), externallyExcludedPaths)
77
+ return getFirstNonDDPathAndLineFromCallsites(getCallSiteList(), externallyExcludedPaths)
95
78
  }
96
79
 
97
80
  function getNodeModulesPaths (...paths) {
@@ -4,6 +4,7 @@ const { MANUAL_KEEP } = require('../../../../../ext/tags')
4
4
  const LRU = require('lru-cache')
5
5
  const vulnerabilitiesFormatter = require('./vulnerabilities-formatter')
6
6
  const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
7
+ const standalone = require('../standalone')
7
8
 
8
9
  const VULNERABILITIES_KEY = 'vulnerabilities'
9
10
  const VULNERABILITY_HASHES_MAX_SIZE = 1000
@@ -57,6 +58,9 @@ function sendVulnerabilities (vulnerabilities, rootSpan) {
57
58
  tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend)
58
59
  tags[MANUAL_KEEP] = 'true'
59
60
  span.addTags(tags)
61
+
62
+ standalone.sample(span)
63
+
60
64
  if (!rootSpan) span.finish()
61
65
  }
62
66
  }
@@ -40,7 +40,7 @@ function enable (_config) {
40
40
  graphql.enable()
41
41
 
42
42
  if (_config.appsec.rasp.enabled) {
43
- rasp.enable()
43
+ rasp.enable(_config)
44
44
  }
45
45
 
46
46
  setTemplates(_config)
@@ -1,11 +1,20 @@
1
1
  'use strict'
2
2
 
3
3
  const { storage } = require('../../../datadog-core')
4
+ const web = require('./../plugins/util/web')
4
5
  const addresses = require('./addresses')
5
6
  const { httpClientRequestStart } = require('./channels')
7
+ const { reportStackTrace } = require('./stack_trace')
6
8
  const waf = require('./waf')
7
9
 
8
- function enable () {
10
+ const RULE_TYPES = {
11
+ SSRF: 'ssrf'
12
+ }
13
+
14
+ let config
15
+
16
+ function enable (_config) {
17
+ config = _config
9
18
  httpClientRequestStart.subscribe(analyzeSsrf)
10
19
  }
11
20
 
@@ -24,12 +33,30 @@ function analyzeSsrf (ctx) {
24
33
  [addresses.HTTP_OUTGOING_URL]: url
25
34
  }
26
35
  // TODO: Currently this is only monitoring, we should
27
- // block the request if SSRF attempt and
28
- // generate stack traces
29
- waf.run({ persistent }, req)
36
+ // block the request if SSRF attempt
37
+ const result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
38
+ handleResult(result, req)
39
+ }
40
+
41
+ function getGenerateStackTraceAction (actions) {
42
+ return actions?.generate_stack
43
+ }
44
+
45
+ function handleResult (actions, req) {
46
+ const generateStackTraceAction = getGenerateStackTraceAction(actions)
47
+ if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
48
+ const rootSpan = web.root(req)
49
+ reportStackTrace(
50
+ rootSpan,
51
+ generateStackTraceAction.stack_id,
52
+ config.appsec.stackTrace.maxDepth,
53
+ config.appsec.stackTrace.maxStackTraces
54
+ )
55
+ }
30
56
  }
31
57
 
32
58
  module.exports = {
33
59
  enable,
34
- disable
60
+ disable,
61
+ handleResult
35
62
  }