dd-trace 6.0.0 → 6.1.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 -2
- package/index.d.ts +21 -1
- package/package.json +32 -29
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +21 -10
- package/packages/datadog-plugin-graphql/src/index.js +17 -1
- package/packages/datadog-plugin-graphql/src/utils.js +9 -2
- package/packages/datadog-plugin-graphql/src/validate.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +51 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
|
@@ -16,51 +16,65 @@ const chatCompletionsAfterChannel = channel('dd-trace:openai:chat.completions:af
|
|
|
16
16
|
const responsesBeforeChannel = channel('dd-trace:openai:responses:before')
|
|
17
17
|
const responsesAfterChannel = channel('dd-trace:openai:responses:after')
|
|
18
18
|
|
|
19
|
+
let isEnabled = false
|
|
20
|
+
let aiguard
|
|
21
|
+
let opts
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Subscribes AI Guard to OpenAI lifecycle channels.
|
|
21
25
|
*
|
|
22
|
-
* @param {object}
|
|
26
|
+
* @param {object} aiguardInstance
|
|
23
27
|
* @param {boolean} block
|
|
24
|
-
* @returns {() => void}
|
|
25
28
|
*/
|
|
26
|
-
function enable (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function onChatCompletionsBefore (ctx) {
|
|
30
|
-
pushEvaluation(ctx, aiguard, getChatCompletionsInputMessages(ctx.args?.[0]), opts)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function onChatCompletionsAfter (ctx) {
|
|
34
|
-
const inputMessages = getChatCompletionsInputMessages(ctx.args?.[0])
|
|
35
|
-
if (!inputMessages?.length) return
|
|
36
|
-
for (const message of getChatCompletionsOutputMessages(ctx.body)) {
|
|
37
|
-
pushEvaluation(ctx, aiguard, [...inputMessages, message], opts)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
29
|
+
function enable (aiguardInstance, block) {
|
|
30
|
+
if (isEnabled) return
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function onResponsesAfter (ctx) {
|
|
46
|
-
const inputMessages = getResponsesInputMessages(ctx.args?.[0])
|
|
47
|
-
if (!inputMessages?.length) return
|
|
48
|
-
const outputMessages = getResponsesOutputMessages(ctx.body)
|
|
49
|
-
if (!outputMessages.length) return
|
|
50
|
-
pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
|
|
51
|
-
}
|
|
32
|
+
aiguard = aiguardInstance
|
|
33
|
+
opts = { block, source: SOURCE_AUTO, integration: 'openai' }
|
|
52
34
|
|
|
53
35
|
chatCompletionsBeforeChannel.subscribe(onChatCompletionsBefore)
|
|
54
36
|
chatCompletionsAfterChannel.subscribe(onChatCompletionsAfter)
|
|
55
37
|
responsesBeforeChannel.subscribe(onResponsesBefore)
|
|
56
38
|
responsesAfterChannel.subscribe(onResponsesAfter)
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
isEnabled = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function disable () {
|
|
44
|
+
if (!isEnabled) return
|
|
45
|
+
|
|
46
|
+
chatCompletionsBeforeChannel.unsubscribe(onChatCompletionsBefore)
|
|
47
|
+
chatCompletionsAfterChannel.unsubscribe(onChatCompletionsAfter)
|
|
48
|
+
responsesBeforeChannel.unsubscribe(onResponsesBefore)
|
|
49
|
+
responsesAfterChannel.unsubscribe(onResponsesAfter)
|
|
50
|
+
|
|
51
|
+
aiguard = undefined
|
|
52
|
+
opts = undefined
|
|
53
|
+
isEnabled = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onChatCompletionsBefore (ctx) {
|
|
57
|
+
pushEvaluation(ctx, aiguard, getChatCompletionsInputMessages(ctx.args?.[0]), opts)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onChatCompletionsAfter (ctx) {
|
|
61
|
+
const inputMessages = getChatCompletionsInputMessages(ctx.args?.[0])
|
|
62
|
+
if (!inputMessages?.length) return
|
|
63
|
+
for (const message of getChatCompletionsOutputMessages(ctx.body)) {
|
|
64
|
+
pushEvaluation(ctx, aiguard, [...inputMessages, message], opts)
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
function onResponsesBefore (ctx) {
|
|
69
|
+
pushEvaluation(ctx, aiguard, getResponsesInputMessages(ctx.args?.[0]), opts)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onResponsesAfter (ctx) {
|
|
73
|
+
const inputMessages = getResponsesInputMessages(ctx.args?.[0])
|
|
74
|
+
if (!inputMessages?.length) return
|
|
75
|
+
const outputMessages = getResponsesOutputMessages(ctx.body)
|
|
76
|
+
if (!outputMessages.length) return
|
|
77
|
+
pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { enable, disable }
|
|
@@ -11,45 +11,59 @@ const doGenerateAfterChannel = channel('dd-trace:vercel-ai:doGenerate:after')
|
|
|
11
11
|
const doStreamBeforeChannel = channel('dd-trace:vercel-ai:doStream:before')
|
|
12
12
|
const doStreamAfterChannel = channel('dd-trace:vercel-ai:doStream:after')
|
|
13
13
|
|
|
14
|
+
let isEnabled = false
|
|
15
|
+
let aiguard
|
|
16
|
+
let opts
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* Subscribes AI Guard to Vercel AI lifecycle channels.
|
|
16
20
|
*
|
|
17
|
-
* @param {object}
|
|
21
|
+
* @param {object} aiguardInstance
|
|
18
22
|
* @param {boolean} block
|
|
19
|
-
* @returns {() => void}
|
|
20
23
|
*/
|
|
21
|
-
function enable (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function onBefore (ctx) {
|
|
25
|
-
pushEvaluation(ctx, aiguard, convertVercelPromptToMessages(ctx.prompt), opts)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function onGenerateAfter (ctx) {
|
|
29
|
-
const inputMessages = convertVercelPromptToMessages(ctx.prompt)
|
|
30
|
-
if (!inputMessages.length || !ctx.result?.content?.length) return
|
|
31
|
-
|
|
32
|
-
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, ctx.result.content), opts)
|
|
33
|
-
}
|
|
24
|
+
function enable (aiguardInstance, block) {
|
|
25
|
+
if (isEnabled) return
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!inputMessages.length || !ctx.chunks?.length) return
|
|
38
|
-
|
|
39
|
-
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, getStreamContent(ctx.chunks)), opts)
|
|
40
|
-
}
|
|
27
|
+
aiguard = aiguardInstance
|
|
28
|
+
opts = { block, source: SOURCE_AUTO, integration: 'ai' }
|
|
41
29
|
|
|
42
30
|
doGenerateBeforeChannel.subscribe(onBefore)
|
|
43
31
|
doGenerateAfterChannel.subscribe(onGenerateAfter)
|
|
44
32
|
doStreamBeforeChannel.subscribe(onBefore)
|
|
45
33
|
doStreamAfterChannel.subscribe(onStreamAfter)
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
isEnabled = true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function disable () {
|
|
39
|
+
if (!isEnabled) return
|
|
40
|
+
|
|
41
|
+
doGenerateBeforeChannel.unsubscribe(onBefore)
|
|
42
|
+
doGenerateAfterChannel.unsubscribe(onGenerateAfter)
|
|
43
|
+
doStreamBeforeChannel.unsubscribe(onBefore)
|
|
44
|
+
doStreamAfterChannel.unsubscribe(onStreamAfter)
|
|
45
|
+
|
|
46
|
+
aiguard = undefined
|
|
47
|
+
opts = undefined
|
|
48
|
+
isEnabled = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onBefore (ctx) {
|
|
52
|
+
pushEvaluation(ctx, aiguard, convertVercelPromptToMessages(ctx.prompt), opts)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onGenerateAfter (ctx) {
|
|
56
|
+
const inputMessages = convertVercelPromptToMessages(ctx.prompt)
|
|
57
|
+
if (!inputMessages.length || !ctx.result?.content?.length) return
|
|
58
|
+
|
|
59
|
+
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, ctx.result.content), opts)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onStreamAfter (ctx) {
|
|
63
|
+
const inputMessages = convertVercelPromptToMessages(ctx.prompt)
|
|
64
|
+
if (!inputMessages.length || !ctx.chunks?.length) return
|
|
65
|
+
|
|
66
|
+
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, getStreamContent(ctx.chunks)), opts)
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -75,4 +89,4 @@ function getStreamContent (chunks) {
|
|
|
75
89
|
return text ? [{ type: 'text', text }] : []
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
module.exports = { enable }
|
|
92
|
+
module.exports = { enable, disable }
|
|
@@ -72,14 +72,17 @@ function enterInApolloServerCoreRequest () {
|
|
|
72
72
|
|
|
73
73
|
function enterInApolloRequest () {
|
|
74
74
|
const req = getActiveRequest()
|
|
75
|
+
if (!req) return
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
if (requestData) {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
requestData
|
|
81
|
-
|
|
77
|
+
let requestData = graphqlRequestData.get(req)
|
|
78
|
+
if (!requestData) {
|
|
79
|
+
// executeHTTPGraphQLRequest is the GraphQL request boundary, so seed here
|
|
80
|
+
// when no upstream hook (express4 middleware, drainHttpServer) has run.
|
|
81
|
+
requestData = { blocked: false }
|
|
82
|
+
graphqlRequestData.set(req, requestData)
|
|
82
83
|
}
|
|
84
|
+
requestData.isInGraphqlRequest = true
|
|
85
|
+
addSpecificEndpoint(req, specificBlockingTypes.GRAPHQL)
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
|
|
@@ -46,15 +46,6 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
|
|
|
46
46
|
this.analyze({ filter }, store)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
return store
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// mquery analyzes and marks on the same channel: its tracing start both reaches
|
|
54
|
-
// the driver scope and is the first place the filters are available.
|
|
55
|
-
const onStartAndBind = (message) => {
|
|
56
|
-
onStart(message)
|
|
57
|
-
return markExecStore()
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
this.addSub('datadog:mongodb:collection:filter:start', onStart)
|
|
@@ -65,8 +56,10 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
|
|
|
65
56
|
this.addSub('datadog:mongoose:model:filter:start', onStart)
|
|
66
57
|
this.addBind('datadog:mongoose:model:filter:exec', markExecStore)
|
|
67
58
|
|
|
59
|
+
// mquery analyzes filters at the prepare step and marks the execution scope at
|
|
60
|
+
// tracing start, where the deferred driver call runs under the marked store.
|
|
68
61
|
this.addSub('datadog:mquery:filter:prepare', onStart)
|
|
69
|
-
this.addBind('tracing:datadog:mquery:filter:start',
|
|
62
|
+
this.addBind('tracing:datadog:mquery:filter:start', markExecStore)
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
configureSanitizers () {
|
|
@@ -9,6 +9,7 @@ const getDebuggerConfig = require('../../debugger/config')
|
|
|
9
9
|
|
|
10
10
|
const probeIdToResolveBreakpointSet = new Map()
|
|
11
11
|
const probeIdToResolveBreakpointRemove = new Map()
|
|
12
|
+
const drainRequestIdToResolveBreakpointHit = new Map()
|
|
12
13
|
|
|
13
14
|
class TestVisDynamicInstrumentation {
|
|
14
15
|
/**
|
|
@@ -57,6 +58,21 @@ class TestVisDynamicInstrumentation {
|
|
|
57
58
|
]
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Waits until all breakpoint hits already being handled by the DI worker have been posted back.
|
|
63
|
+
*
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
*/
|
|
66
|
+
waitForInFlightBreakpointHits () {
|
|
67
|
+
if (!this.worker) return Promise.resolve()
|
|
68
|
+
|
|
69
|
+
const requestId = randomUUID()
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
drainRequestIdToResolveBreakpointHit.set(requestId, resolve)
|
|
72
|
+
this.breakpointHitChannel.port2.postMessage({ drainRequestId: requestId })
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
isReady () {
|
|
61
77
|
return this._readyPromise
|
|
62
78
|
}
|
|
@@ -130,7 +146,16 @@ class TestVisDynamicInstrumentation {
|
|
|
130
146
|
}
|
|
131
147
|
}).unref?.()
|
|
132
148
|
|
|
133
|
-
this.breakpointHitChannel.port2.on('message', ({ snapshot }) => {
|
|
149
|
+
this.breakpointHitChannel.port2.on('message', ({ snapshot, drainRequestId }) => {
|
|
150
|
+
if (drainRequestId) {
|
|
151
|
+
const resolve = drainRequestIdToResolveBreakpointHit.get(drainRequestId)
|
|
152
|
+
if (resolve) {
|
|
153
|
+
resolve()
|
|
154
|
+
drainRequestIdToResolveBreakpointHit.delete(drainRequestId)
|
|
155
|
+
}
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
const { probe: { id: probeId } } = snapshot
|
|
135
160
|
const onHit = this.onHitBreakpointByProbeId.get(probeId)
|
|
136
161
|
if (onHit) {
|
|
@@ -28,12 +28,20 @@ const {
|
|
|
28
28
|
getStackFromCallFrames,
|
|
29
29
|
} = require('../../../debugger/devtools_client/state')
|
|
30
30
|
const log = require('../../../log')
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
let processTags
|
|
33
|
+
if (config.propagateProcessTags?.enabled) {
|
|
34
|
+
processTags = require('../../../process-tags')
|
|
35
|
+
processTags.initialize()
|
|
36
|
+
}
|
|
32
37
|
|
|
33
38
|
let sessionStarted = false
|
|
39
|
+
let inFlightBreakpointHits = 0
|
|
40
|
+
let isBreakpointHitDrainScheduled = false
|
|
34
41
|
|
|
35
42
|
const breakpointIdToProbe = new Map()
|
|
36
43
|
const probeIdToBreakpointId = new Map()
|
|
44
|
+
const breakpointHitDrainRequests = []
|
|
37
45
|
|
|
38
46
|
const limits = {
|
|
39
47
|
maxReferenceDepth: DEFAULT_MAX_REFERENCE_DEPTH,
|
|
@@ -42,6 +50,55 @@ const limits = {
|
|
|
42
50
|
maxLength: DEFAULT_MAX_LENGTH,
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Remove empty collection arrays before sending snapshots through the Test Optimization logs path.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} value - Snapshot object or sub-object.
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
function removeEmptyCollectionProperties (value) {
|
|
60
|
+
const stack = [value]
|
|
61
|
+
|
|
62
|
+
while (stack.length > 0) {
|
|
63
|
+
const current = stack.pop()
|
|
64
|
+
if (!current || typeof current !== 'object') continue
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(current)) {
|
|
67
|
+
for (const item of current) {
|
|
68
|
+
if (item && typeof item === 'object') {
|
|
69
|
+
stack.push(item)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(current.elements)) {
|
|
76
|
+
if (current.elements.length === 0) {
|
|
77
|
+
delete current.elements
|
|
78
|
+
} else {
|
|
79
|
+
stack.push(current.elements)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(current.entries)) {
|
|
84
|
+
if (current.entries.length === 0) {
|
|
85
|
+
delete current.entries
|
|
86
|
+
} else {
|
|
87
|
+
stack.push(current.entries)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const key of Object.keys(current)) {
|
|
92
|
+
if (key === 'elements' || key === 'entries') continue
|
|
93
|
+
|
|
94
|
+
const child = current[key]
|
|
95
|
+
if (child && typeof child === 'object') {
|
|
96
|
+
stack.push(child)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
45
102
|
session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint], callFrames } }) => {
|
|
46
103
|
const probe = breakpointIdToProbe.get(hitBreakpoint)
|
|
47
104
|
if (!probe) {
|
|
@@ -49,32 +106,47 @@ session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint]
|
|
|
49
106
|
return session.post('Debugger.resume')
|
|
50
107
|
}
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
id: randomUUID(),
|
|
60
|
-
timestamp: Date.now(),
|
|
61
|
-
probe: {
|
|
62
|
-
id: probe.id,
|
|
63
|
-
version: '0',
|
|
64
|
-
location: probe.location,
|
|
65
|
-
},
|
|
66
|
-
captures: {
|
|
67
|
-
lines: { [probe.location.lines[0]]: { locals: processLocalState() } },
|
|
68
|
-
},
|
|
69
|
-
stack,
|
|
70
|
-
language: 'javascript',
|
|
71
|
-
}
|
|
109
|
+
inFlightBreakpointHits++
|
|
110
|
+
try {
|
|
111
|
+
const stack = await getStackFromCallFrames(callFrames)
|
|
112
|
+
|
|
113
|
+
const { processLocalState } = await getLocalStateForCallFrame(callFrames[0], limits)
|
|
114
|
+
|
|
115
|
+
await session.post('Debugger.resume')
|
|
72
116
|
|
|
73
|
-
|
|
74
|
-
|
|
117
|
+
const snapshot = {
|
|
118
|
+
id: randomUUID(),
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
probe: {
|
|
121
|
+
id: probe.id,
|
|
122
|
+
version: 0,
|
|
123
|
+
location: probe.location,
|
|
124
|
+
},
|
|
125
|
+
captures: {
|
|
126
|
+
lines: { [probe.location.lines[0]]: { locals: processLocalState() } },
|
|
127
|
+
},
|
|
128
|
+
stack,
|
|
129
|
+
language: 'javascript',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
removeEmptyCollectionProperties(snapshot.captures)
|
|
133
|
+
|
|
134
|
+
if (processTags) {
|
|
135
|
+
snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
breakpointHitChannel.postMessage({ snapshot })
|
|
139
|
+
} finally {
|
|
140
|
+
inFlightBreakpointHits--
|
|
141
|
+
scheduleBreakpointHitDrain()
|
|
75
142
|
}
|
|
143
|
+
})
|
|
76
144
|
|
|
77
|
-
|
|
145
|
+
breakpointHitChannel.on('message', ({ drainRequestId }) => {
|
|
146
|
+
if (!drainRequestId) return
|
|
147
|
+
|
|
148
|
+
breakpointHitDrainRequests.push(drainRequestId)
|
|
149
|
+
scheduleBreakpointHitDrain()
|
|
78
150
|
})
|
|
79
151
|
|
|
80
152
|
breakpointRemoveChannel.on('message', async (probeId) => {
|
|
@@ -154,3 +226,22 @@ function start () {
|
|
|
154
226
|
sessionStarted = true
|
|
155
227
|
return session.post('Debugger.enable') // return instead of await to reduce number of promises created
|
|
156
228
|
}
|
|
229
|
+
|
|
230
|
+
function drainBreakpointHitRequests () {
|
|
231
|
+
if (inFlightBreakpointHits !== 0) return
|
|
232
|
+
|
|
233
|
+
for (const drainRequestId of breakpointHitDrainRequests) {
|
|
234
|
+
breakpointHitChannel.postMessage({ drainRequestId })
|
|
235
|
+
}
|
|
236
|
+
breakpointHitDrainRequests.length = 0
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scheduleBreakpointHitDrain () {
|
|
240
|
+
if (isBreakpointHitDrainScheduled) return
|
|
241
|
+
|
|
242
|
+
isBreakpointHitDrainScheduled = true
|
|
243
|
+
setImmediate(() => {
|
|
244
|
+
isBreakpointHitDrainScheduled = false
|
|
245
|
+
drainBreakpointHitRequests()
|
|
246
|
+
})
|
|
247
|
+
}
|
|
@@ -68,6 +68,9 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
68
68
|
evpProxyPrefix,
|
|
69
69
|
})
|
|
70
70
|
this._codeCoverageReportUrl = this._url
|
|
71
|
+
// Screenshot media uploads go through the Agent's evp_proxy: the uploader prefixes the
|
|
72
|
+
// path with evpProxyPrefix and sets X-Datadog-EVP-Subdomain: api (see uploadTestScreenshot).
|
|
73
|
+
this._testScreenshotUploadUrl = this._url
|
|
71
74
|
if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
|
|
72
75
|
const canFowardLogs = getCanForwardDebuggerLogs(err, agentInfo)
|
|
73
76
|
if (canFowardLogs) {
|
|
@@ -31,6 +31,8 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
this._apiUrl = url || new URL(`https://api.${site}`)
|
|
34
|
+
// Media uploads (raw bytes + DD-API-KEY) go to the same api.<site> host as the rest of the API.
|
|
35
|
+
this._testScreenshotUploadUrl = this._apiUrl
|
|
34
36
|
// Agentless is always gzip compatible
|
|
35
37
|
this._isGzipCompatible = true
|
|
36
38
|
}
|
|
@@ -40,6 +42,7 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
|
|
|
40
42
|
try {
|
|
41
43
|
apiUrl = new URL(apiUrl)
|
|
42
44
|
this._apiUrl = apiUrl
|
|
45
|
+
this._testScreenshotUploadUrl = apiUrl
|
|
43
46
|
} catch (e) {
|
|
44
47
|
log.error('Error setting CI exporter api url', e)
|
|
45
48
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { hostname: getHostname } = require('node:os')
|
|
3
4
|
const URL = require('url').URL
|
|
4
5
|
|
|
6
|
+
const { version: tracerVersion } = require('../../../../../package.json')
|
|
5
7
|
const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration')
|
|
6
8
|
const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites')
|
|
7
9
|
const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests')
|
|
@@ -10,11 +12,14 @@ const { getTestManagementTests: getTestManagementTestsRequest } =
|
|
|
10
12
|
const { writeSettingsToCache } = require('../test-optimization-cache')
|
|
11
13
|
const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
|
|
12
14
|
const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
|
|
15
|
+
const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
|
|
13
16
|
const log = require('../../log')
|
|
14
17
|
const BufferingExporter = require('../../exporters/common/buffering-exporter')
|
|
15
18
|
const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
|
|
16
19
|
const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
|
|
17
20
|
|
|
21
|
+
const hostname = getHostname()
|
|
22
|
+
|
|
18
23
|
function getTestConfigurationTags (tags) {
|
|
19
24
|
if (!tags) {
|
|
20
25
|
return {}
|
|
@@ -37,6 +42,34 @@ function getIsTestSessionTrace (trace) {
|
|
|
37
42
|
const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
|
|
38
43
|
const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
|
|
39
44
|
|
|
45
|
+
function appendLogTag (tags, key, value) {
|
|
46
|
+
if (value !== undefined) {
|
|
47
|
+
tags.push(`${key}:${value}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getLogTags (logMessage, { env, version }, gitRepositoryUrl, gitCommitSha) {
|
|
52
|
+
const tags = []
|
|
53
|
+
if (Array.isArray(logMessage.ddtags)) {
|
|
54
|
+
for (const tag of logMessage.ddtags) {
|
|
55
|
+
tags.push(tag)
|
|
56
|
+
}
|
|
57
|
+
} else if (logMessage.ddtags) {
|
|
58
|
+
for (const tag of logMessage.ddtags.split(',')) {
|
|
59
|
+
tags.push(tag)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
appendLogTag(tags, 'env', env)
|
|
64
|
+
appendLogTag(tags, 'version', version)
|
|
65
|
+
appendLogTag(tags, 'debugger_version', tracerVersion)
|
|
66
|
+
appendLogTag(tags, 'host_name', hostname)
|
|
67
|
+
appendLogTag(tags, GIT_COMMIT_SHA, gitCommitSha)
|
|
68
|
+
appendLogTag(tags, GIT_REPOSITORY_URL, gitRepositoryUrl)
|
|
69
|
+
|
|
70
|
+
return tags.join(',')
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
class CiVisibilityExporter extends BufferingExporter {
|
|
41
74
|
constructor (config) {
|
|
42
75
|
super(config)
|
|
@@ -49,6 +82,9 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
49
82
|
// AKA CI Vis Protocol
|
|
50
83
|
this._canUseCiVisProtocol = false
|
|
51
84
|
|
|
85
|
+
this._isTestFailureScreenshotsEnabled =
|
|
86
|
+
Boolean(config?.testOptimization?.DD_TEST_FAILURE_SCREENSHOTS_ENABLED)
|
|
87
|
+
|
|
52
88
|
const gitUploadTimeoutId = setTimeout(() => {
|
|
53
89
|
this._resolveGit(new Error('Timeout while uploading git metadata'))
|
|
54
90
|
}, GIT_UPLOAD_TIMEOUT)
|
|
@@ -338,21 +374,18 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
338
374
|
const { service, env, version } = this._config
|
|
339
375
|
|
|
340
376
|
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
`${GIT_REPOSITORY_URL}:${gitRepositoryUrl}`,
|
|
344
|
-
`${GIT_COMMIT_SHA}:${gitCommitSha}`,
|
|
345
|
-
].join(','),
|
|
377
|
+
...logMessage,
|
|
378
|
+
ddtags: getLogTags(logMessage, { env, version }, gitRepositoryUrl, gitCommitSha),
|
|
346
379
|
level: 'error',
|
|
347
380
|
service,
|
|
381
|
+
hostname,
|
|
348
382
|
dd: {
|
|
349
|
-
...
|
|
383
|
+
...logMessage.dd,
|
|
350
384
|
service,
|
|
351
385
|
env,
|
|
352
386
|
version,
|
|
353
387
|
},
|
|
354
388
|
ddsource: 'dd_debugger',
|
|
355
|
-
...logMessage,
|
|
356
389
|
}
|
|
357
390
|
}
|
|
358
391
|
|
|
@@ -458,6 +491,41 @@ class CiVisibilityExporter extends BufferingExporter {
|
|
|
458
491
|
evpProxyPrefix: this.evpProxyPrefix,
|
|
459
492
|
}, callback)
|
|
460
493
|
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Returns whether the exporter can upload test failure screenshots.
|
|
497
|
+
*
|
|
498
|
+
* @returns {boolean}
|
|
499
|
+
*/
|
|
500
|
+
canUploadTestScreenshots () {
|
|
501
|
+
return Boolean(this._testScreenshotUploadUrl) && this._isTestFailureScreenshotsEnabled
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Uploads a single test screenshot to the Test Optimization media intake.
|
|
506
|
+
*
|
|
507
|
+
* @param {object} options - Upload options
|
|
508
|
+
* @param {string} options.filePath - Path to the screenshot file
|
|
509
|
+
* @param {string} options.traceId - Test trace id used as the screenshot key
|
|
510
|
+
* @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
|
|
511
|
+
* @param {number} options.capturedAtMs - Capture time in epoch milliseconds
|
|
512
|
+
* @param {Function} callback - Callback function (err)
|
|
513
|
+
*/
|
|
514
|
+
uploadTestScreenshot ({ filePath, traceId, idempotencyKey, capturedAtMs }, callback) {
|
|
515
|
+
if (!this._testScreenshotUploadUrl) {
|
|
516
|
+
return callback(new Error('Test screenshot upload URL not configured'))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
uploadTestScreenshotRequest({
|
|
520
|
+
filePath,
|
|
521
|
+
traceId,
|
|
522
|
+
idempotencyKey,
|
|
523
|
+
capturedAtMs,
|
|
524
|
+
url: this._testScreenshotUploadUrl,
|
|
525
|
+
isEvpProxy: !!this._isUsingEvpProxy,
|
|
526
|
+
evpProxyPrefix: this.evpProxyPrefix,
|
|
527
|
+
}, callback)
|
|
528
|
+
}
|
|
461
529
|
}
|
|
462
530
|
|
|
463
531
|
module.exports = CiVisibilityExporter
|