dd-trace 6.13.0 → 6.15.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 (208) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  8. package/ci/test-optimization-validation/package-check.js +1 -1
  9. package/ci/test-optimization-validation/report-writer.js +7 -5
  10. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  11. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  12. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  13. package/index.d.ts +8 -0
  14. package/package.json +13 -10
  15. package/packages/datadog-esbuild/index.js +3 -1
  16. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  17. package/packages/datadog-instrumentations/src/fs.js +24 -20
  18. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  19. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  20. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  21. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  22. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +14 -5
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  34. package/packages/datadog-instrumentations/src/jest.js +166 -45
  35. package/packages/datadog-instrumentations/src/knex.js +76 -0
  36. package/packages/datadog-instrumentations/src/mariadb.js +6 -0
  37. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  38. package/packages/datadog-instrumentations/src/mocha/main.js +66 -20
  39. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  40. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  41. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  42. package/packages/datadog-instrumentations/src/net.js +14 -17
  43. package/packages/datadog-instrumentations/src/next.js +212 -62
  44. package/packages/datadog-instrumentations/src/openai.js +42 -40
  45. package/packages/datadog-instrumentations/src/pg.js +15 -0
  46. package/packages/datadog-instrumentations/src/playwright.js +113 -13
  47. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  48. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  49. package/packages/datadog-instrumentations/src/undici.js +53 -4
  50. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
  51. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  52. package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
  53. package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
  54. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  55. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  56. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  57. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  58. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  59. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  60. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  61. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  62. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  63. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  64. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  65. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  66. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  67. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  68. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  69. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  70. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  71. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  72. package/packages/datadog-plugin-jest/src/index.js +0 -6
  73. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  74. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  75. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  76. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  77. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  78. package/packages/datadog-plugin-next/src/index.js +63 -29
  79. package/packages/datadog-plugin-playwright/src/index.js +167 -27
  80. package/packages/datadog-plugin-undici/src/index.js +27 -41
  81. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  82. package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
  83. package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
  84. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  85. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  86. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  87. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  88. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  89. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  90. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  91. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  92. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  93. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -95
  94. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  95. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  96. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  97. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  100. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
  101. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  102. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  103. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  104. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
  105. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  106. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  107. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  108. package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
  109. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  110. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  111. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  112. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  113. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +180 -0
  114. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  115. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  116. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  117. package/packages/dd-trace/src/config/index.js +73 -13
  118. package/packages/dd-trace/src/config/parsers.js +11 -1
  119. package/packages/dd-trace/src/config/stable.js +3 -3
  120. package/packages/dd-trace/src/config/supported-configurations.json +22 -2
  121. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  122. package/packages/dd-trace/src/debugger/config.js +12 -1
  123. package/packages/dd-trace/src/debugger/constants.js +1 -0
  124. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  125. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +28 -0
  126. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
  127. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  128. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  129. package/packages/dd-trace/src/debugger/index.js +23 -3
  130. package/packages/dd-trace/src/dogstatsd.js +510 -76
  131. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  132. package/packages/dd-trace/src/encode/0.4.js +5 -5
  133. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  134. package/packages/dd-trace/src/evp_proxy/direct.js +3 -25
  135. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  136. package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
  137. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  138. package/packages/dd-trace/src/exporters/agentless/writer.js +103 -119
  139. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  140. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  141. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  142. package/packages/dd-trace/src/exporters/common/proxy.js +52 -0
  143. package/packages/dd-trace/src/exporters/common/request.js +42 -14
  144. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  145. package/packages/dd-trace/src/exporters/common/writer.js +9 -7
  146. package/packages/dd-trace/src/flush.js +6 -3
  147. package/packages/dd-trace/src/id.js +14 -0
  148. package/packages/dd-trace/src/index.js +5 -2
  149. package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
  150. package/packages/dd-trace/src/llmobs/index.js +32 -32
  151. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  152. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  153. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  154. package/packages/dd-trace/src/llmobs/util.js +0 -33
  155. package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
  156. package/packages/dd-trace/src/log/index.js +7 -1
  157. package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
  158. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  159. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  160. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
  161. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  162. package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
  163. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
  164. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
  165. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +5 -0
  166. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  167. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  168. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  169. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  170. package/packages/dd-trace/src/opentracing/propagation/text_map.js +101 -20
  171. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  172. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  173. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  174. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  175. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  176. package/packages/dd-trace/src/plugins/util/test.js +83 -20
  177. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  178. package/packages/dd-trace/src/plugins/util/web.js +4 -9
  179. package/packages/dd-trace/src/priority_sampler.js +4 -0
  180. package/packages/dd-trace/src/profiling/config.js +1 -0
  181. package/packages/dd-trace/src/profiling/oom.js +7 -2
  182. package/packages/dd-trace/src/profiling/profiler.js +23 -13
  183. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  184. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  185. package/packages/dd-trace/src/proxy.js +75 -5
  186. package/packages/dd-trace/src/remote_config/index.js +319 -101
  187. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  188. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  189. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
  190. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
  191. package/packages/dd-trace/src/sampling_rule.js +39 -10
  192. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
  193. package/packages/dd-trace/src/serverless/vercel.js +53 -20
  194. package/packages/dd-trace/src/serverless.js +10 -3
  195. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  196. package/packages/dd-trace/src/span_processor.js +18 -3
  197. package/packages/dd-trace/src/span_stats.js +24 -28
  198. package/packages/dd-trace/src/startup-log.js +3 -2
  199. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  200. package/packages/dd-trace/src/telemetry/send-data.js +23 -20
  201. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  202. package/packages/dd-trace/src/tracer.js +4 -5
  203. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  204. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  205. package/vendor/dist/protobufjs/index.js +1 -1
  206. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  208. package/packages/dd-trace/src/encode/agentless-json.js +0 -210
@@ -16,6 +16,8 @@ const {
16
16
  HTTP_RESPONSE_HEADERS,
17
17
  } = tags
18
18
 
19
+ const UPGRADE_CHANNEL = 'apm:undici:request:upgrade'
20
+
19
21
  // WeakMap to store span context for native undici request objects
20
22
  const requestContexts = new WeakMap()
21
23
 
@@ -33,6 +35,7 @@ class UndiciPlugin extends HttpClientPlugin {
33
35
  this.addSub('undici:request:headers', this.#onNativeRequestHeaders.bind(this))
34
36
  this.addSub('undici:request:trailers', this.#onNativeRequestTrailers.bind(this))
35
37
  this.addSub('undici:request:error', this.#onNativeRequestError.bind(this))
38
+ this.addSub(UPGRADE_CHANNEL, this.#onNativeRequestUpgrade.bind(this))
36
39
  }
37
40
 
38
41
  // ===========================================
@@ -112,30 +115,15 @@ class UndiciPlugin extends HttpClientPlugin {
112
115
  // Store span context for request for later retrieval
113
116
  requestContexts.set(request, {
114
117
  span,
115
- store,
116
- uri,
117
118
  method,
118
119
  })
119
-
120
- // Enter the span context
121
- storage('legacy').enterWith({ ...store, span })
122
120
  }
123
121
 
124
122
  #onNativeRequestBodySent ({ request }) {
125
123
  const ctx = requestContexts.get(request)
126
124
  if (!ctx || ctx.method !== 'CONNECT') return
127
125
 
128
- const { span, store } = ctx
129
-
130
- this.config.hooks.request(span, null, null)
131
-
132
- span.finish()
133
-
134
- requestContexts.delete(request)
135
-
136
- if (store) {
137
- storage('legacy').enterWith(store)
138
- }
126
+ this.#finishNativeRequest(request)
139
127
  }
140
128
 
141
129
  #onNativeRequestHeaders ({ request, response }) {
@@ -160,50 +148,48 @@ class UndiciPlugin extends HttpClientPlugin {
160
148
  }
161
149
 
162
150
  #onNativeRequestTrailers ({ request }) {
163
- const ctx = requestContexts.get(request)
164
- if (!ctx) return
165
-
166
- const { span, store } = ctx
167
-
168
- // Call the request hook if configured
169
- this.config.hooks.request(span, null, null)
170
-
171
- // Finish the span
172
- span.finish()
151
+ this.#finishNativeRequest(request)
152
+ }
173
153
 
174
- // Clean up
175
- requestContexts.delete(request)
154
+ #onNativeRequestError ({ request, error }) {
155
+ this.#finishNativeRequest(request, error)
156
+ }
176
157
 
177
- // Restore parent store
178
- if (store) {
179
- storage('legacy').enterWith(store)
180
- }
158
+ /**
159
+ * @param {{ error?: Error, headers: unknown, request: object, statusCode: number }} context
160
+ */
161
+ #onNativeRequestUpgrade ({ error, headers, request, statusCode }) {
162
+ this.#onNativeRequestHeaders({ request, response: { headers, statusCode } })
163
+ this.#finishNativeRequest(request, error)
181
164
  }
182
165
 
183
- #onNativeRequestError ({ request, error }) {
166
+ /**
167
+ * @param {object} request
168
+ * @param {Error} [error]
169
+ */
170
+ #finishNativeRequest (request, error) {
184
171
  const ctx = requestContexts.get(request)
185
172
  if (!ctx) return
186
173
 
187
- const { span, store } = ctx
174
+ const { span } = ctx
188
175
 
189
176
  // Don't record AbortError as an error - it's user-initiated cancellation
190
177
  if (error && error.name !== 'AbortError') {
191
178
  span.setTag('error', error)
192
179
  }
193
180
 
194
- // Call the request hook if configured
195
- this.config.hooks.request(span, null, null)
181
+ const requestHook = this.config.hooks.request
182
+ if (requestHook !== noop) {
183
+ this.tracer.scope().activate(span, () => {
184
+ requestHook(span, null, null)
185
+ })
186
+ }
196
187
 
197
188
  // Finish the span
198
189
  span.finish()
199
190
 
200
191
  // Clean up
201
192
  requestContexts.delete(request)
202
-
203
- // Restore parent store
204
- if (store) {
205
- storage('legacy').enterWith(store)
206
- }
207
193
  }
208
194
 
209
195
  // ===========================================
@@ -361,13 +361,13 @@ class VitestPlugin extends CiPlugin {
361
361
  [TEST_SOURCE_START]: testStartLine || 1,
362
362
  [TEST_STATUS]: 'skip',
363
363
  [TEST_FINAL_STATUS]: 'skip',
364
- ...(isAttemptToFix ? { [TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX]: 'true' } : {}),
365
- ...(isDisabled ? { [TEST_MANAGEMENT_IS_DISABLED]: 'true' } : {}),
366
- ...(isQuarantined ? { [TEST_MANAGEMENT_IS_QUARANTINED]: 'true' } : {}),
367
- ...(isNew ? { [TEST_IS_NEW]: 'true' } : {}),
368
- ...(isRumActive ? { [TEST_IS_RUM_ACTIVE]: 'true' } : {}),
369
- ...(isTestFrameworkWorker ? { [TEST_IS_TEST_FRAMEWORK_WORKER]: 'true' } : {}),
370
364
  }
365
+ if (isAttemptToFix) extraTags[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] = 'true'
366
+ if (isDisabled) extraTags[TEST_MANAGEMENT_IS_DISABLED] = 'true'
367
+ if (isQuarantined) extraTags[TEST_MANAGEMENT_IS_QUARANTINED] = 'true'
368
+ if (isNew) extraTags[TEST_IS_NEW] = 'true'
369
+ if (isRumActive) extraTags[TEST_IS_RUM_ACTIVE] = 'true'
370
+ if (isTestFrameworkWorker) extraTags[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
371
371
  setBrowserTags(extraTags, {
372
372
  browserDriver,
373
373
  browserName,
@@ -523,7 +523,6 @@ class VitestPlugin extends CiPlugin {
523
523
  this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_FINISHED, 'suite', { library: coverageLibrary })
524
524
  this.telemetry.distribution(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeFiles.length)
525
525
  }
526
- this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
527
526
  testSuiteSpan.finish()
528
527
  finishAllTraceSpans(testSuiteSpan)
529
528
  }
@@ -567,7 +566,6 @@ class VitestPlugin extends CiPlugin {
567
566
  this.addSub('ci:vitest:session:finish', ({
568
567
  status,
569
568
  error,
570
- isTestSessionFinalizationError,
571
569
  testCodeCoverageLinesTotal,
572
570
  isEarlyFlakeDetectionEnabled,
573
571
  isEarlyFlakeDetectionFaulty,
@@ -590,9 +588,6 @@ class VitestPlugin extends CiPlugin {
590
588
  this.testSessionSpan.setTag(TEST_STATUS, status)
591
589
  this.testModuleSpan.setTag(TEST_STATUS, status)
592
590
  if (error) {
593
- if (isTestSessionFinalizationError) {
594
- this.tracer._exporter.setDeferredTestSuiteError?.(error)
595
- }
596
591
  this.testModuleSpan.setTag('error', error)
597
592
  this.testSessionSpan.setTag('error', error)
598
593
  }
@@ -622,7 +617,6 @@ class VitestPlugin extends CiPlugin {
622
617
  if (vitestPool) {
623
618
  this.testSessionSpan.setTag(VITEST_POOL, vitestPool)
624
619
  }
625
- this.tracer._exporter.exportDeferredTestSuiteSpans?.()
626
620
  this.testModuleSpan.finish()
627
621
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
628
622
  this.testSessionSpan.finish()
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { HTTP_CLIENT_IP, HTTP_USERAGENT, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
4
- const clone = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
4
+ const createRfdc = require('../../../../vendor/dist/rfdc')
5
+ const clone = createRfdc({ proto: false, circles: false })
5
6
  const { USER_ID, USER_SESSION_ID } = require('../appsec/addresses')
6
7
  const { getActiveRequest } = require('../appsec/store')
7
8
  const { keepTrace } = require('../priority_sampler')
@@ -208,7 +208,14 @@ function walkContentBlocks (blocks) {
208
208
  function partsToContent (parts, hasImages) {
209
209
  if (!parts.length) return
210
210
  if (hasImages) return parts
211
- return parts.map(p => p.text).join('\n')
211
+ let content = ''
212
+ let isFirstPart = true
213
+ for (const part of parts) {
214
+ if (!isFirstPart) content += '\n'
215
+ content += part.text
216
+ isFirstPart = false
217
+ }
218
+ return content
212
219
  }
213
220
 
214
221
  /**
@@ -322,7 +322,14 @@ function openAIResponseContentToMessageContent (content) {
322
322
 
323
323
  if (!parts.length) return
324
324
  if (hasImages) return parts
325
- return parts.map(part => part.text).join('\n')
325
+ let textContent = ''
326
+ let isFirstPart = true
327
+ for (const part of parts) {
328
+ if (!isFirstPart) textContent += '\n'
329
+ textContent += part.text
330
+ isFirstPart = false
331
+ }
332
+ return textContent
326
333
  }
327
334
 
328
335
  /**
@@ -62,19 +62,26 @@ function convertVercelPromptToMessages (prompt) {
62
62
  if (hasImages) {
63
63
  messages.push({ role: 'user', content: contentParts })
64
64
  } else {
65
- messages.push({ role: 'user', content: contentParts.map(p => p.text).join('\n') })
65
+ let content = ''
66
+ let isFirstPart = true
67
+ for (const part of contentParts) {
68
+ if (!isFirstPart) content += '\n'
69
+ content += part.text
70
+ isFirstPart = false
71
+ }
72
+ messages.push({ role: 'user', content })
66
73
  }
67
74
  break
68
75
  }
69
76
 
70
77
  case 'assistant': {
71
- const textParts = []
78
+ let text
72
79
  const toolCalls = []
73
80
  if (!Array.isArray(msg.content)) break
74
81
 
75
82
  for (const part of msg.content) {
76
83
  if (part.type === 'text') {
77
- textParts.push(part.text)
84
+ text = text === undefined ? part.text : `${text}\n${part.text}`
78
85
  } else if (part.type === 'tool-call') {
79
86
  toolCalls.push({
80
87
  id: part.toolCallId,
@@ -88,8 +95,8 @@ function convertVercelPromptToMessages (prompt) {
88
95
 
89
96
  if (toolCalls.length > 0) {
90
97
  messages.push({ role: 'assistant', tool_calls: toolCalls })
91
- } else if (textParts.length > 0) {
92
- messages.push({ role: 'assistant', content: textParts.join('\n') })
98
+ } else if (text !== undefined) {
99
+ messages.push({ role: 'assistant', content: text })
93
100
  }
94
101
  break
95
102
  }
@@ -161,7 +168,14 @@ function buildTextOutputMessages (inputMessages, text) {
161
168
  function buildOutputMessages (inputMessages, content) {
162
169
  const toolCalls = content.filter(c => c.type === 'tool-call')
163
170
  if (toolCalls.length) return buildToolCallOutputMessages(inputMessages, toolCalls)
164
- const text = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
171
+ let text = ''
172
+ let isFirstTextPart = true
173
+ for (const part of content) {
174
+ if (part.type !== 'text') continue
175
+ if (!isFirstTextPart) text += '\n'
176
+ text += part.text
177
+ isFirstTextPart = false
178
+ }
165
179
  if (text) return buildTextOutputMessages(inputMessages, text)
166
180
  return []
167
181
  }
@@ -5,6 +5,8 @@ const { isSchemaAttribute } = require('../reporter')
5
5
  const appsecTelemetry = require('../telemetry')
6
6
  const sampler = require('./sampler')
7
7
 
8
+ /** @typedef {import('../../opentracing/span')} DatadogSpan */
9
+
8
10
  /**
9
11
  * Map a sampling decision into the corresponding API Security telemetry metric.
10
12
  *
@@ -13,14 +15,14 @@ const sampler = require('./sampler')
13
15
  * - MISSING_ROUTE: missing_route
14
16
  * - SKIP: no metric emitted
15
17
  *
16
- * @param {import('http').IncomingMessage} req
18
+ * @param {DatadogSpan} rootSpan Span the sampling decision was attached to
17
19
  * @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
18
20
  * @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
19
21
  */
20
- function reportRequest (req, samplingDecision, wafResult) {
22
+ function reportRootSpanRequest (rootSpan, samplingDecision, wafResult) {
21
23
  switch (samplingDecision) {
22
24
  case sampler.SamplingDecision.SAMPLE: {
23
- const framework = getFramework(req)
25
+ const framework = getFramework(rootSpan)
24
26
  if (hasSchemaAttributes(wafResult?.attributes)) {
25
27
  appsecTelemetry.incrementApiSecRequestSchemaMetric(framework)
26
28
  } else {
@@ -29,13 +31,26 @@ function reportRequest (req, samplingDecision, wafResult) {
29
31
  break
30
32
  }
31
33
  case sampler.SamplingDecision.MISSING_ROUTE:
32
- appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(req))
34
+ appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(rootSpan))
33
35
  break
34
36
  }
35
37
  }
36
38
 
37
- function getFramework (req) {
38
- return web.root(req)?.context()?.getTag?.('component')
39
+ /**
40
+ * Node HTTP adapter over {@link reportRootSpanRequest}.
41
+ *
42
+ * @param {import('http').IncomingMessage} req
43
+ * @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
44
+ * @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
45
+ */
46
+ function reportRequest (req, samplingDecision, wafResult) {
47
+ if (samplingDecision === sampler.SamplingDecision.SKIP) return
48
+
49
+ reportRootSpanRequest(web.root(req), samplingDecision, wafResult)
50
+ }
51
+
52
+ function getFramework (rootSpan) {
53
+ return rootSpan?.context?.()?.getTag?.('component')
39
54
  }
40
55
 
41
56
  function hasSchemaAttributes (attributes) {
@@ -50,6 +65,8 @@ module.exports = {
50
65
  configure: sampler.configure,
51
66
  disable: sampler.disable,
52
67
  sampleRequest: sampler.sampleRequest,
68
+ sampleRootSpanRequest: sampler.sampleRootSpanRequest,
53
69
  reportRequest,
70
+ reportRootSpanRequest,
54
71
  SamplingDecision: sampler.SamplingDecision,
55
72
  }
@@ -8,6 +8,8 @@ const { keepTrace } = require('../../priority_sampler')
8
8
  const { ASM } = require('../../standalone/product')
9
9
  const { isBlocked } = require('../blocking')
10
10
 
11
+ /** @typedef {import('../../opentracing/span')} DatadogSpan */
12
+
11
13
  const MAX_SIZE = 4096
12
14
 
13
15
  const SamplingDecision = Object.freeze({
@@ -44,77 +46,113 @@ function disable () {
44
46
  }
45
47
 
46
48
  /**
47
- * @param {import('http').IncomingMessage} req
48
- * @param {import('http').ServerResponse} res
49
+ * @param {DatadogSpan} rootSpan Span the sampling decision is attached to
50
+ * @param {object} request
51
+ * @param {string} request.method
52
+ * @param {number|string} request.statusCode
53
+ * @param {string|undefined} request.route Tri-state: a route string, an empty string (still a
54
+ * valid route — dd-trace-js represents the express root path '/' as an empty path segment), or
55
+ * `undefined` meaning no route information at all.
56
+ * @param {boolean} [request.blocked] Whether the response was blocked by ASM
49
57
  * @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
50
58
  * @returns {'sample' | 'missing_route' | 'skip'}
51
59
  */
52
- function sampleRequest (req, res, record = false) {
60
+ function sampleRootSpanRequest (rootSpan, request, record = false) {
53
61
  if (!enabled) return SamplingDecision.SKIP
54
62
 
55
- const rootSpan = web.root(req)
56
63
  if (!rootSpan) return SamplingDecision.SKIP
57
64
 
58
- if (!asmStandaloneEnabled) {
59
- let priority = getSpanPriority(rootSpan)
60
- if (!priority) {
61
- rootSpan._prioritySampler?.sample(rootSpan)
62
- priority = getSpanPriority(rootSpan)
63
- }
65
+ const { method, statusCode, route, blocked = false } = request ?? {}
64
66
 
65
- if (priority === AUTO_REJECT || priority === USER_REJECT) {
66
- return SamplingDecision.SKIP
67
- }
67
+ if (!method || !statusCode) {
68
+ log.warn('[ASM] Unsupported groupkey for API security')
69
+ return SamplingDecision.SKIP
68
70
  }
69
71
 
70
- const resolved = resolveSamplingKey(req, res)
71
- if (!resolved) return SamplingDecision.SKIP
72
+ if (isRejected(rootSpan)) return SamplingDecision.SKIP
72
73
 
73
- if (record && resolved.route === null) {
74
- if (resolved.status === 404 || isBlocked(res)) return SamplingDecision.SKIP
74
+ if (isRoutelessRecord(route, record)) {
75
+ if (isNotFound(statusCode) || blocked) return SamplingDecision.SKIP
75
76
  return SamplingDecision.MISSING_ROUTE
76
77
  }
77
78
 
78
- if (sampledRequests.has(resolved.key)) return SamplingDecision.SKIP
79
+ const key = buildSamplingKey(method, route, statusCode)
80
+ if (sampledRequests.has(key)) return SamplingDecision.SKIP
79
81
 
80
82
  if (asmStandaloneEnabled) {
81
83
  keepTrace(rootSpan, ASM)
82
84
  }
83
85
 
84
86
  if (record) {
85
- sampledRequests.set(resolved.key, undefined)
87
+ sampledRequests.set(key, undefined)
86
88
  }
87
89
 
88
90
  return SamplingDecision.SAMPLE
89
91
  }
90
92
 
91
93
  /**
94
+ * Node HTTP adapter over {@link sampleRootSpanRequest}.
95
+ *
92
96
  * @param {import('http').IncomingMessage} req
93
97
  * @param {import('http').ServerResponse} res
94
- * @returns {boolean} Whether this request's endpoint is currently recorded in the TTL cache.
98
+ * @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
99
+ * @returns {'sample' | 'missing_route' | 'skip'}
95
100
  */
96
- function wasSampled (req, res) {
97
- const resolved = resolveSamplingKey(req, res)
98
- return resolved !== null && sampledRequests.has(resolved.key)
99
- }
101
+ function sampleRequest (req, res, record = false) {
102
+ if (!enabled) return SamplingDecision.SKIP
100
103
 
101
- function resolveSamplingKey (req, res) {
102
- const method = req.method
103
- const status = res.statusCode
104
+ const rootSpan = web.root(req)
105
+ if (!rootSpan) return SamplingDecision.SKIP
104
106
 
105
- if (!method || !status) {
106
- log.warn('[ASM] Unsupported groupkey for API security')
107
- return null
108
- }
107
+ if (isRejected(rootSpan)) return SamplingDecision.SKIP
108
+
109
+ const statusCode = res.statusCode
110
+ const route = getRouteOrEndpoint(web.getContext(req), statusCode)
111
+
112
+ return sampleRootSpanRequest(rootSpan, {
113
+ method: req.method,
114
+ statusCode,
115
+ route,
116
+ blocked: isRoutelessRecord(route, record) && !isNotFound(statusCode) ? isBlocked(res) : false,
117
+ }, record)
118
+ }
109
119
 
110
- const context = web.getContext(req)
111
- const route = getRouteOrEndpoint(context, status)
120
+ /**
121
+ * Whether this is a request with no route information
122
+ *
123
+ * @param {string|undefined} route
124
+ * @param {boolean} record
125
+ * @returns {boolean}
126
+ */
127
+ function isRoutelessRecord (route, record) {
128
+ return record && route === undefined
129
+ }
112
130
 
113
- // route === null signals "no route information at all". An empty string is still a valid
114
- // route (dd-trace-js represents the express root path '/' as an empty path segment).
115
- return { method, status, route, key: method + (route ?? '') + status }
131
+ /**
132
+ * @param {number|string} statusCode
133
+ * @returns {boolean}
134
+ */
135
+ function isNotFound (statusCode) {
136
+ return Number(statusCode) === 404
137
+ }
138
+
139
+ /**
140
+ * @param {string} method
141
+ * @param {string|undefined} route A route string, an empty string (still a valid route), or
142
+ * `undefined`
143
+ * @param {number|string} statusCode
144
+ * @returns {string}
145
+ */
146
+ function buildSamplingKey (method, route, statusCode) {
147
+ return method + (route ?? '') + statusCode
116
148
  }
117
149
 
150
+ /**
151
+ * @param {{ paths?: string[], span?: object }} [context] Web context of the request
152
+ * @param {number|string} statusCode
153
+ * @returns {string|undefined} A route string, an empty string (still a valid route), or
154
+ * `undefined` when no route information is available.
155
+ */
118
156
  function getRouteOrEndpoint (context, statusCode) {
119
157
  // The router plugin populates `context.paths` whenever the framework matched something.
120
158
  // For express's root '/' route the matched path is normalized to '' (see datadog-plugin-router),
@@ -125,23 +163,44 @@ function getRouteOrEndpoint (context, statusCode) {
125
163
  return paths.join('')
126
164
  }
127
165
 
128
- if (statusCode === 404) return null
166
+ if (isNotFound(statusCode)) return
129
167
 
130
168
  const endpoint = context?.span?.context()?.getTag?.('http.endpoint')
131
169
  if (endpoint) return endpoint
170
+ }
132
171
 
133
- return null
172
+ /**
173
+ * Whether the span's trace is dropped, forcing the sampling decision if none was made yet.
174
+ *
175
+ * Always false under ASM standalone: there the trace is kept via `keepTrace(rootSpan, ASM)`
176
+ * regardless of the APM sampling decision.
177
+ *
178
+ * @param {DatadogSpan} rootSpan
179
+ * @returns {boolean}
180
+ */
181
+ function isRejected (rootSpan) {
182
+ if (asmStandaloneEnabled) return false
183
+
184
+ let priority = getSpanPriority(rootSpan)
185
+ if (priority == null) {
186
+ rootSpan._prioritySampler?.sample(rootSpan)
187
+ priority = getSpanPriority(rootSpan)
188
+ }
189
+
190
+ return priority === AUTO_REJECT || priority === USER_REJECT
134
191
  }
135
192
 
136
193
  function getSpanPriority (span) {
137
194
  const spanContext = span.context?.()
138
- return spanContext._sampling?.priority
195
+ const priority = spanContext?._sampling?.priority
196
+
197
+ return priority == null ? priority : Number(priority)
139
198
  }
140
199
 
141
200
  module.exports = {
142
201
  configure,
143
202
  disable,
144
203
  sampleRequest,
145
- wasSampled,
204
+ sampleRootSpanRequest,
146
205
  SamplingDecision,
147
206
  }
@@ -32,11 +32,15 @@ function disable () {
32
32
  disableGraphql()
33
33
  }
34
34
 
35
- function onGraphqlStartResolver ({ abortController, resolverInfo }) {
35
+ /**
36
+ * @param {{ createResolverInfo?: (data: object) => object | null }} data
37
+ */
38
+ function onGraphqlStartResolver (data) {
36
39
  const req = getActiveRequest()
37
40
 
38
41
  if (!req) return
39
42
 
43
+ const resolverInfo = data.createResolverInfo?.(data)
40
44
  if (!resolverInfo || typeof resolverInfo !== 'object') return
41
45
 
42
46
  const result = waf.run({ ephemeral: { [addresses.HTTP_INCOMING_GRAPHQL_RESOLVER]: resolverInfo } }, req)
@@ -46,7 +50,7 @@ function onGraphqlStartResolver ({ abortController, resolverInfo }) {
46
50
  if (requestData?.isInGraphqlRequest) {
47
51
  requestData.blocked = true
48
52
  requestData.wafAction = blockingAction
49
- abortController?.abort()
53
+ data.abortController?.abort()
50
54
  }
51
55
  }
52
56
  }
@@ -178,12 +178,12 @@ class TaintTrackingPlugin extends SourceIastPlugin {
178
178
  addGraphQLSubscriptions () {
179
179
  this.addSub(
180
180
  { channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY },
181
- (data) => {
181
+ ({ rootCtx, args }) => {
182
182
  const iastContext = getIastContext(storage('legacy').getStore())
183
- const source = data.rootCtx?.source
183
+ const source = rootCtx?.source
184
184
  const ranges = source && getRanges(iastContext, source)
185
185
  if (ranges?.length) {
186
- this._taintTrackingHandler(ranges[0].iinfo.type, data.args, null, iastContext)
186
+ this._taintTrackingHandler(ranges[0].iinfo.type, args, null, iastContext)
187
187
  }
188
188
  }
189
189
  )
@@ -242,7 +242,8 @@ function lodashTaintTrackingHandler (message) {
242
242
  const context = getContextDefault()
243
243
  const transactionId = getTransactionId(context)
244
244
  if (transactionId) {
245
- message.result = getLodashTaintedUtilFn(message.operation)(transactionId, message.result, ...message.arguments)
245
+ const taintResult = getLodashTaintedUtilFn(message.operation)
246
+ message.result = taintResult(transactionId, message.result, ...message.arguments)
246
247
  }
247
248
  } catch (e) {
248
249
  log.error('[ASM] Error invoking CSI lodash %s', message.operation, e)
@@ -46,8 +46,14 @@ function filterTags (tags) {
46
46
  }
47
47
 
48
48
  function processTagValue (tags) {
49
- return tags.map(tag => tag.includes(':') ? getSegment(tag, ':', 1) : tag)
50
- .join('_').replaceAll('.', '_')
49
+ let processedTags = ''
50
+ let isFirstTag = true
51
+ for (const tag of tags) {
52
+ if (!isFirstTag) processedTags += '_'
53
+ processedTags += tag.includes(':') ? getSegment(tag, ':', 1) : tag
54
+ isFirstTag = false
55
+ }
56
+ return processedTags.replaceAll('.', '_')
51
57
  }
52
58
 
53
59
  module.exports = {
@@ -2,7 +2,9 @@
2
2
 
3
3
  const log = require('../../../../../log')
4
4
 
5
- const COMMAND_PATTERN = String.raw`^(?:\s*(?:sudo|doas)\s+)?\b\S+\b\s(.*)`
5
+ // `\S+\s` accepts command tokens that end in punctuation. The optional prefix uses horizontal
6
+ // whitespace so multiline matching cannot start inside a line-terminator run.
7
+ const COMMAND_PATTERN = String.raw`^(?:[ \t]*(?:sudo|doas)[ \t]+)?\S+\s([\s\S]*)`
6
8
  const pattern = new RegExp(COMMAND_PATTERN, 'gmi')
7
9
 
8
10
  module.exports = function extractSensitiveRanges (evidence) {