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.
- package/LICENSE-3rdparty.csv +1 -0
- package/ext/exporters.d.ts +1 -1
- package/index.d.ts +47 -1
- package/init.js +40 -1
- package/initialize.mjs +8 -5
- package/package.json +24 -20
- package/packages/datadog-core/src/storage/index.js +1 -10
- package/packages/datadog-esbuild/index.js +5 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
- package/packages/datadog-instrumentations/src/cucumber.js +76 -34
- package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
- package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
- package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
- package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
- package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/vitest.js +303 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
- package/packages/datadog-plugin-child_process/src/index.js +1 -1
- package/packages/datadog-plugin-cucumber/src/index.js +24 -1
- package/packages/datadog-plugin-mocha/src/index.js +25 -4
- package/packages/datadog-plugin-openai/src/index.js +52 -30
- package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
- package/packages/datadog-plugin-vitest/src/index.js +156 -0
- package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
- package/packages/dd-trace/src/appsec/index.js +1 -1
- package/packages/dd-trace/src/appsec/rasp.js +32 -5
- package/packages/dd-trace/src/appsec/recommended.json +208 -3
- package/packages/dd-trace/src/appsec/reporter.js +64 -20
- package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
- package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
- package/packages/dd-trace/src/appsec/standalone.js +130 -0
- package/packages/dd-trace/src/appsec/telemetry.js +33 -1
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/config.js +110 -40
- package/packages/dd-trace/src/constants.js +3 -1
- package/packages/dd-trace/src/datastreams/processor.js +2 -1
- package/packages/dd-trace/src/exporters/agent/index.js +2 -2
- package/packages/dd-trace/src/format.js +1 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
- package/packages/dd-trace/src/opentracing/span.js +4 -1
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +5 -1
- package/packages/dd-trace/src/priority_sampler.js +2 -5
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/proxy.js +3 -1
- package/packages/dd-trace/src/rate_limiter.js +2 -2
- package/packages/dd-trace/src/span_stats.js +4 -3
- package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
- package/packages/dd-trace/src/tracer.js +2 -2
- package/packages/dd-trace/src/util.js +6 -1
- 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(
|
|
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
|
|
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,
|
|
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(
|
|
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', ({
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
19
|
-
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|