dd-trace 5.110.0 → 5.112.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 (164) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/README.md +13 -8
  3. package/ci/init.js +21 -2
  4. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  5. package/ext/tags.js +2 -0
  6. package/index.d.ts +34 -1
  7. package/initialize.mjs +5 -6
  8. package/loader-hook.mjs +76 -55
  9. package/package.json +37 -34
  10. package/packages/datadog-instrumentations/src/ai.js +45 -0
  11. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  12. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  13. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  14. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  15. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  16. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  17. package/packages/datadog-instrumentations/src/fs.js +8 -6
  18. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  25. package/packages/datadog-instrumentations/src/jest.js +713 -63
  26. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  27. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  28. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  29. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +13 -2
  31. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  32. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1297 -0
  33. package/packages/datadog-instrumentations/src/vitest-main.js +1594 -0
  34. package/packages/datadog-instrumentations/src/vitest-util.js +254 -0
  35. package/packages/datadog-instrumentations/src/vitest-worker.js +823 -0
  36. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  37. package/packages/datadog-plugin-ai/src/index.js +1 -1
  38. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  39. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +19 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  44. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  45. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  46. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  47. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  48. package/packages/datadog-plugin-cucumber/src/index.js +15 -24
  49. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +360 -14
  50. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  51. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  52. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  53. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  54. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  55. package/packages/datadog-plugin-graphql/src/execute.js +639 -12
  56. package/packages/datadog-plugin-graphql/src/index.js +37 -9
  57. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  58. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  59. package/packages/datadog-plugin-graphql/src/validate.js +17 -6
  60. package/packages/datadog-plugin-http/src/index.js +6 -8
  61. package/packages/datadog-plugin-jest/src/index.js +31 -15
  62. package/packages/datadog-plugin-mocha/src/index.js +40 -15
  63. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  64. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  65. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  66. package/packages/datadog-plugin-openai/src/services.js +2 -2
  67. package/packages/datadog-plugin-playwright/src/index.js +4 -3
  68. package/packages/datadog-plugin-vitest/src/index.js +120 -72
  69. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  70. package/packages/dd-trace/src/aiguard/index.js +9 -14
  71. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  72. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  73. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  74. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  75. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  76. package/packages/dd-trace/src/appsec/channels.js +3 -1
  77. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  78. package/packages/dd-trace/src/appsec/graphql.js +14 -11
  79. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +23 -23
  80. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  81. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  82. package/packages/dd-trace/src/appsec/index.js +10 -1
  83. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  84. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  85. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  86. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  87. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  88. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  89. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  90. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  91. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +5 -2
  92. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  93. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  94. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +6 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  96. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +130 -20
  97. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  98. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  99. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  100. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  101. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  102. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  103. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  104. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  105. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  106. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  107. package/packages/dd-trace/src/config/defaults.js +3 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +71 -57
  109. package/packages/dd-trace/src/config/helper.js +1 -0
  110. package/packages/dd-trace/src/config/index.js +35 -22
  111. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  112. package/packages/dd-trace/src/config/remote_config.js +1 -1
  113. package/packages/dd-trace/src/config/supported-configurations.json +127 -56
  114. package/packages/dd-trace/src/constants.js +7 -0
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  116. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  117. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  118. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  119. package/packages/dd-trace/src/llmobs/index.js +1 -1
  120. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  121. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  122. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  123. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  124. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  125. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  126. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  127. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  128. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  129. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  130. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  131. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  132. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  133. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  135. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  136. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  137. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  138. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  139. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  140. package/packages/dd-trace/src/plugin_manager.js +23 -7
  141. package/packages/dd-trace/src/plugins/ci_plugin.js +159 -10
  142. package/packages/dd-trace/src/plugins/index.js +2 -0
  143. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  144. package/packages/dd-trace/src/plugins/util/test.js +15 -9
  145. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  146. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  147. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  148. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  149. package/packages/dd-trace/src/proxy.js +8 -8
  150. package/packages/dd-trace/src/span_processor.js +5 -6
  151. package/packages/dd-trace/src/span_stats.js +96 -78
  152. package/packages/dd-trace/src/startup-log.js +2 -1
  153. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  154. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  155. package/packages/dd-trace/src/telemetry/index.js +2 -2
  156. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  157. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  158. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  159. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  160. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  161. package/vendor/dist/protobufjs/index.js +1 -1
  162. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  163. package/vendor/dist/shell-quote/index.js +1 -1
  164. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -36,7 +36,7 @@ class WAFContextWrapper {
36
36
  }
37
37
  }
38
38
 
39
- run ({ persistent, ephemeral }, raspRule, req) {
39
+ run ({ persistent, ephemeral }, raspRule, req, rootSpan) {
40
40
  if (this.ddwafContext.disposed) {
41
41
  log.warn('[ASM] Calling run on a disposed context')
42
42
  if (raspRule) {
@@ -156,10 +156,10 @@ class WAFContextWrapper {
156
156
  metrics.wafTimeout = result.timeout
157
157
 
158
158
  if (ruleTriggered) {
159
- Reporter.reportAttack(result, req)
159
+ Reporter.reportAttack(result, req, rootSpan)
160
160
  }
161
161
 
162
- Reporter.reportAttributes(result.attributes, req)
162
+ Reporter.reportAttributes(result.attributes, req, rootSpan)
163
163
 
164
164
  return result
165
165
  } catch (err) {
@@ -171,7 +171,7 @@ class WAFContextWrapper {
171
171
  wafRunFinished.publish({ payload })
172
172
  }
173
173
 
174
- Reporter.reportMetrics(metrics, raspRule, req)
174
+ Reporter.reportMetrics(metrics, raspRule, req, rootSpan)
175
175
  }
176
176
  }
177
177
 
@@ -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
- const processTags = require('../../../process-tags')
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
- const stack = await getStackFromCallFrames(callFrames)
53
-
54
- const { processLocalState } = await getLocalStateForCallFrame(callFrames[0], limits)
55
-
56
- await session.post('Debugger.resume')
57
-
58
- const snapshot = {
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
- if (config.propagateProcessTags?.enabled) {
74
- snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject
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
- breakpointHitChannel.postMessage({ snapshot })
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
+ }
@@ -17,6 +17,7 @@ const {
17
17
 
18
18
  const { getNumFromKnownTests } = require('../../plugins/util/test')
19
19
  const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
20
+ const { validateKnownTestsResponse } = require('../test-optimization-http-cache-schema')
20
21
 
21
22
  const MAX_KNOWN_TESTS_PAGES = 10_000
22
23
 
@@ -52,6 +53,19 @@ function mergeKnownTests (aggregate, page) {
52
53
  return aggregate
53
54
  }
54
55
 
56
+ function parseJsonResponse (rawJson) {
57
+ return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
58
+ }
59
+
60
+ function parseKnownTestsResponse (rawJson, options = {}) {
61
+ const parsedResponse = parseJsonResponse(rawJson)
62
+ if (options.validateRequiredFields) {
63
+ validateKnownTestsResponse(parsedResponse)
64
+ }
65
+ const { data: { attributes: { tests } } } = parsedResponse
66
+ return tests
67
+ }
68
+
55
69
  function getKnownTests ({
56
70
  url,
57
71
  isEvpProxy,
@@ -151,12 +165,12 @@ function fetchFromApi ({
151
165
  options.path = `${evpProxyPrefix}/api/v2/ci/libraries/tests`
152
166
  options.headers['X-Datadog-EVP-Subdomain'] = 'api'
153
167
  } else {
154
- const { apiKey } = getConfig()
155
- if (!apiKey) {
168
+ const { DD_API_KEY } = getConfig()
169
+ if (!DD_API_KEY) {
156
170
  return done(new Error('Known tests were not fetched because Datadog API key is not defined.'))
157
171
  }
158
172
 
159
- options.headers['dd-api-key'] = apiKey
173
+ options.headers['dd-api-key'] = DD_API_KEY
160
174
  }
161
175
 
162
176
  const configurations = {
@@ -211,8 +225,9 @@ function fetchFromApi ({
211
225
  try {
212
226
  totalResponseBytes += res.length
213
227
 
214
- const { data: { attributes } } = JSON.parse(res)
215
- const { tests: pageTests, page_info: responsePageInfo } = attributes
228
+ const parsedResponse = parseJsonResponse(res)
229
+ const pageTests = parseKnownTestsResponse(parsedResponse)
230
+ const { page_info: responsePageInfo } = parsedResponse.data.attributes
216
231
 
217
232
  aggregateTests = mergeKnownTests(aggregateTests, pageTests)
218
233
 
@@ -251,4 +266,4 @@ function fetchFromApi ({
251
266
  fetchPage(null)
252
267
  }
253
268
 
254
- module.exports = { getKnownTests }
269
+ module.exports = { getKnownTests, parseKnownTestsResponse }
@@ -40,7 +40,7 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
40
40
  lookup,
41
41
  protocolVersion,
42
42
  headers,
43
- isTestDynamicInstrumentationEnabled,
43
+ testOptimization,
44
44
  } = config
45
45
 
46
46
  fetchAgentInfo(this._url, (err, agentInfo) => {
@@ -68,7 +68,10 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
68
68
  evpProxyPrefix,
69
69
  })
70
70
  this._codeCoverageReportUrl = this._url
71
- if (isTestDynamicInstrumentationEnabled) {
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
74
+ if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
72
75
  const canFowardLogs = getCanForwardDebuggerLogs(err, agentInfo)
73
76
  if (canFowardLogs) {
74
77
  const DynamicInstrumentationLogsWriter = require('../agentless/di-logs-writer')
@@ -29,7 +29,7 @@ class Writer extends BaseWriter {
29
29
  path: '/api/v2/citestcov',
30
30
  method: 'POST',
31
31
  headers: {
32
- 'dd-api-key': getConfig().apiKey,
32
+ 'dd-api-key': getConfig().DD_API_KEY,
33
33
  ...form.getHeaders(),
34
34
  },
35
35
  timeout: 15_000,
@@ -26,7 +26,7 @@ class DynamicInstrumentationLogsWriter extends BaseWriter {
26
26
  path: '/api/v2/logs',
27
27
  method: 'POST',
28
28
  headers: {
29
- 'dd-api-key': getConfig().apiKey,
29
+ 'dd-api-key': getConfig().DD_API_KEY,
30
30
  'Content-Type': 'application/json',
31
31
  },
32
32
  timeout: this.timeout,
@@ -9,7 +9,8 @@ const CoverageWriter = require('./coverage-writer')
9
9
  class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
10
10
  constructor (config) {
11
11
  super(config)
12
- const { tags, site, DD_CIVISIBILITY_AGENTLESS_URL: url, isTestDynamicInstrumentationEnabled } = config
12
+ const { tags, site, testOptimization } = config
13
+ const { DD_CIVISIBILITY_AGENTLESS_URL: url } = testOptimization
13
14
  // we don't need to request /info because we are using agentless by configuration
14
15
  this._isInitialized = true
15
16
  this._resolveCanUseCiVisProtocol(true)
@@ -23,13 +24,15 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
23
24
 
24
25
  this._codeCoverageReportUrl = url || new URL(`https://ci-intake.${site}`)
25
26
 
26
- if (isTestDynamicInstrumentationEnabled) {
27
+ if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
27
28
  const DynamicInstrumentationLogsWriter = require('./di-logs-writer')
28
29
  this._logsUrl = url || new URL(`https://http-intake.logs.${site}`)
29
30
  this._logsWriter = new DynamicInstrumentationLogsWriter({ url: this._logsUrl, tags })
30
31
  }
31
32
 
32
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
33
36
  // Agentless is always gzip compatible
34
37
  this._isGzipCompatible = true
35
38
  }
@@ -39,6 +42,7 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
39
42
  try {
40
43
  apiUrl = new URL(apiUrl)
41
44
  this._apiUrl = apiUrl
45
+ this._testScreenshotUploadUrl = apiUrl
42
46
  } catch (e) {
43
47
  log.error('Error setting CI exporter api url', e)
44
48
  }
@@ -30,7 +30,7 @@ class Writer extends BaseWriter {
30
30
  path: '/api/v2/citestcycle',
31
31
  method: 'POST',
32
32
  headers: {
33
- 'dd-api-key': getConfig().apiKey,
33
+ 'dd-api-key': getConfig().DD_API_KEY,
34
34
  'Content-Type': 'application/msgpack',
35
35
  },
36
36
  timeout: 15_000,
@@ -1,18 +1,25 @@
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')
8
10
  const { getTestManagementTests: getTestManagementTestsRequest } =
9
11
  require('../test-management/get-test-management-tests')
12
+ const { writeSettingsToCache } = require('../test-optimization-cache')
13
+ const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
10
14
  const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
15
+ const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
11
16
  const log = require('../../log')
12
17
  const BufferingExporter = require('../../exporters/common/buffering-exporter')
13
18
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
14
19
  const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
15
20
 
21
+ const hostname = getHostname()
22
+
16
23
  function getTestConfigurationTags (tags) {
17
24
  if (!tags) {
18
25
  return {}
@@ -35,6 +42,34 @@ function getIsTestSessionTrace (trace) {
35
42
  const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
36
43
  const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
37
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
+
38
73
  class CiVisibilityExporter extends BufferingExporter {
39
74
  constructor (config) {
40
75
  super(config)
@@ -42,10 +77,14 @@ class CiVisibilityExporter extends BufferingExporter {
42
77
  this._coverageTimer = undefined
43
78
  this._logsTimer = undefined
44
79
  this._coverageBuffer = []
80
+ this._testOptimizationHttpCache = new TestOptimizationHttpCache()
45
81
  // The library can use new features like ITR and test suite level visibility
46
82
  // AKA CI Vis Protocol
47
83
  this._canUseCiVisProtocol = false
48
84
 
85
+ this._isTestFailureScreenshotsEnabled =
86
+ Boolean(config?.testOptimization?.DD_TEST_FAILURE_SCREENSHOTS_ENABLED)
87
+
49
88
  const gitUploadTimeoutId = setTimeout(() => {
50
89
  this._resolveGit(new Error('Timeout while uploading git metadata'))
51
90
  }, GIT_UPLOAD_TIMEOUT)
@@ -86,7 +125,7 @@ class CiVisibilityExporter extends BufferingExporter {
86
125
  }
87
126
 
88
127
  shouldRequestSkippableSuites () {
89
- return !!(this._config.isIntelligentTestRunnerEnabled &&
128
+ return !!(this._config.testOptimization.DD_CIVISIBILITY_ITR_ENABLED &&
90
129
  this._canUseCiVisProtocol &&
91
130
  this._libraryConfig?.isSuitesSkippingEnabled)
92
131
  }
@@ -101,7 +140,7 @@ class CiVisibilityExporter extends BufferingExporter {
101
140
  shouldRequestTestManagementTests () {
102
141
  return !!(
103
142
  this._canUseCiVisProtocol &&
104
- this._config.isTestManagementEnabled &&
143
+ this._config.testOptimization.DD_TEST_MANAGEMENT_ENABLED &&
105
144
  this._libraryConfig?.isTestManagementEnabled
106
145
  )
107
146
  }
@@ -133,11 +172,21 @@ class CiVisibilityExporter extends BufferingExporter {
133
172
  if (!this.shouldRequestSkippableSuites()) {
134
173
  return callback(null, [])
135
174
  }
175
+ const requestConfiguration = this.getRequestConfiguration(testConfiguration)
176
+ const cachedSkippableSuites = this._testOptimizationHttpCache.readSkippableSuites({
177
+ testLevel: requestConfiguration.testLevel,
178
+ isCoverageReportUploadEnabled: requestConfiguration.isCoverageReportUploadEnabled,
179
+ })
180
+ if (cachedSkippableSuites !== CACHE_MISS) {
181
+ const { skippableSuites, correlationId, coverage } = cachedSkippableSuites
182
+ return callback(null, skippableSuites, correlationId, coverage)
183
+ }
184
+
136
185
  this._gitUploadPromise.then(gitUploadError => {
137
186
  if (gitUploadError) {
138
187
  return callback(gitUploadError, [])
139
188
  }
140
- getSkippableSuitesRequest(this.getRequestConfiguration(testConfiguration), callback)
189
+ getSkippableSuitesRequest(requestConfiguration, callback)
141
190
  })
142
191
  }
143
192
 
@@ -145,6 +194,10 @@ class CiVisibilityExporter extends BufferingExporter {
145
194
  if (!this.shouldRequestKnownTests()) {
146
195
  return callback(null)
147
196
  }
197
+ const cachedKnownTests = this._testOptimizationHttpCache.readKnownTests()
198
+ if (cachedKnownTests !== CACHE_MISS) {
199
+ return callback(null, cachedKnownTests)
200
+ }
148
201
  getKnownTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
149
202
  }
150
203
 
@@ -152,6 +205,10 @@ class CiVisibilityExporter extends BufferingExporter {
152
205
  if (!this.shouldRequestTestManagementTests()) {
153
206
  return callback(null)
154
207
  }
208
+ const cachedTestManagementTests = this._testOptimizationHttpCache.readTestManagementTests()
209
+ if (cachedTestManagementTests !== CACHE_MISS) {
210
+ return callback(null, cachedTestManagementTests)
211
+ }
155
212
  getTestManagementTestsRequest(this.getRequestConfiguration(testConfiguration), callback)
156
213
  }
157
214
 
@@ -161,13 +218,30 @@ class CiVisibilityExporter extends BufferingExporter {
161
218
  */
162
219
  getLibraryConfiguration (testConfiguration, callback) {
163
220
  const { repositoryUrl } = testConfiguration
164
- this.sendGitMetadata(repositoryUrl)
165
221
  this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
166
222
  if (!canUseCiVisProtocol) {
167
223
  return callback(null, {})
168
224
  }
169
225
  const configuration = this.getRequestConfiguration(testConfiguration)
226
+ const cachedLibraryConfig = this._testOptimizationHttpCache.readSettings()
227
+ if (cachedLibraryConfig !== CACHE_MISS) {
228
+ log.debug('Test Optimization HTTP cache settings found, skipping settings request')
229
+ writeSettingsToCache(cachedLibraryConfig)
230
+ this._libraryConfig = this.filterConfiguration(cachedLibraryConfig)
231
+ const canUseCachedSkippableSuites = !this.shouldRequestSkippableSuites() ||
232
+ this._testOptimizationHttpCache.hasValidSkippableSuites({
233
+ testLevel: configuration.testLevel,
234
+ isCoverageReportUploadEnabled: configuration.isCoverageReportUploadEnabled,
235
+ })
236
+ if (this._libraryConfig.requireGit && !canUseCachedSkippableSuites) {
237
+ this.sendGitMetadata(repositoryUrl)
238
+ } else {
239
+ this._resolveGit()
240
+ }
241
+ return callback(null, this._libraryConfig)
242
+ }
170
243
 
244
+ this.sendGitMetadata(repositoryUrl)
171
245
  getLibraryConfigurationRequest(configuration, (err, libraryConfig) => {
172
246
  /**
173
247
  * **Important**: this._libraryConfig remains empty in testing frameworks
@@ -218,29 +292,32 @@ class CiVisibilityExporter extends BufferingExporter {
218
292
  isImpactedTestsEnabled,
219
293
  isCoverageReportUploadEnabled,
220
294
  } = remoteConfiguration
295
+ const { testOptimization } = this._config
221
296
  return {
222
297
  isCodeCoverageEnabled,
223
298
  isSuitesSkippingEnabled,
224
299
  isItrEnabled,
225
300
  requireGit,
226
- isEarlyFlakeDetectionEnabled: isEarlyFlakeDetectionEnabled && this._config.isEarlyFlakeDetectionEnabled,
301
+ isEarlyFlakeDetectionEnabled:
302
+ isEarlyFlakeDetectionEnabled && testOptimization.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
227
303
  earlyFlakeDetectionNumRetries,
228
304
  earlyFlakeDetectionSlowTestRetries,
229
305
  earlyFlakeDetectionFaultyThreshold,
230
- isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && this._config.isFlakyTestRetriesEnabled,
231
- flakyTestRetriesCount: this._config.flakyTestRetriesCount,
232
- isDiEnabled: isDiEnabled && this._config.isTestDynamicInstrumentationEnabled,
306
+ isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_ENABLED,
307
+ flakyTestRetriesCount: testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_COUNT,
308
+ isDiEnabled: isDiEnabled && testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED,
233
309
  isKnownTestsEnabled,
234
- isTestManagementEnabled: isTestManagementEnabled && this._config.isTestManagementEnabled,
310
+ isTestManagementEnabled: isTestManagementEnabled && testOptimization.DD_TEST_MANAGEMENT_ENABLED,
235
311
  testManagementAttemptToFixRetries:
236
- testManagementAttemptToFixRetries ?? this._config.testManagementAttemptToFixRetries,
237
- isImpactedTestsEnabled: isImpactedTestsEnabled && this._config.isImpactedTestsEnabled,
312
+ testManagementAttemptToFixRetries ?? testOptimization.DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES,
313
+ isImpactedTestsEnabled:
314
+ isImpactedTestsEnabled && testOptimization.DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED,
238
315
  isCoverageReportUploadEnabled,
239
316
  }
240
317
  }
241
318
 
242
319
  sendGitMetadata (repositoryUrl) {
243
- if (!this._config.isGitUploadEnabled) {
320
+ if (!this._config.testOptimization.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED) {
244
321
  return
245
322
  }
246
323
  this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
@@ -297,28 +374,26 @@ class CiVisibilityExporter extends BufferingExporter {
297
374
  const { service, env, version } = this._config
298
375
 
299
376
  return {
300
- ddtags: [
301
- ...(logMessage.ddtags || []),
302
- `${GIT_REPOSITORY_URL}:${gitRepositoryUrl}`,
303
- `${GIT_COMMIT_SHA}:${gitCommitSha}`,
304
- ].join(','),
377
+ ...logMessage,
378
+ ddtags: getLogTags(logMessage, { env, version }, gitRepositoryUrl, gitCommitSha),
305
379
  level: 'error',
306
380
  service,
381
+ hostname,
307
382
  dd: {
308
- ...(logMessage.dd || []),
383
+ ...logMessage.dd,
309
384
  service,
310
385
  env,
311
386
  version,
312
387
  },
313
388
  ddsource: 'dd_debugger',
314
- ...logMessage,
315
389
  }
316
390
  }
317
391
 
318
392
  // DI logs
319
393
  exportDiLogs (testEnvironmentMetadata, logMessage) {
320
394
  // TODO: could we lose logs if it's not initialized?
321
- if (!this._config.isTestDynamicInstrumentationEnabled || !this._isInitialized || !this._canForwardLogs) {
395
+ if (!this._config.testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED ||
396
+ !this._isInitialized || !this._canForwardLogs) {
322
397
  return
323
398
  }
324
399
 
@@ -416,6 +491,41 @@ class CiVisibilityExporter extends BufferingExporter {
416
491
  evpProxyPrefix: this.evpProxyPrefix,
417
492
  }, callback)
418
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
+ }
419
529
  }
420
530
 
421
531
  module.exports = CiVisibilityExporter