dd-trace 5.75.0 → 5.77.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 -1
- package/ci/init.js +1 -0
- package/package.json +15 -15
- package/packages/datadog-instrumentations/src/express.js +18 -7
- package/packages/datadog-instrumentations/src/jest.js +16 -16
- package/packages/datadog-instrumentations/src/openai.js +8 -0
- package/packages/datadog-instrumentations/src/playwright.js +47 -0
- package/packages/datadog-instrumentations/src/router.js +9 -8
- package/packages/datadog-instrumentations/src/vitest.js +94 -8
- package/packages/datadog-plugin-cucumber/src/index.js +2 -1
- package/packages/datadog-plugin-grpc/src/util.js +5 -1
- package/packages/datadog-plugin-http/src/client.js +5 -1
- package/packages/datadog-plugin-http2/src/client.js +5 -1
- package/packages/datadog-plugin-openai/src/stream-helpers.js +26 -1
- package/packages/datadog-plugin-openai/src/tracing.js +46 -1
- package/packages/datadog-plugin-vitest/src/index.js +5 -1
- package/packages/dd-trace/src/appsec/api_security_sampler.js +1 -1
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +7 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -3
- package/packages/dd-trace/src/appsec/rasp/lfi.js +23 -9
- package/packages/dd-trace/src/appsec/stack_trace.js +6 -6
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +30 -7
- package/packages/dd-trace/src/config.js +10 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +8 -1
- package/packages/dd-trace/src/debugger/devtools_client/log.js +15 -4
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +5 -1
- package/packages/dd-trace/src/exporters/common/docker.js +1 -1
- package/packages/dd-trace/src/exporters/span-stats/writer.js +4 -5
- package/packages/dd-trace/src/format.js +4 -5
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +9 -6
- package/packages/dd-trace/src/llmobs/plugins/anthropic.js +3 -6
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +3 -2
- package/packages/dd-trace/src/llmobs/plugins/openai.js +216 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +4 -3
- package/packages/dd-trace/src/llmobs/tagger.js +9 -3
- package/packages/dd-trace/src/llmobs/util.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/exposures.js +2 -2
- package/packages/dd-trace/src/opentelemetry/span.js +3 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +3 -1
- package/packages/dd-trace/src/plugins/util/serverless.js +1 -2
- package/packages/dd-trace/src/plugins/util/test.js +16 -1
- package/packages/dd-trace/src/plugins/util/web.js +5 -1
- package/packages/dd-trace/src/profiling/config.js +32 -10
- package/packages/dd-trace/src/proxy.js +2 -2
- package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
- package/packages/dd-trace/src/remote_config/index.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -2
- package/packages/dd-trace/src/span_processor.js +4 -1
- package/packages/dd-trace/src/startup-log.js +24 -38
- package/packages/dd-trace/src/supported-configurations.json +1 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const LLMObsPlugin = require('./base')
|
|
4
4
|
|
|
5
|
+
const allowedParamKeys = new Set([
|
|
6
|
+
'max_output_tokens',
|
|
7
|
+
'temperature',
|
|
8
|
+
'stream',
|
|
9
|
+
'reasoning'
|
|
10
|
+
])
|
|
11
|
+
|
|
5
12
|
function isIterable (obj) {
|
|
6
13
|
if (obj == null) {
|
|
7
14
|
return false
|
|
@@ -19,7 +26,7 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
19
26
|
const methodName = gateResource(normalizeOpenAIResourceName(resource))
|
|
20
27
|
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
21
28
|
|
|
22
|
-
const inputs = ctx.args[0] // completion, chat completion, and
|
|
29
|
+
const inputs = ctx.args[0] // completion, chat completion, embeddings, and responses take one argument
|
|
23
30
|
const operation = getOperation(methodName)
|
|
24
31
|
const kind = operation === 'embedding' ? 'embedding' : 'llm'
|
|
25
32
|
|
|
@@ -53,6 +60,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
53
60
|
this._tagChatCompletion(span, inputs, response, error)
|
|
54
61
|
} else if (operation === 'embedding') {
|
|
55
62
|
this._tagEmbedding(span, inputs, response, error)
|
|
63
|
+
} else if (operation === 'response') {
|
|
64
|
+
this.#tagResponse(span, inputs, response, error)
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
if (!error) {
|
|
@@ -75,19 +84,30 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
75
84
|
const tokenUsage = response.usage
|
|
76
85
|
|
|
77
86
|
if (tokenUsage) {
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
// Responses API uses input_tokens, Chat/Completions use prompt_tokens
|
|
88
|
+
const inputTokens = tokenUsage.input_tokens ?? tokenUsage.prompt_tokens
|
|
89
|
+
if (inputTokens !== undefined) metrics.inputTokens = inputTokens
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// Responses API uses output_tokens, Chat/Completions use completion_tokens
|
|
92
|
+
const outputTokens = tokenUsage.output_tokens ?? tokenUsage.completion_tokens
|
|
93
|
+
if (outputTokens !== undefined) metrics.outputTokens = outputTokens
|
|
83
94
|
|
|
84
95
|
const totalTokens = tokenUsage.total_tokens || (inputTokens + outputTokens)
|
|
85
|
-
if (totalTokens) metrics.totalTokens = totalTokens
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
if (totalTokens !== undefined) metrics.totalTokens = totalTokens
|
|
97
|
+
|
|
98
|
+
// Cache tokens - Responses API uses input_tokens_details, Chat/Completions use prompt_tokens_details
|
|
99
|
+
// For Responses API, always include cache tokens (even if 0)
|
|
100
|
+
// For Chat API, only include if > 0
|
|
101
|
+
if (tokenUsage.input_tokens_details) {
|
|
102
|
+
// Responses API - always include
|
|
103
|
+
const cacheReadTokens = tokenUsage.input_tokens_details.cached_tokens
|
|
104
|
+
if (cacheReadTokens !== undefined) metrics.cacheReadTokens = cacheReadTokens
|
|
105
|
+
} else if (tokenUsage.prompt_tokens_details) {
|
|
106
|
+
// Chat/Completions API - only include if > 0
|
|
107
|
+
const cacheReadTokens = tokenUsage.prompt_tokens_details.cached_tokens
|
|
108
|
+
if (cacheReadTokens) {
|
|
109
|
+
metrics.cacheReadTokens = cacheReadTokens
|
|
110
|
+
}
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
@@ -191,6 +211,183 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
191
211
|
|
|
192
212
|
this._tagger.tagMetadata(span, metadata)
|
|
193
213
|
}
|
|
214
|
+
|
|
215
|
+
#tagResponse (span, inputs, response, error) {
|
|
216
|
+
// Tag metadata - use allowlist approach for request parameters
|
|
217
|
+
|
|
218
|
+
const { input, model, ...parameters } = inputs
|
|
219
|
+
|
|
220
|
+
// Create input messages
|
|
221
|
+
const inputMessages = []
|
|
222
|
+
|
|
223
|
+
// Add system message if instructions exist
|
|
224
|
+
if (inputs.instructions) {
|
|
225
|
+
inputMessages.push({ role: 'system', content: inputs.instructions })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle input - can be string or array of mixed messages
|
|
229
|
+
if (Array.isArray(input)) {
|
|
230
|
+
for (const item of input) {
|
|
231
|
+
if (item.type === 'function_call') {
|
|
232
|
+
// Function call: convert to message with tool_calls
|
|
233
|
+
// Parse arguments if it's a JSON string
|
|
234
|
+
let parsedArgs = item.arguments
|
|
235
|
+
if (typeof parsedArgs === 'string') {
|
|
236
|
+
try {
|
|
237
|
+
parsedArgs = JSON.parse(parsedArgs)
|
|
238
|
+
} catch {
|
|
239
|
+
parsedArgs = {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
inputMessages.push({
|
|
243
|
+
role: 'assistant',
|
|
244
|
+
toolCalls: [{
|
|
245
|
+
toolId: item.call_id,
|
|
246
|
+
name: item.name,
|
|
247
|
+
arguments: parsedArgs,
|
|
248
|
+
type: item.type
|
|
249
|
+
}]
|
|
250
|
+
})
|
|
251
|
+
} else if (item.type === 'function_call_output') {
|
|
252
|
+
// Function output: convert to user message with tool_results
|
|
253
|
+
inputMessages.push({
|
|
254
|
+
role: 'user',
|
|
255
|
+
toolResults: [{
|
|
256
|
+
toolId: item.call_id,
|
|
257
|
+
result: item.output,
|
|
258
|
+
name: item.name || '',
|
|
259
|
+
type: item.type
|
|
260
|
+
}]
|
|
261
|
+
})
|
|
262
|
+
} else if (item.role && item.content) {
|
|
263
|
+
// Regular message
|
|
264
|
+
inputMessages.push({ role: item.role, content: item.content })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// Simple string input
|
|
269
|
+
inputMessages.push({ role: 'user', content: input })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (error) {
|
|
273
|
+
this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create output messages
|
|
278
|
+
const outputMessages = []
|
|
279
|
+
|
|
280
|
+
// Handle output - can be string (streaming) or array of message objects (non-streaming)
|
|
281
|
+
if (typeof response.output === 'string') {
|
|
282
|
+
// Simple text output (streaming)
|
|
283
|
+
outputMessages.push({ role: 'assistant', content: response.output })
|
|
284
|
+
} else if (Array.isArray(response.output)) {
|
|
285
|
+
// Array output - process all items to extract reasoning, messages, and tool calls
|
|
286
|
+
// Non-streaming: array of items (messages, function_calls, or reasoning)
|
|
287
|
+
for (const item of response.output) {
|
|
288
|
+
// Handle reasoning type (reasoning responses)
|
|
289
|
+
if (item.type === 'reasoning') {
|
|
290
|
+
// Extract reasoning text from summary
|
|
291
|
+
let reasoningText = ''
|
|
292
|
+
if (Array.isArray(item.summary) && item.summary.length > 0) {
|
|
293
|
+
const summaryItem = item.summary[0]
|
|
294
|
+
if (summaryItem.type === 'summary_text' && summaryItem.text) {
|
|
295
|
+
reasoningText = summaryItem.text
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
outputMessages.push({
|
|
299
|
+
role: 'reasoning',
|
|
300
|
+
content: reasoningText
|
|
301
|
+
})
|
|
302
|
+
} else if (item.type === 'function_call') {
|
|
303
|
+
// Handle function_call type (responses API tool calls)
|
|
304
|
+
let args = item.arguments
|
|
305
|
+
// Parse arguments if it's a JSON string
|
|
306
|
+
if (typeof args === 'string') {
|
|
307
|
+
try {
|
|
308
|
+
args = JSON.parse(args)
|
|
309
|
+
} catch {
|
|
310
|
+
args = {}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
outputMessages.push({
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
toolCalls: [{
|
|
316
|
+
toolId: item.call_id,
|
|
317
|
+
name: item.name,
|
|
318
|
+
arguments: args,
|
|
319
|
+
type: item.type
|
|
320
|
+
}]
|
|
321
|
+
})
|
|
322
|
+
} else {
|
|
323
|
+
// Handle regular message objects
|
|
324
|
+
const outputMsg = { role: item.role || 'assistant', content: '' }
|
|
325
|
+
|
|
326
|
+
// Extract content from message
|
|
327
|
+
if (Array.isArray(item.content)) {
|
|
328
|
+
// Content is array of content parts
|
|
329
|
+
// For responses API, text content has type 'output_text', not 'text'
|
|
330
|
+
const textParts = item.content
|
|
331
|
+
.filter(c => c.type === 'output_text')
|
|
332
|
+
.map(c => c.text)
|
|
333
|
+
outputMsg.content = textParts.join('')
|
|
334
|
+
} else if (typeof item.content === 'string') {
|
|
335
|
+
outputMsg.content = item.content
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Extract tool calls if present in message.tool_calls
|
|
339
|
+
if (Array.isArray(item.tool_calls)) {
|
|
340
|
+
outputMsg.toolCalls = item.tool_calls.map(tc => {
|
|
341
|
+
let args = tc.function?.arguments || tc.arguments
|
|
342
|
+
// Parse arguments if it's a JSON string
|
|
343
|
+
if (typeof args === 'string') {
|
|
344
|
+
try {
|
|
345
|
+
args = JSON.parse(args)
|
|
346
|
+
} catch {
|
|
347
|
+
args = {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
toolId: tc.id,
|
|
352
|
+
name: tc.function?.name || tc.name,
|
|
353
|
+
arguments: args,
|
|
354
|
+
type: tc.type || 'function_call'
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
outputMessages.push(outputMsg)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (response.output_text) {
|
|
363
|
+
// Fallback: use output_text if available (for simple non-streaming responses without reasoning/tools)
|
|
364
|
+
outputMessages.push({ role: 'assistant', content: response.output_text })
|
|
365
|
+
} else {
|
|
366
|
+
// No output
|
|
367
|
+
outputMessages.push({ role: 'assistant', content: '' })
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this._tagger.tagLLMIO(span, inputMessages, outputMessages)
|
|
371
|
+
|
|
372
|
+
const metadata = Object.entries(parameters).reduce((obj, [key, value]) => {
|
|
373
|
+
if (allowedParamKeys.has(key)) {
|
|
374
|
+
obj[key] = value
|
|
375
|
+
}
|
|
376
|
+
return obj
|
|
377
|
+
}, {})
|
|
378
|
+
|
|
379
|
+
// Add fields from response object (convert numbers to floats)
|
|
380
|
+
if (response.temperature !== undefined) metadata.temperature = Number(response.temperature)
|
|
381
|
+
if (response.top_p !== undefined) metadata.top_p = Number(response.top_p)
|
|
382
|
+
if (response.tool_choice !== undefined) metadata.tool_choice = response.tool_choice
|
|
383
|
+
if (response.truncation !== undefined) metadata.truncation = response.truncation
|
|
384
|
+
if (response.text !== undefined) metadata.text = response.text
|
|
385
|
+
if (response.usage?.output_tokens_details?.reasoning_tokens !== undefined) {
|
|
386
|
+
metadata.reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this._tagger.tagMetadata(span, metadata)
|
|
390
|
+
}
|
|
194
391
|
}
|
|
195
392
|
|
|
196
393
|
// TODO: this will be moved to the APM integration
|
|
@@ -207,13 +404,18 @@ function normalizeOpenAIResourceName (resource) {
|
|
|
207
404
|
// embeddings
|
|
208
405
|
case 'embeddings.create':
|
|
209
406
|
return 'createEmbedding'
|
|
407
|
+
|
|
408
|
+
// responses
|
|
409
|
+
case 'responses.create':
|
|
410
|
+
return 'createResponse'
|
|
411
|
+
|
|
210
412
|
default:
|
|
211
413
|
return resource
|
|
212
414
|
}
|
|
213
415
|
}
|
|
214
416
|
|
|
215
417
|
function gateResource (resource) {
|
|
216
|
-
return ['createCompletion', 'createChatCompletion', 'createEmbedding'].includes(resource)
|
|
418
|
+
return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse'].includes(resource)
|
|
217
419
|
? resource
|
|
218
420
|
: undefined
|
|
219
421
|
}
|
|
@@ -226,6 +428,8 @@ function getOperation (resource) {
|
|
|
226
428
|
return 'chat'
|
|
227
429
|
case 'createEmbedding':
|
|
228
430
|
return 'embedding'
|
|
431
|
+
case 'createResponse':
|
|
432
|
+
return 'response'
|
|
229
433
|
default:
|
|
230
434
|
// should never happen
|
|
231
435
|
return 'unknown'
|
|
@@ -203,8 +203,8 @@ class LLMObsSpanProcessor {
|
|
|
203
203
|
// This function can be reused for other fields if needed
|
|
204
204
|
// Messages, Documents, and Metrics are safeguarded in `llmobs/tagger.js`
|
|
205
205
|
#addObject (obj, carrier) {
|
|
206
|
-
|
|
207
|
-
seenObjects
|
|
206
|
+
// Capture root object by default
|
|
207
|
+
const seenObjects = new WeakSet([obj])
|
|
208
208
|
|
|
209
209
|
const isCircular = value => {
|
|
210
210
|
if (typeof value !== 'object') return false
|
|
@@ -224,7 +224,8 @@ class LLMObsSpanProcessor {
|
|
|
224
224
|
continue
|
|
225
225
|
}
|
|
226
226
|
if (value !== null && typeof value === 'object') {
|
|
227
|
-
|
|
227
|
+
carrier[key] = Array.isArray(value) ? [] : {}
|
|
228
|
+
add(value, carrier[key])
|
|
228
229
|
} else {
|
|
229
230
|
carrier[key] = value
|
|
230
231
|
}
|
|
@@ -292,11 +292,17 @@ class LLMObsTagger {
|
|
|
292
292
|
continue
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const { result, toolId, type } = toolResult
|
|
295
|
+
const { result, toolId, name = '', type } = toolResult
|
|
296
296
|
const toolResultObj = {}
|
|
297
297
|
|
|
298
298
|
const condition1 = this.#tagConditionalString(result, 'Tool result', toolResultObj, 'result')
|
|
299
299
|
const condition2 = this.#tagConditionalString(toolId, 'Tool ID', toolResultObj, 'tool_id')
|
|
300
|
+
// name can be empty string, so always include it
|
|
301
|
+
if (typeof name === 'string') {
|
|
302
|
+
toolResultObj.name = name
|
|
303
|
+
} else {
|
|
304
|
+
this.#handleFailure(`[LLMObs] Expected tool result name to be a string, instead got "${typeof name}"`)
|
|
305
|
+
}
|
|
300
306
|
const condition3 = this.#tagConditionalString(type, 'Tool type', toolResultObj, 'type')
|
|
301
307
|
|
|
302
308
|
if (condition1 && condition2 && condition3) {
|
|
@@ -332,13 +338,13 @@ class LLMObsTagger {
|
|
|
332
338
|
const toolId = message.toolId
|
|
333
339
|
const messageObj = { content }
|
|
334
340
|
|
|
341
|
+
let condition = this.#tagConditionalString(role, 'Message role', messageObj, 'role')
|
|
342
|
+
|
|
335
343
|
const valid = typeof content === 'string'
|
|
336
344
|
if (!valid) {
|
|
337
345
|
this.#handleFailure('Message content must be a string.', 'invalid_io_messages')
|
|
338
346
|
}
|
|
339
347
|
|
|
340
|
-
let condition = this.#tagConditionalString(role, 'Message role', messageObj, 'role')
|
|
341
|
-
|
|
342
348
|
if (toolCalls) {
|
|
343
349
|
const filteredToolCalls = this.#filterToolCalls(toolCalls)
|
|
344
350
|
|
|
@@ -6,7 +6,7 @@ function encodeUnicode (str = '') {
|
|
|
6
6
|
let result = ''
|
|
7
7
|
for (let i = 0; i < str.length; i++) {
|
|
8
8
|
const code = str.charCodeAt(i)
|
|
9
|
-
result += code > 127 ?
|
|
9
|
+
result += code > 127 ? String.raw`\u${code.toString(16).padStart(4, '0')}` : str[i]
|
|
10
10
|
}
|
|
11
11
|
return result
|
|
12
12
|
}
|
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* @typedef {Object} ExposureContext
|
|
30
|
-
* @property {string}
|
|
30
|
+
* @property {string} service - Service name
|
|
31
31
|
* @property {string} [version] - Service version
|
|
32
32
|
* @property {string} [env] - Service environment
|
|
33
33
|
*/
|
|
@@ -127,7 +127,7 @@ class ExposuresWriter extends BaseFFEWriter {
|
|
|
127
127
|
*/
|
|
128
128
|
_buildContext () {
|
|
129
129
|
const context = {
|
|
130
|
-
|
|
130
|
+
service: this._config.service || 'unknown'
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// Only include version and env if they are defined
|
|
@@ -10,7 +10,7 @@ const { timeInputToHrTime } = require('@opentelemetry/core')
|
|
|
10
10
|
const tracer = require('../../')
|
|
11
11
|
const DatadogSpan = require('../opentracing/span')
|
|
12
12
|
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('../constants')
|
|
13
|
-
const { SERVICE_NAME, RESOURCE_NAME } = require('../../../../ext/tags')
|
|
13
|
+
const { SERVICE_NAME, RESOURCE_NAME, SPAN_KIND } = require('../../../../ext/tags')
|
|
14
14
|
const kinds = require('../../../../ext/kinds')
|
|
15
15
|
|
|
16
16
|
const SpanContext = require('./span_context')
|
|
@@ -146,7 +146,8 @@ class Span {
|
|
|
146
146
|
integrationName: parentTracer?._isOtelLibrary ? 'otel.library' : 'otel',
|
|
147
147
|
tags: {
|
|
148
148
|
[SERVICE_NAME]: _tracer._service,
|
|
149
|
-
[RESOURCE_NAME]: spanName
|
|
149
|
+
[RESOURCE_NAME]: spanName,
|
|
150
|
+
[SPAN_KIND]: spanKindNames[kind]
|
|
150
151
|
},
|
|
151
152
|
links
|
|
152
153
|
}, _tracer._debug)
|
|
@@ -34,7 +34,8 @@ const {
|
|
|
34
34
|
getLibraryCapabilitiesTags,
|
|
35
35
|
getPullRequestDiff,
|
|
36
36
|
getModifiedFilesFromDiff,
|
|
37
|
-
getPullRequestBaseBranch
|
|
37
|
+
getPullRequestBaseBranch,
|
|
38
|
+
TEST_IS_TEST_FRAMEWORK_WORKER
|
|
38
39
|
} = require('./util/test')
|
|
39
40
|
const { getRepositoryRoot } = require('./util/git')
|
|
40
41
|
const Plugin = require('./plugin')
|
|
@@ -311,6 +312,7 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
311
312
|
span.parent_id = id(span.parent_id)
|
|
312
313
|
|
|
313
314
|
if (span.name?.startsWith(`${this.constructor.id}.`)) {
|
|
315
|
+
span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
|
|
314
316
|
// augment with git information (since it will not be available in the worker)
|
|
315
317
|
for (const key in this.testEnvironmentMetadata) {
|
|
316
318
|
// CAREFUL: this bypasses the metadata/metrics distinction
|
|
@@ -92,6 +92,8 @@ const CI_APP_ORIGIN = 'ciapp-test'
|
|
|
92
92
|
const JEST_TEST_RUNNER = 'test.jest.test_runner'
|
|
93
93
|
const JEST_DISPLAY_NAME = 'test.jest.display_name'
|
|
94
94
|
|
|
95
|
+
const VITEST_POOL = 'test.vitest.pool'
|
|
96
|
+
|
|
95
97
|
const CUCUMBER_IS_PARALLEL = 'test.cucumber.is_parallel'
|
|
96
98
|
const MOCHA_IS_PARALLEL = 'test.mocha.is_parallel'
|
|
97
99
|
|
|
@@ -130,6 +132,8 @@ const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90
|
|
|
130
132
|
const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100
|
|
131
133
|
const VITEST_WORKER_LOGS_PAYLOAD_CODE = 102
|
|
132
134
|
|
|
135
|
+
const TEST_IS_TEST_FRAMEWORK_WORKER = 'test.is_test_framework_worker'
|
|
136
|
+
|
|
133
137
|
// Library Capabilities Tagging
|
|
134
138
|
const DD_CAPABILITIES_TEST_IMPACT_ANALYSIS = '_dd.library_capabilities.test_impact_analysis'
|
|
135
139
|
const DD_CAPABILITIES_EARLY_FLAKE_DETECTION = '_dd.library_capabilities.early_flake_detection'
|
|
@@ -203,6 +207,7 @@ module.exports = {
|
|
|
203
207
|
TEST_FRAMEWORK_VERSION,
|
|
204
208
|
JEST_TEST_RUNNER,
|
|
205
209
|
JEST_DISPLAY_NAME,
|
|
210
|
+
VITEST_POOL,
|
|
206
211
|
CUCUMBER_IS_PARALLEL,
|
|
207
212
|
MOCHA_IS_PARALLEL,
|
|
208
213
|
TEST_TYPE,
|
|
@@ -223,6 +228,7 @@ module.exports = {
|
|
|
223
228
|
PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
|
|
224
229
|
VITEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
225
230
|
VITEST_WORKER_LOGS_PAYLOAD_CODE,
|
|
231
|
+
TEST_IS_TEST_FRAMEWORK_WORKER,
|
|
226
232
|
TEST_SOURCE_START,
|
|
227
233
|
TEST_SKIPPED_BY_ITR,
|
|
228
234
|
TEST_IS_NEW,
|
|
@@ -621,20 +627,29 @@ function getCodeOwnersFileEntries (rootDir) {
|
|
|
621
627
|
return entries.reverse()
|
|
622
628
|
}
|
|
623
629
|
|
|
630
|
+
const codeOwnersPerFileName = new Map()
|
|
631
|
+
|
|
624
632
|
function getCodeOwnersForFilename (filename, entries) {
|
|
625
633
|
if (!entries) {
|
|
626
634
|
return null
|
|
627
635
|
}
|
|
636
|
+
if (codeOwnersPerFileName.has(filename)) {
|
|
637
|
+
return codeOwnersPerFileName.get(filename)
|
|
638
|
+
}
|
|
628
639
|
for (const entry of entries) {
|
|
629
640
|
try {
|
|
630
641
|
const isResponsible = ignore().add(entry.pattern).ignores(filename)
|
|
631
642
|
if (isResponsible) {
|
|
632
|
-
|
|
643
|
+
const codeOwners = JSON.stringify(entry.owners)
|
|
644
|
+
codeOwnersPerFileName.set(filename, codeOwners)
|
|
645
|
+
return codeOwners
|
|
633
646
|
}
|
|
634
647
|
} catch {
|
|
648
|
+
codeOwnersPerFileName.set(filename, null)
|
|
635
649
|
return null
|
|
636
650
|
}
|
|
637
651
|
}
|
|
652
|
+
codeOwnersPerFileName.set(filename, null)
|
|
638
653
|
return null
|
|
639
654
|
}
|
|
640
655
|
|
|
@@ -577,13 +577,17 @@ function getHeadersToRecord (config) {
|
|
|
577
577
|
return []
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
function isNot500ErrorCode (code) {
|
|
581
|
+
return code < 500
|
|
582
|
+
}
|
|
583
|
+
|
|
580
584
|
function getStatusValidator (config) {
|
|
581
585
|
if (typeof config.validateStatus === 'function') {
|
|
582
586
|
return config.validateStatus
|
|
583
587
|
} else if (config.hasOwnProperty('validateStatus')) {
|
|
584
588
|
log.error('Expected `validateStatus` to be a function.')
|
|
585
589
|
}
|
|
586
|
-
return
|
|
590
|
+
return isNot500ErrorCode
|
|
587
591
|
}
|
|
588
592
|
|
|
589
593
|
const noop = () => {}
|
|
@@ -269,9 +269,27 @@ module.exports = { Config }
|
|
|
269
269
|
function getProfilers ({
|
|
270
270
|
DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS
|
|
271
271
|
}) {
|
|
272
|
-
// First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to
|
|
272
|
+
// First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to space + wall
|
|
273
273
|
// Use a Set to avoid duplicates
|
|
274
|
-
|
|
274
|
+
// NOTE: space profiler is very deliberately in the first position. This way
|
|
275
|
+
// when profilers are stopped sequentially one after the other to create
|
|
276
|
+
// snapshots the space profile won't include memory taken by profiles created
|
|
277
|
+
// before it in the sequence. That memory is ultimately transient and will be
|
|
278
|
+
// released when all profiles are subsequently encoded.
|
|
279
|
+
const profilers = new Set((DD_PROFILING_PROFILERS ?? 'space,wall').split(','))
|
|
280
|
+
|
|
281
|
+
let spaceExplicitlyEnabled = false
|
|
282
|
+
// Add/remove space depending on the value of DD_PROFILING_HEAP_ENABLED
|
|
283
|
+
if (DD_PROFILING_HEAP_ENABLED != null) {
|
|
284
|
+
if (isTrue(DD_PROFILING_HEAP_ENABLED)) {
|
|
285
|
+
if (!profilers.has('space')) {
|
|
286
|
+
profilers.add('space')
|
|
287
|
+
spaceExplicitlyEnabled = true
|
|
288
|
+
}
|
|
289
|
+
} else if (isFalse(DD_PROFILING_HEAP_ENABLED)) {
|
|
290
|
+
profilers.delete('space')
|
|
291
|
+
}
|
|
292
|
+
}
|
|
275
293
|
|
|
276
294
|
// Add/remove wall depending on the value of DD_PROFILING_WALLTIME_ENABLED
|
|
277
295
|
if (DD_PROFILING_WALLTIME_ENABLED != null) {
|
|
@@ -279,19 +297,23 @@ function getProfilers ({
|
|
|
279
297
|
profilers.add('wall')
|
|
280
298
|
} else if (isFalse(DD_PROFILING_WALLTIME_ENABLED)) {
|
|
281
299
|
profilers.delete('wall')
|
|
300
|
+
profilers.delete('cpu') // remove alias too
|
|
282
301
|
}
|
|
283
302
|
}
|
|
284
303
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
const profilersArray = [...profilers]
|
|
305
|
+
// If space was added through DD_PROFILING_HEAP_ENABLED, ensure it is in the
|
|
306
|
+
// first position. Basically, the only way for it not to be in the first
|
|
307
|
+
// position is if it was explicitly specified in a different position in
|
|
308
|
+
// DD_PROFILING_PROFILERS.
|
|
309
|
+
if (spaceExplicitlyEnabled) {
|
|
310
|
+
const spaceIdx = profilersArray.indexOf('space')
|
|
311
|
+
if (spaceIdx > 0) {
|
|
312
|
+
profilersArray.splice(spaceIdx, 1)
|
|
313
|
+
profilersArray.unshift('space')
|
|
291
314
|
}
|
|
292
315
|
}
|
|
293
|
-
|
|
294
|
-
return [...profilers]
|
|
316
|
+
return profilersArray
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
function getExportStrategy (name, options) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const NoopProxy = require('./noop/proxy')
|
|
3
3
|
const DatadogTracer = require('./tracer')
|
|
4
|
-
const
|
|
4
|
+
const getConfig = require('./config')
|
|
5
5
|
const runtimeMetrics = require('./runtime_metrics')
|
|
6
6
|
const log = require('./log')
|
|
7
7
|
const { setStartupLogPluginManager } = require('./startup-log')
|
|
@@ -98,7 +98,7 @@ class Tracer extends NoopProxy {
|
|
|
98
98
|
this._initialized = true
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
const config =
|
|
101
|
+
const config = getConfig(options) // TODO: support dynamic code config
|
|
102
102
|
|
|
103
103
|
if (config.crashtracking.enabled) {
|
|
104
104
|
require('./crashtracking').start(config)
|
|
@@ -16,6 +16,8 @@ module.exports = {
|
|
|
16
16
|
APM_TRACING_LOGS_INJECTION: 1n << 13n,
|
|
17
17
|
APM_TRACING_HTTP_HEADER_TAGS: 1n << 14n,
|
|
18
18
|
APM_TRACING_CUSTOM_TAGS: 1n << 15n,
|
|
19
|
+
ASM_PROCESSOR_OVERRIDES: 1n << 16n,
|
|
20
|
+
ASM_CUSTOM_DATA_SCANNERS: 1n << 17n,
|
|
19
21
|
ASM_EXCLUSION_DATA: 1n << 18n,
|
|
20
22
|
APM_TRACING_ENABLED: 1n << 19n,
|
|
21
23
|
ASM_RASP_SQLI: 1n << 21n,
|
|
@@ -90,6 +90,8 @@ function enableWafUpdate (appsecConfig) {
|
|
|
90
90
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true)
|
|
91
91
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true)
|
|
92
92
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true)
|
|
93
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, true)
|
|
94
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, true)
|
|
93
95
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, true)
|
|
94
96
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true)
|
|
95
97
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, true)
|
|
@@ -129,6 +131,8 @@ function disableWafUpdate () {
|
|
|
129
131
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false)
|
|
130
132
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false)
|
|
131
133
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false)
|
|
134
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, false)
|
|
135
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, false)
|
|
132
136
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, false)
|
|
133
137
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false)
|
|
134
138
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, false)
|
|
@@ -22,7 +22,7 @@ let nativeMetrics = null
|
|
|
22
22
|
let gcObserver = null
|
|
23
23
|
let interval = null
|
|
24
24
|
let client = null
|
|
25
|
-
let lastTime =
|
|
25
|
+
let lastTime = 0
|
|
26
26
|
let lastCpuUsage = null
|
|
27
27
|
let eventLoopDelayObserver = null
|
|
28
28
|
|
|
@@ -103,7 +103,6 @@ module.exports = {
|
|
|
103
103
|
interval = null
|
|
104
104
|
|
|
105
105
|
client = null
|
|
106
|
-
lastTime = 0n
|
|
107
106
|
lastCpuUsage = null
|
|
108
107
|
|
|
109
108
|
gcObserver?.disconnect()
|
|
@@ -339,6 +338,7 @@ function startGCObserver () {
|
|
|
339
338
|
|
|
340
339
|
gcObserver = new PerformanceObserver(list => {
|
|
341
340
|
for (const entry of list.getEntries()) {
|
|
341
|
+
// @ts-expect-error - entry.detail?.kind and entry.kind are not typed
|
|
342
342
|
const type = gcType(entry.detail?.kind || entry.kind)
|
|
343
343
|
const duration = entry.duration * 1_000_000
|
|
344
344
|
|
|
@@ -47,11 +47,14 @@ class SpanProcessor {
|
|
|
47
47
|
this._spanSampler.sample(spanContext)
|
|
48
48
|
this._gitMetadataTagger.tagGitMetadata(spanContext)
|
|
49
49
|
|
|
50
|
+
let isChunkRoot = true
|
|
51
|
+
|
|
50
52
|
for (const span of started) {
|
|
51
53
|
if (span._duration === undefined) {
|
|
52
54
|
active.push(span)
|
|
53
55
|
} else {
|
|
54
|
-
const formattedSpan = format(span)
|
|
56
|
+
const formattedSpan = format(span, isChunkRoot)
|
|
57
|
+
isChunkRoot = false
|
|
55
58
|
this._stats?.onSpanFinished(formattedSpan)
|
|
56
59
|
formatted.push(formattedSpan)
|
|
57
60
|
|