dd-trace 5.110.0 → 5.111.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 (115) hide show
  1. package/README.md +13 -8
  2. package/ci/init.js +21 -2
  3. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  4. package/ext/tags.js +2 -0
  5. package/index.d.ts +13 -0
  6. package/initialize.mjs +5 -6
  7. package/loader-hook.mjs +76 -55
  8. package/package.json +9 -9
  9. package/packages/datadog-instrumentations/src/fs.js +8 -6
  10. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  13. package/packages/datadog-instrumentations/src/mongoose.js +14 -2
  14. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
  15. package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
  16. package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
  17. package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
  18. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  22. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +10 -4
  24. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  25. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  26. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  27. package/packages/datadog-plugin-graphql/src/execute.js +627 -11
  28. package/packages/datadog-plugin-graphql/src/index.js +20 -8
  29. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  30. package/packages/datadog-plugin-graphql/src/validate.js +16 -5
  31. package/packages/datadog-plugin-http/src/index.js +6 -8
  32. package/packages/datadog-plugin-jest/src/index.js +1 -1
  33. package/packages/datadog-plugin-mocha/src/index.js +1 -1
  34. package/packages/datadog-plugin-openai/src/services.js +2 -2
  35. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  36. package/packages/datadog-plugin-vitest/src/index.js +69 -63
  37. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  38. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  39. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  40. package/packages/dd-trace/src/appsec/channels.js +3 -1
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  42. package/packages/dd-trace/src/appsec/graphql.js +5 -5
  43. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
  44. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  45. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  46. package/packages/dd-trace/src/appsec/index.js +10 -1
  47. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  48. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  49. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  50. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  51. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  52. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  53. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
  54. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  55. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
  57. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
  59. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  60. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  61. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  62. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  63. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  64. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  65. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  66. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  67. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  68. package/packages/dd-trace/src/config/defaults.js +3 -2
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +61 -57
  70. package/packages/dd-trace/src/config/helper.js +1 -0
  71. package/packages/dd-trace/src/config/index.js +35 -22
  72. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  73. package/packages/dd-trace/src/config/remote_config.js +1 -1
  74. package/packages/dd-trace/src/config/supported-configurations.json +90 -56
  75. package/packages/dd-trace/src/constants.js +7 -0
  76. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  77. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  78. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  79. package/packages/dd-trace/src/llmobs/index.js +1 -1
  80. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  81. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  82. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  83. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  84. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  85. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  87. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  88. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  89. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  90. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  91. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  92. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  93. package/packages/dd-trace/src/plugin_manager.js +7 -7
  94. package/packages/dd-trace/src/plugins/ci_plugin.js +12 -3
  95. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  96. package/packages/dd-trace/src/plugins/util/test.js +11 -9
  97. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  98. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  99. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  100. package/packages/dd-trace/src/proxy.js +8 -8
  101. package/packages/dd-trace/src/span_processor.js +5 -6
  102. package/packages/dd-trace/src/span_stats.js +96 -78
  103. package/packages/dd-trace/src/startup-log.js +2 -1
  104. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  105. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  106. package/packages/dd-trace/src/telemetry/index.js +2 -2
  107. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  108. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  109. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  110. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  111. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  112. package/vendor/dist/protobufjs/index.js +1 -1
  113. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  114. package/vendor/dist/shell-quote/index.js +1 -1
  115. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -41,7 +41,7 @@ function enable (_config) {
41
41
  downstreamAnalysisCount = new WeakMap()
42
42
  responseBodyIgnoredCount = new WeakMap()
43
43
 
44
- const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
44
+ const bodyAnalysisSampleRate = config.appsec.DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE
45
45
  samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
46
46
 
47
47
  if (samplingRate !== bodyAnalysisSampleRate) {
@@ -105,7 +105,7 @@ function recordResponseBodyIgnored (req, tag) {
105
105
  * @returns {boolean} whether downstream response body should be collected for AppSec.
106
106
  */
107
107
  function evaluateResponseBodyCollection (originatingReq, res) {
108
- const maxBytes = config.appsec.apiSecurity.maxDownstreamBodyBytes
108
+ const maxBytes = config.appsec.DD_API_SECURITY_MAX_DOWNSTREAM_BODY_BYTES
109
109
 
110
110
  const mime = extractMimeType(res.headers?.['content-type'])
111
111
  if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
@@ -137,7 +137,7 @@ function shouldSampleBody (req) {
137
137
  globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
138
138
 
139
139
  const currentCount = bodyAnalysisCount.get(req) || 0
140
- if (currentCount >= config.appsec.apiSecurity?.maxDownstreamRequestBodyAnalysis) {
140
+ if (currentCount >= config.appsec.DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS) {
141
141
  return false
142
142
  }
143
143
 
@@ -166,7 +166,7 @@ function isRedirectResponse (res) {
166
166
  * @param {object} ctx http client instrumentation context (mutated).
167
167
  */
168
168
  function planResponseBodyCollection (originatingReq, res, ctx) {
169
- if (!config?.appsec.apiSecurity) {
169
+ if (!config) {
170
170
  return
171
171
  }
172
172
 
@@ -12,7 +12,7 @@ const {
12
12
  const waf = require('./waf')
13
13
  const addresses = require('./addresses')
14
14
  const {
15
- startGraphqlResolve,
15
+ startGraphqlResolver,
16
16
  graphqlMiddlewareChannel,
17
17
  apolloHttpServerChannel,
18
18
  apolloChannel,
@@ -32,7 +32,7 @@ function disable () {
32
32
  disableGraphql()
33
33
  }
34
34
 
35
- function onGraphqlStartResolve ({ context, resolverInfo }) {
35
+ function onGraphqlStartResolver ({ abortController, resolverInfo }) {
36
36
  const req = getActiveRequest()
37
37
 
38
38
  if (!req) return
@@ -46,7 +46,7 @@ function onGraphqlStartResolve ({ context, resolverInfo }) {
46
46
  if (requestData?.isInGraphqlRequest) {
47
47
  requestData.blocked = true
48
48
  requestData.wafAction = blockingAction
49
- context?.abortController?.abort()
49
+ abortController?.abort()
50
50
  }
51
51
  }
52
52
  }
@@ -153,11 +153,11 @@ function disableApollo () {
153
153
  }
154
154
 
155
155
  function enableGraphql () {
156
- startGraphqlResolve.subscribe(onGraphqlStartResolve)
156
+ startGraphqlResolver.subscribe(onGraphqlStartResolver)
157
157
  }
158
158
 
159
159
  function disableGraphql () {
160
- if (startGraphqlResolve.hasSubscribers) startGraphqlResolve.unsubscribe(onGraphqlStartResolve)
160
+ if (startGraphqlResolver.hasSubscribers) startGraphqlResolver.unsubscribe(onGraphqlStartResolver)
161
161
  }
162
162
 
163
163
  module.exports = {
@@ -15,6 +15,20 @@ const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mq
15
15
  const SAFE_OPERATORS = new Set(['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin',
16
16
  '$exists', '$type', '$mod', '$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'])
17
17
 
18
+ // Scope a marked child store for a query's deferred execution so the nested mongodb
19
+ // driver query skips re-analysis. `bindStore` enters the child only for that async
20
+ // scope and restores the parent on its own, so no marker can be stranded for a later
21
+ // request to inherit, and two concurrent queries of the same request each branch from
22
+ // the shared parent rather than from each other.
23
+ function markExecStore () {
24
+ const store = storage('legacy').getStore()
25
+ if (store && !store.nosqlAnalyzed && getIastContext(store)) {
26
+ return { ...store, nosqlAnalyzed: true }
27
+ }
28
+
29
+ return store
30
+ }
31
+
18
32
  class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
19
33
  constructor () {
20
34
  super(NOSQL_MONGODB_INJECTION)
@@ -27,7 +41,7 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
27
41
  // Anything that accesses the storage is context dependent
28
42
  const onStart = ({ filters }) => {
29
43
  const store = storage('legacy').getStore()
30
- if (store && !store.nosqlAnalyzed && filters?.length) {
44
+ if (store && !store.nosqlAnalyzed && filters?.length && getIastContext(store)) {
31
45
  for (const filter of filters) {
32
46
  this.analyze({ filter }, store)
33
47
  }
@@ -36,30 +50,23 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
36
50
  return store
37
51
  }
38
52
 
39
- const onStartAndEnterWithStore = (message) => {
40
- const store = onStart(message || {})
41
- if (store) {
42
- storage('legacy').enterWith({ ...store, nosqlAnalyzed: true, nosqlParentStore: store })
43
- }
44
- }
45
-
46
- // Anything that accesses the storage is context dependent
47
- // eslint-disable-next-line unicorn/consistent-function-scoping
48
- const onFinish = () => {
49
- const store = storage('legacy').getStore()
50
- if (store?.nosqlParentStore) {
51
- storage('legacy').enterWith(store.nosqlParentStore)
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()
53
58
  }
54
59
 
55
60
  this.addSub('datadog:mongodb:collection:filter:start', onStart)
56
61
 
57
- this.addSub('datadog:mongoose:model:filter:start', onStartAndEnterWithStore)
58
- this.addSub('datadog:mongoose:model:filter:finish', onFinish)
62
+ // A mongoose query builds, executes, and reaches the driver in separate async
63
+ // steps. The filters are analyzed at the build, but the marker is bound around
64
+ // the execution channel, where the driver call still runs under the parent store.
65
+ this.addSub('datadog:mongoose:model:filter:start', onStart)
66
+ this.addBind('datadog:mongoose:model:filter:exec', markExecStore)
59
67
 
60
68
  this.addSub('datadog:mquery:filter:prepare', onStart)
61
- this.addSub('tracing:datadog:mquery:filter:start', onStartAndEnterWithStore)
62
- this.addSub('tracing:datadog:mquery:filter:asyncEnd', onFinish)
69
+ this.addBind('tracing:datadog:mquery:filter:start', onStartAndBind)
63
70
  }
64
71
 
65
72
  configureSanitizers () {
@@ -21,10 +21,10 @@ let controlsKeys
21
21
  let hooks
22
22
 
23
23
  function configure (iastConfig) {
24
- if (!iastConfig?.securityControlsConfiguration) return
24
+ if (!iastConfig.DD_IAST_SECURITY_CONTROLS_CONFIGURATION) return
25
25
 
26
26
  try {
27
- controls = parse(iastConfig.securityControlsConfiguration)
27
+ controls = parse(iastConfig.DD_IAST_SECURITY_CONTROLS_CONFIGURATION)
28
28
  if (controls?.size > 0) {
29
29
  hooks = new WeakSet()
30
30
  controlsKeys = [...controls.keys()]
@@ -6,7 +6,8 @@ const { initRequestNamespace, finalizeRequestNamespace, globalNamespace } = requ
6
6
 
7
7
  class Telemetry {
8
8
  configure (config, verbosity) {
9
- const telemetryAndMetricsEnabled = config?.telemetry?.enabled && config.telemetry.metrics
9
+ const telemetryAndMetricsEnabled = config.telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED &&
10
+ config.telemetry.DD_TELEMETRY_METRICS_ENABLED
10
11
 
11
12
  this.verbosity = telemetryAndMetricsEnabled ? getVerbosity(verbosity) : Verbosity.OFF
12
13
  this.enabled = this.verbosity !== Verbosity.OFF
@@ -16,6 +16,8 @@ const {
16
16
  fastifyCookieParser,
17
17
  incomingHttpRequestStart,
18
18
  incomingHttpRequestEnd,
19
+ lambdaStartInvocation,
20
+ lambdaEndInvocation,
19
21
  passportVerify,
20
22
  passportUser,
21
23
  expressSession,
@@ -43,6 +45,7 @@ const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction }
43
45
  const { getActiveRequest } = require('./store')
44
46
  const UserTracking = require('./user_tracking')
45
47
  const graphql = require('./graphql')
48
+ const lambda = require('./lambda')
46
49
  const rasp = require('./rasp')
47
50
 
48
51
  const responseAnalyzedSet = new WeakSet()
@@ -99,11 +102,15 @@ function enable (_config) {
99
102
  stripeCheckoutSessionCreate.subscribe(onStripeCheckoutSessionCreate)
100
103
  stripePaymentIntentCreate.subscribe(onStripePaymentIntentCreate)
101
104
  stripeConstructEvent.subscribe(onStripeConstructEvent)
105
+ lambdaStartInvocation.subscribe(lambda.onLambdaStartInvocation)
106
+ lambdaEndInvocation.subscribe(lambda.onLambdaEndInvocation)
102
107
 
103
108
  isEnabled = true
104
109
  config = _config
105
110
  } catch (err) {
106
- if (!IS_SERVERLESS) {
111
+ if (IS_SERVERLESS) {
112
+ log.debug('[ASM] Serverless mode: suppressing error log, calling disable()')
113
+ } else {
107
114
  log.error('[ASM] Unable to start AppSec', err)
108
115
  }
109
116
 
@@ -544,6 +551,8 @@ function disable () {
544
551
  if (stripeCheckoutSessionCreate.hasSubscribers) stripeCheckoutSessionCreate.unsubscribe(onStripeCheckoutSessionCreate)
545
552
  if (stripePaymentIntentCreate.hasSubscribers) stripePaymentIntentCreate.unsubscribe(onStripePaymentIntentCreate)
546
553
  if (stripeConstructEvent.hasSubscribers) stripeConstructEvent.unsubscribe(onStripeConstructEvent)
554
+ if (lambdaStartInvocation.hasSubscribers) lambdaStartInvocation.unsubscribe(lambda.onLambdaStartInvocation)
555
+ if (lambdaEndInvocation.hasSubscribers) lambdaEndInvocation.unsubscribe(lambda.onLambdaEndInvocation)
547
556
  }
548
557
 
549
558
  /**
@@ -0,0 +1,135 @@
1
+ 'use strict'
2
+
3
+ const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
4
+
5
+ const log = require('../log')
6
+ const addresses = require('./addresses')
7
+ const Reporter = require('./reporter')
8
+ const waf = require('./waf')
9
+
10
+ // Tracks spans for which start-invocation has been processed, so that
11
+ // end-invocation can gate correctly
12
+ const activeInvocations = new WeakSet()
13
+
14
+ /**
15
+ * Maps pre-extracted HTTP data from the Lambda event to WAF addresses,
16
+ * runs the WAF, and reports results on the span.
17
+ *
18
+ * @param {{ span: object, headers: Record<string, string>, method: string, path: string,
19
+ * query: Record<string, string | string[]> | undefined, body: string | object | undefined,
20
+ * isBase64Encoded: boolean, clientIp: string | undefined,
21
+ * pathParams: Record<string, string> | undefined,
22
+ * cookies: Record<string, string> | undefined,
23
+ * route: string | undefined }} data
24
+ */
25
+ function onLambdaStartInvocation (data) {
26
+ try {
27
+ const { span, headers, method, path, query, body, clientIp, pathParams, cookies } = data
28
+
29
+ if (!span) {
30
+ log.warn('[ASM] No span provided in Lambda start invocation')
31
+ return
32
+ }
33
+
34
+ activeInvocations.add(span)
35
+
36
+ span.addTags({
37
+ '_dd.appsec.enabled': 1,
38
+ '_dd.runtime_family': 'nodejs',
39
+ })
40
+
41
+ const persistent = {}
42
+
43
+ if (path) {
44
+ persistent[addresses.HTTP_INCOMING_URL] = path
45
+ }
46
+
47
+ if (method) {
48
+ persistent[addresses.HTTP_INCOMING_METHOD] = method
49
+ }
50
+
51
+ if (headers) {
52
+ // Cookie header is already stripped by the Lambda layer's event-data-extractor
53
+ persistent[addresses.HTTP_INCOMING_HEADERS] = headers
54
+ }
55
+
56
+ if (clientIp) {
57
+ span.setTag(HTTP_CLIENT_IP, clientIp)
58
+ persistent[addresses.HTTP_CLIENT_IP] = clientIp
59
+ }
60
+
61
+ if (query) {
62
+ persistent[addresses.HTTP_INCOMING_QUERY] = query
63
+ }
64
+
65
+ if (body !== undefined && body !== null) {
66
+ persistent[addresses.HTTP_INCOMING_BODY] = body
67
+ }
68
+
69
+ if (pathParams) {
70
+ persistent[addresses.HTTP_INCOMING_PARAMS] = pathParams
71
+ }
72
+
73
+ if (cookies) {
74
+ persistent[addresses.HTTP_INCOMING_COOKIES] = cookies
75
+ }
76
+
77
+ waf.run({ persistent }, span, undefined, span)
78
+ } catch (err) {
79
+ log.error('[ASM] Error in Lambda start-invocation handler', err)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Maps response data to WAF addresses, runs a final WAF pass,
85
+ * disposes the WAF context, and finishes the request report.
86
+ *
87
+ * @param {{ span: object, statusCode: string | undefined,
88
+ * responseHeaders: Record<string, string> | undefined }} data
89
+ */
90
+ function onLambdaEndInvocation (data) {
91
+ try {
92
+ const { span, statusCode, responseHeaders } = data
93
+
94
+ if (!span) {
95
+ log.warn('[ASM] No span provided in Lambda end invocation')
96
+ return
97
+ }
98
+
99
+ if (!activeInvocations.has(span)) {
100
+ return
101
+ }
102
+
103
+ activeInvocations.delete(span)
104
+
105
+ let hasPersistentData = false
106
+ const persistent = {}
107
+
108
+ if (statusCode) {
109
+ persistent[addresses.HTTP_INCOMING_RESPONSE_CODE] = String(statusCode)
110
+ hasPersistentData = true
111
+ }
112
+
113
+ if (responseHeaders) {
114
+ const filteredHeaders = { ...responseHeaders }
115
+ delete filteredHeaders['set-cookie']
116
+ persistent[addresses.HTTP_INCOMING_RESPONSE_HEADERS] = filteredHeaders
117
+ hasPersistentData = true
118
+ }
119
+
120
+ if (hasPersistentData) {
121
+ waf.run({ persistent }, span, undefined, span)
122
+ }
123
+
124
+ waf.disposeContext(span)
125
+
126
+ Reporter.finishRequest(span, null, {}, undefined, span)
127
+ } catch (err) {
128
+ log.error('[ASM] Error in Lambda end-invocation handler', err)
129
+ }
130
+ }
131
+
132
+ module.exports = {
133
+ onLambdaStartInvocation,
134
+ onLambdaEndInvocation,
135
+ }
@@ -318,13 +318,21 @@ function reportWafConfigUpdate (product, rcConfigId, diagnostics, wafVersion) {
318
318
  }
319
319
  }
320
320
 
321
- function reportMetrics (metrics, raspRule, req) {
321
+ /**
322
+ * @param {object} metrics - WAF run metrics
323
+ * @param {string} [raspRule] - RASP rule identifier
324
+ * @param {object} [req] - Request key (plain object for lambda)
325
+ * @param {object} [rootSpan] - Root span (required for lambda)
326
+ */
327
+ function reportMetrics (metrics, raspRule, req, rootSpan) {
322
328
  if (!req) {
323
329
  req = getActiveRequest()
324
330
  }
325
- const rootSpan = req && web.root(req)
331
+ if (!rootSpan) {
332
+ rootSpan = req && web.root(req)
333
+ }
326
334
 
327
- if (!rootSpan) return
335
+ if (!req || !rootSpan) return
328
336
 
329
337
  if (metrics.rulesVersion) {
330
338
  rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
@@ -353,13 +361,26 @@ function reportTruncationMetrics (rootSpan, metrics) {
353
361
  }
354
362
  }
355
363
 
356
- function reportAttack ({ events: attackData, actions }, req) {
364
+ // NOTE: `req` in the WAF execution path may be any object, not necessarily
365
+ // an HTTP IncomingMessage. In Lambda, it is a plain context key ({}) with no
366
+ // HTTP properties. Always guard HTTP-specific property access
367
+ // See tests in lambda.spec.js for enforcement.
368
+
369
+ /**
370
+ * @param {{ events: Array, actions: object }} result - WAF result with attack data
371
+ * @param {object} [req] - Request key. May be an HTTP IncomingMessage or a plain object (Lambda)
372
+ * @param {object} [rootSpan] - Root span (required for lambda)
373
+ */
374
+ function reportAttack ({ events: attackData, actions }, req, rootSpan) {
357
375
  if (!req) {
358
376
  req = getActiveRequest()
359
377
  }
360
378
 
361
- const rootSpan = web.root(req)
362
- if (!rootSpan) return
379
+ if (!rootSpan) {
380
+ rootSpan = web.root(req)
381
+ }
382
+
383
+ if (!req || !rootSpan) return
363
384
 
364
385
  const spanContext = rootSpan.context()
365
386
 
@@ -494,14 +515,21 @@ function isSchemaAttribute (attribute) {
494
515
  return attribute.startsWith('_dd.appsec.s.')
495
516
  }
496
517
 
497
- function reportAttributes (attributes, req) {
518
+ /**
519
+ * @param {object} [attributes] - WAF result attributes
520
+ * @param {object} [req] - Request key (plain object for lambda)
521
+ * @param {object} [rootSpan] - Root span (required for lambda)
522
+ */
523
+ function reportAttributes (attributes, req, rootSpan) {
498
524
  if (!attributes) return
499
525
 
500
526
  if (!req) {
501
527
  req = getActiveRequest()
502
528
  }
503
529
 
504
- const rootSpan = web.root(req)
530
+ if (!rootSpan) {
531
+ rootSpan = web.root(req)
532
+ }
505
533
 
506
534
  if (!rootSpan) return
507
535
 
@@ -517,8 +545,17 @@ function reportAttributes (attributes, req) {
517
545
  rootSpan.addTags(tags)
518
546
  }
519
547
 
520
- function finishRequest (req, res, storedResponseHeaders, requestBody) {
521
- const rootSpan = web.root(req)
548
+ /**
549
+ * @param {object} [req] - Request key (null for Lambda)
550
+ * @param {object} [res] - Response object (null for lambda)
551
+ * @param {object} storedResponseHeaders
552
+ * @param {object} [requestBody]
553
+ * @param {object} [rootSpan] - Root span (required for lambda)
554
+ */
555
+ function finishRequest (req, res, storedResponseHeaders, requestBody, rootSpan) {
556
+ if (!rootSpan) {
557
+ rootSpan = web.root(req)
558
+ }
522
559
  if (!rootSpan) return
523
560
 
524
561
  if (metricsQueue.size) {
@@ -569,6 +606,8 @@ function finishRequest (req, res, storedResponseHeaders, requestBody) {
569
606
  rootSpan.setTag('_dd.appsec.rasp.rule.eval', metrics.raspEvalCount)
570
607
  }
571
608
 
609
+ if (!req) return
610
+
572
611
  incrementWafRequestsMetric(req)
573
612
 
574
613
  const tags = rootSpan.context().getTags()
@@ -27,7 +27,7 @@ const metricsStoreMap = new WeakMap()
27
27
  let enabled = false
28
28
 
29
29
  function enable (config) {
30
- enabled = config.telemetry?.enabled && config.telemetry?.metrics
30
+ enabled = config.telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED && config.telemetry.DD_TELEMETRY_METRICS_ENABLED
31
31
  }
32
32
 
33
33
  function disable () {
@@ -110,7 +110,19 @@ function removeConfig (configPath) {
110
110
  }
111
111
  }
112
112
 
113
- function run (data, req, raspRule) {
113
+ // `req` in the WAF execution path may be any object, not necessarily
114
+ // an HTTP IncomingMessage (it is a plain object for lambda).
115
+ // Always guard HTTP-specific property access.
116
+ // @see packages/dd-trace/test/appsec/lambda.spec.js
117
+
118
+ /**
119
+ * @param {object} data - WAF address data ({ persistent, ephemeral })
120
+ * @param {object} req - Request key for WAF context lookup. May be an HTTP
121
+ * IncomingMessage or a plain object (Lambda invocation key).
122
+ * @param {string} [raspRule] - RASP rule identifier
123
+ * @param {object} [rootSpan] - Root span to tag (required for Lambda)
124
+ */
125
+ function run (data, req, raspRule, rootSpan) {
114
126
  if (!req) {
115
127
  req = getActiveRequest()
116
128
  if (!req) {
@@ -120,12 +132,12 @@ function run (data, req, raspRule) {
120
132
  }
121
133
 
122
134
  const wafContext = waf.wafManager.getWAFContext(req)
123
- const result = wafContext.run(data, raspRule, req)
135
+ const result = wafContext.run(data, raspRule, req, rootSpan)
124
136
 
125
137
  if (result?.keep) {
126
138
  if (limiter.isAllowed()) {
127
- const rootSpan = web.root(req)
128
- keepTrace(rootSpan, ASM)
139
+ const span = rootSpan || web.root(req)
140
+ keepTrace(span, ASM)
129
141
  } else {
130
142
  updateRateLimitedMetric(req)
131
143
  }
@@ -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
 
@@ -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,7 @@ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
68
68
  evpProxyPrefix,
69
69
  })
70
70
  this._codeCoverageReportUrl = this._url
71
- if (isTestDynamicInstrumentationEnabled) {
71
+ if (testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
72
72
  const canFowardLogs = getCanForwardDebuggerLogs(err, agentInfo)
73
73
  if (canFowardLogs) {
74
74
  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,7 +24,7 @@ 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,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,