dd-trace 5.108.0 → 5.109.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 (58) hide show
  1. package/index.d.ts +22 -1
  2. package/package.json +2 -1
  3. package/packages/datadog-instrumentations/src/ai.js +43 -48
  4. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  5. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  7. package/packages/datadog-instrumentations/src/electron.js +1 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  11. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  12. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  13. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  14. package/packages/datadog-instrumentations/src/next.js +34 -0
  15. package/packages/datadog-instrumentations/src/openai.js +77 -18
  16. package/packages/datadog-instrumentations/src/redis.js +0 -1
  17. package/packages/datadog-instrumentations/src/vitest.js +60 -1
  18. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  23. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  24. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  25. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
  26. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  27. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
  28. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  29. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  30. package/packages/datadog-plugin-fs/src/index.js +1 -0
  31. package/packages/datadog-plugin-redis/src/index.js +1 -2
  32. package/packages/datadog-plugin-vitest/src/index.js +4 -1
  33. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  34. package/packages/dd-trace/src/aiguard/index.js +11 -49
  35. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  36. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  37. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  38. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  39. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  40. package/packages/dd-trace/src/appsec/channels.js +1 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  45. package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
  46. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
  47. package/packages/dd-trace/src/config/supported-configurations.json +24 -2
  48. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
  49. package/packages/dd-trace/src/dogstatsd.js +15 -8
  50. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  51. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  52. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  53. package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
  54. package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
  55. package/packages/dd-trace/src/plugins/index.js +3 -0
  56. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  57. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  58. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
@@ -14,19 +14,32 @@ const {
14
14
  const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
15
15
  const UINT64_MAX = (1n << 64n) - 1n
16
16
 
17
+ const SUPPORTED_RESPONSE_BODY_MIME_TYPES = new Set([
18
+ 'application/json',
19
+ 'text/json',
20
+ 'application/x-www-form-urlencoded',
21
+ ])
22
+
23
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE =
24
+ '_dd.appsec.downstream_request.response_body_ignored.content_type_invalid'
25
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING =
26
+ '_dd.appsec.downstream_request.response_body_ignored.content_length_missing'
27
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG =
28
+ '_dd.appsec.downstream_request.response_body_ignored.content_length_too_big'
29
+
17
30
  let config
18
31
  let samplingRate
19
32
  let globalRequestCounter
20
33
  let bodyAnalysisCount
21
34
  let downstreamAnalysisCount
22
- let redirectBodyCollectionDecisions
35
+ let responseBodyIgnoredCount
23
36
 
24
37
  function enable (_config) {
25
38
  config = _config
26
39
  globalRequestCounter = 0n
27
40
  bodyAnalysisCount = new WeakMap()
28
41
  downstreamAnalysisCount = new WeakMap()
29
- redirectBodyCollectionDecisions = new WeakMap()
42
+ responseBodyIgnoredCount = new WeakMap()
30
43
 
31
44
  const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
32
45
  samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
@@ -43,49 +56,84 @@ function disable () {
43
56
  globalRequestCounter = null
44
57
  bodyAnalysisCount = null
45
58
  downstreamAnalysisCount = null
46
- redirectBodyCollectionDecisions = null
59
+ responseBodyIgnoredCount = null
47
60
  }
48
61
 
49
62
  /**
50
- * Check we have a stored redirect body collection decision for a given URL.
51
- * @param {import('http').IncomingMessage} req outgoing request.
52
- * @param {string} outgoingUrl the URL being requested.
53
- * @returns {boolean} the stored decision
63
+ * @param {string|string[]|undefined} contentLength raw content-length header value.
64
+ * @returns {number|null} parsed content length or null when invalid.
54
65
  */
55
- function consumeRedirectBodyCollectionDecision (req, outgoingUrl) {
56
- const decisions = redirectBodyCollectionDecisions.get(req)
57
- if (!decisions) return false
66
+ function parseContentLengthHeader (contentLength) {
67
+ if (contentLength == null) {
68
+ return null
69
+ }
70
+
71
+ const value = Array.isArray(contentLength) ? contentLength[0] : contentLength
72
+ const parsed = Number.parseInt(String(value), 10)
58
73
 
59
- return decisions.delete(outgoingUrl)
74
+ if (!Number.isFinite(parsed) || parsed < 0) {
75
+ return null
76
+ }
77
+
78
+ return parsed
60
79
  }
61
80
 
62
81
  /**
63
- * Stores a redirect body collection decision for a follow-up request.
64
- * @param {import('http').IncomingMessage} req outgoing request.
65
- * @param {string} redirectUrl the URL to redirect to.
82
+ * Increments a response-body-ignored counter on the service-entry span.
83
+ * @param {import('http').IncomingMessage} req originating request.
84
+ * @param {string} tag full `_dd.appsec.downstream_request.response_body_ignored.*` span tag.
66
85
  */
67
- function storeRedirectBodyCollectionDecision (req, redirectUrl) {
68
- let decisions = redirectBodyCollectionDecisions.get(req)
86
+ function recordResponseBodyIgnored (req, tag) {
87
+ const span = web.root(req)
88
+ if (!span) return
69
89
 
70
- if (!decisions) {
71
- decisions = new Set()
72
- redirectBodyCollectionDecisions.set(req, decisions)
90
+ let counts = responseBodyIgnoredCount.get(req)
91
+ if (!counts) {
92
+ counts = {}
93
+ responseBodyIgnoredCount.set(req, counts)
73
94
  }
74
95
 
75
- decisions.add(redirectUrl)
96
+ const current = counts[tag] || 0
97
+ const next = current + 1
98
+ counts[tag] = next
99
+ span.setTag(tag, next)
76
100
  }
77
101
 
78
102
  /**
79
- * Determines whether the current downstream request/responses bodies should be sampled for analysis.
80
- * @param {import('http').IncomingMessage} req outgoing request.
81
- * @param {string} outgoingUrl the URL being requested (to check for redirect decisions).
82
- * @returns {boolean} true when the downstream response body should be captured.
103
+ * @param {import('http').IncomingMessage} originatingReq inbound request (for metrics).
104
+ * @param {import('http').IncomingMessage} res downstream response.
105
+ * @returns {boolean} whether downstream response body should be collected for AppSec.
83
106
  */
84
- function shouldSampleBody (req, outgoingUrl) {
85
- // Check if there's a stored decision from a previous redirect
86
- const storedDecision = consumeRedirectBodyCollectionDecision(req, outgoingUrl)
87
- if (storedDecision) return true
107
+ function evaluateResponseBodyCollection (originatingReq, res) {
108
+ const maxBytes = config.appsec.apiSecurity.maxDownstreamBodyBytes
109
+
110
+ const mime = extractMimeType(res.headers?.['content-type'])
111
+ if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
112
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE)
113
+ return false
114
+ }
115
+
116
+ const declaredContentLength = parseContentLengthHeader(res.headers?.['content-length'])
117
+ if (declaredContentLength == null || declaredContentLength === 0) {
118
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING)
119
+ return false
120
+ }
121
+
122
+ if (declaredContentLength > maxBytes) {
123
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG)
124
+ return false
125
+ }
126
+
127
+ return true
128
+ }
88
129
 
130
+ /**
131
+ * Probabilistic gate for downstream response body capture (rate + per-request cap).
132
+ * Only used from {@link planResponseBodyCollection}; does not increment {@link bodyAnalysisCount}.
133
+ * @param {import('http').IncomingMessage} req originating server request.
134
+ * @returns {boolean}
135
+ */
136
+ function shouldSampleBody (req) {
89
137
  globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
90
138
 
91
139
  const currentCount = bodyAnalysisCount.get(req) || 0
@@ -97,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
97
145
  // Replace 1000n with the accuraccy that we want to maintain
98
146
  const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
99
147
 
100
- const shouldCollectBody = hashed <= threshold
148
+ return hashed <= threshold
149
+ }
150
+
151
+ /**
152
+ * @param {import('http').IncomingMessage} res downstream HTTP response.
153
+ * @returns {boolean}
154
+ */
155
+ function isRedirectResponse (res) {
156
+ const location = res.headers?.location || ''
157
+ return res.statusCode >= 300 && res.statusCode < 400 && !!location
158
+ }
159
+
160
+ /**
161
+ * Plans downstream response body capture on the instrumentation ctx when response headers arrive.
162
+ * Redirect responses (3xx + Location) are ignored; each outbound hop is evaluated independently
163
+ * when its own non-redirect response arrives.
164
+ * @param {import('http').IncomingMessage} originatingReq incoming server request.
165
+ * @param {import('http').IncomingMessage} res downstream response.
166
+ * @param {object} ctx http client instrumentation context (mutated).
167
+ */
168
+ function planResponseBodyCollection (originatingReq, res, ctx) {
169
+ if (!config?.appsec.apiSecurity) {
170
+ return
171
+ }
101
172
 
102
- // Track body analysis count if we're sampling the response body
103
- if (shouldCollectBody) {
104
- incrementBodyAnalysisCount(req)
173
+ if (isRedirectResponse(res)) {
174
+ return
105
175
  }
106
176
 
107
- return shouldCollectBody
177
+ if (!shouldSampleBody(originatingReq)) {
178
+ return
179
+ }
180
+
181
+ if (evaluateResponseBodyCollection(originatingReq, res)) {
182
+ ctx.shouldCollectBody = true
183
+ incrementBodyAnalysisCount(originatingReq)
184
+ }
108
185
  }
109
186
 
110
187
  /**
@@ -145,25 +222,6 @@ function extractRequestData (ctx) {
145
222
  return addresses
146
223
  }
147
224
 
148
- /**
149
- * Checks if a response is a redirect
150
- * @param {import('http').IncomingMessage} req incoming server request.
151
- * @param {import('http').IncomingMessage} res downstream response object.
152
- * @returns {boolean} is redirect.
153
- */
154
- function handleRedirectResponse (req, res) {
155
- const isRedirect = res.statusCode >= 300 && res.statusCode < 400
156
- const redirectLocation = res.headers?.location || ''
157
-
158
- if (isRedirect && redirectLocation) {
159
- // Store the body collection decision for the redirect target
160
- storeRedirectBodyCollectionDecision(req, redirectLocation)
161
- return true
162
- }
163
-
164
- return false
165
- }
166
-
167
225
  /**
168
226
  * Extracts response data for WAF analysis.
169
227
  * @param {import('http').IncomingMessage} res downstream response object.
@@ -291,13 +349,8 @@ function extractMimeType (contentType) {
291
349
  module.exports = {
292
350
  enable,
293
351
  disable,
294
- shouldSampleBody,
295
- handleRedirectResponse,
352
+ planResponseBodyCollection,
296
353
  incrementDownstreamAnalysisCount,
297
354
  extractRequestData,
298
355
  extractResponseData,
299
- // exports for tests
300
- parseBody,
301
- getMethod,
302
- storeRedirectBodyCollectionDecision,
303
356
  }
@@ -2,24 +2,66 @@
2
2
 
3
3
  const log = require('../../../../../log')
4
4
 
5
- const LDAP_PATTERN = String.raw`\(.*?(?:~=|=|<=|>=)(?<LITERAL>[^)]+)\)`
6
- const pattern = new RegExp(LDAP_PATTERN, 'gmi')
5
+ const OPEN_PAREN = 0x28
6
+ const CLOSE_PAREN = 0x29
7
+ const EQUALS = 0x3D
8
+ const TILDE = 0x7E
9
+ const LESS = 0x3C
10
+ const GREATER = 0x3E
7
11
 
12
+ // Linear scanner for LDAP assertion-filter values. For each parenthesised group
13
+ // "(attr <op> value)" where <op> is "=", "~=", "<=", or ">=" and no nested
14
+ // parenthesis appears before the operator, report the value range [opEnd, ')').
15
+ // The cursor only ever moves forward, so total work is O(input length).
8
16
  module.exports = function extractSensitiveRanges (evidence) {
9
17
  try {
10
- pattern.lastIndex = 0
18
+ const value = evidence?.value
11
19
  const tokens = []
20
+ if (typeof value !== 'string') return tokens
12
21
 
13
- let regexResult = pattern.exec(evidence.value)
14
- while (regexResult != null) {
15
- if (!regexResult.groups.LITERAL) continue
16
- // Computing indices manually since Node.js 12 does not support d flag on regular expressions
17
- // TODO Get indices from group by adding d flag in regular expression
18
- const start = regexResult.index + (regexResult[0].length - regexResult.groups.LITERAL.length - 1)
19
- const end = start + regexResult.groups.LITERAL.length
20
- tokens.push({ start, end })
21
- regexResult = pattern.exec(evidence.value)
22
+ const length = value.length
23
+ let cursor = 0
24
+
25
+ while (cursor < length) {
26
+ const open = value.indexOf('(', cursor)
27
+ if (open === -1) break
28
+
29
+ let scan = open + 1
30
+ let opStart = -1
31
+ let opLen = 0
32
+
33
+ while (scan < length) {
34
+ const code = value.charCodeAt(scan)
35
+ if (code === OPEN_PAREN || code === CLOSE_PAREN) break
36
+ if (code === EQUALS) {
37
+ opStart = scan
38
+ opLen = 1
39
+ break
40
+ }
41
+ if ((code === TILDE || code === LESS || code === GREATER) &&
42
+ scan + 1 < length && value.charCodeAt(scan + 1) === EQUALS) {
43
+ opStart = scan
44
+ opLen = 2
45
+ break
46
+ }
47
+ scan++
48
+ }
49
+
50
+ if (opStart === -1) {
51
+ cursor = open + 1
52
+ continue
53
+ }
54
+
55
+ const close = value.indexOf(')', opStart + opLen)
56
+ if (close === -1) break
57
+
58
+ const start = opStart + opLen
59
+ if (start < close) {
60
+ tokens.push({ start, end: close })
61
+ }
62
+ cursor = close + 1
22
63
  }
64
+
23
65
  return tokens
24
66
  } catch (e) {
25
67
  log.debug('[ASM] Error extracting sensitive ranges', e)
@@ -3,7 +3,11 @@
3
3
  const log = require('../../../../../log')
4
4
 
5
5
  const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@'
6
- const QUERY_FRAGMENT = '[?#&]([^=&;]+)=([^?#&]+)'
6
+ // The key class excludes `?` and `#` so the greedy quantifier is bounded per fragment.
7
+ // Query keys cannot legitimately contain those characters (they delimit query/fragment
8
+ // boundaries), so excluding them preserves match semantics for valid URLs while keeping
9
+ // the regex linear on arbitrary input.
10
+ const QUERY_FRAGMENT = '[?#&]([^=&;?#]+)=([^?#&]+)'
7
11
  const pattern = new RegExp([AUTHORITY, QUERY_FRAGMENT].join('|'), 'gmi')
8
12
 
9
13
  module.exports = function extractSensitiveRanges (evidence) {
@@ -17,6 +17,11 @@ const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyz
17
17
 
18
18
  const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
19
19
 
20
+ // Upper bound on the evidence-string length the redaction analyzers will scan. Oversized
21
+ // values bypass the analyzer entirely and are emitted as a fully-redacted placeholder.
22
+ // Counted in JS string characters (UTF-16 code units), not bytes.
23
+ const MAX_EVIDENCE_LENGTH = 32_768
24
+
20
25
  class SensitiveHandler {
21
26
  constructor () {
22
27
  this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
@@ -53,11 +58,31 @@ class SensitiveHandler {
53
58
 
54
59
  scrubEvidence (vulnerabilityType, evidence, sourcesIndexes, sources) {
55
60
  const sensitiveAnalyzer = this._sensitiveAnalyzers.get(vulnerabilityType)
56
- if (sensitiveAnalyzer) {
57
- const sensitiveRanges = sensitiveAnalyzer(evidence)
58
- if (evidence.ranges || sensitiveRanges?.length) {
59
- return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
61
+ if (!sensitiveAnalyzer) {
62
+ return null
63
+ }
64
+
65
+ // Oversized evidence: skip the analyzer and emit a fully-redacted placeholder. Mark
66
+ // every source backing this vulnerability as redacted so the formatter strips their
67
+ // raw `value` before they leave the process.
68
+ if (typeof evidence.value === 'string' && evidence.value.length > MAX_EVIDENCE_LENGTH) {
69
+ const redactedSources = []
70
+ for (const sourceIndex of sourcesIndexes) {
71
+ const source = sources[sourceIndex]
72
+ if (source && !source.redacted) {
73
+ source.pattern = ''.padEnd(source.value.length, REDACTED_SOURCE_BUFFER)
74
+ source.redacted = true
75
+ }
76
+ if (!redactedSources.includes(sourceIndex)) {
77
+ redactedSources.push(sourceIndex)
78
+ }
60
79
  }
80
+ return { redactedValueParts: [{ redacted: true }], redactedSources }
81
+ }
82
+
83
+ const sensitiveRanges = sensitiveAnalyzer(evidence)
84
+ if (evidence.ranges || sensitiveRanges?.length) {
85
+ return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
61
86
  }
62
87
  return null
63
88
  }
@@ -3,6 +3,7 @@
3
3
  const { format } = require('url')
4
4
  const {
5
5
  httpClientRequestStart,
6
+ httpClientResponseStart,
6
7
  httpClientResponseFinish,
7
8
  } = require('../channels')
8
9
  const addresses = require('../addresses')
@@ -21,6 +22,7 @@ function enable (_config) {
21
22
  downstream.enable(_config)
22
23
 
23
24
  httpClientRequestStart.subscribe(analyzeSsrf)
25
+ httpClientResponseStart.subscribe(planResponseBodyCollection)
24
26
  httpClientResponseFinish.subscribe(handleResponseFinish)
25
27
  }
26
28
 
@@ -28,6 +30,7 @@ function disable () {
28
30
  downstream.disable()
29
31
 
30
32
  if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
33
+ if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
31
34
  if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
32
35
  }
33
36
 
@@ -37,9 +40,6 @@ function analyzeSsrf (ctx) {
37
40
 
38
41
  if (!req || !outgoingUrl) return
39
42
 
40
- // Determine if we should collect the response body based on sampling rate and redirect URL
41
- ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
42
-
43
43
  const requestAddresses = downstream.extractRequestData(ctx)
44
44
 
45
45
  const ephemeral = {
@@ -56,13 +56,23 @@ function analyzeSsrf (ctx) {
56
56
  downstream.incrementDownstreamAnalysisCount(req)
57
57
  }
58
58
 
59
+ /**
60
+ * Channel handler: plans downstream response body capture once response headers are available.
61
+ * @param {{ ctx: object, res: import('http').IncomingMessage }} payload channel payload.
62
+ */
63
+ function planResponseBodyCollection ({ ctx, res }) {
64
+ const originatingRequest = getActiveRequest()
65
+ if (!originatingRequest || !res) return
66
+
67
+ downstream.planResponseBodyCollection(originatingRequest, res, ctx)
68
+ }
69
+
59
70
  /**
60
71
  * Finalizes body collection for the response and triggers RASP analysis.
61
- * @param {{
62
- * ctx: object,
63
- * res: import('http').IncomingMessage,
64
- * body: string|Buffer|null
65
- * }} payload event payload from the channel.
72
+ * @param {object} params event payload from the channel.
73
+ * @param {object} params.ctx instrumentation context.
74
+ * @param {import('http').IncomingMessage} params.res downstream response.
75
+ * @param {string|Buffer|null} params.body collected body.
66
76
  */
67
77
  function handleResponseFinish ({ ctx, res, body }) {
68
78
  // downstream response object
@@ -71,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
71
81
  const originatingRequest = getActiveRequest()
72
82
  if (!originatingRequest) return
73
83
 
74
- // Skip body analysis for redirect responses
75
- const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
76
- const responseBody = evaluateBody ? body : null
84
+ const responseBody = ctx.shouldCollectBody ? body : null
77
85
  runResponseEvaluation(res, originatingRequest, responseBody)
78
86
  }
79
87
 
@@ -11,6 +11,7 @@ export interface GeneratedConfig {
11
11
  enabled: boolean;
12
12
  endpointCollectionEnabled: boolean;
13
13
  endpointCollectionMessageLimit: number;
14
+ maxDownstreamBodyBytes: number;
14
15
  maxDownstreamRequestBodyAnalysis: number;
15
16
  sampleDelay: number;
16
17
  };
@@ -87,6 +88,7 @@ export interface GeneratedConfig {
87
88
  DD_CRASHTRACKING_ENABLED: boolean;
88
89
  DD_CUSTOM_PARENT_ID: string | undefined;
89
90
  DD_CUSTOM_TRACE_ID: string | undefined;
91
+ DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED: boolean;
90
92
  DD_ENABLE_LAGE_PACKAGE_NAME: boolean;
91
93
  DD_ENABLE_NX_SERVICE_NAME: boolean;
92
94
  DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: boolean;
@@ -177,6 +179,7 @@ export interface GeneratedConfig {
177
179
  DD_TRACE_APOLLO_SUBGRAPH_ENABLED: boolean;
178
180
  DD_TRACE_AVSC_ENABLED: boolean;
179
181
  DD_TRACE_AWS_ADD_SPAN_POINTERS: boolean;
182
+ DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED: boolean;
180
183
  DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED: boolean;
181
184
  DD_TRACE_AWS_SDK_AWS_ENABLED: boolean;
182
185
  DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED: boolean;
@@ -167,6 +167,14 @@
167
167
  "default": "1"
168
168
  }
169
169
  ],
170
+ "DD_API_SECURITY_MAX_DOWNSTREAM_BODY_BYTES": [
171
+ {
172
+ "implementation": "A",
173
+ "type": "int",
174
+ "internalPropertyName": "appsec.apiSecurity.maxDownstreamBodyBytes",
175
+ "default": "10485760"
176
+ }
177
+ ],
170
178
  "DD_API_SECURITY_SAMPLE_DELAY": [
171
179
  {
172
180
  "implementation": "A",
@@ -1247,6 +1255,20 @@
1247
1255
  "default": "false"
1248
1256
  }
1249
1257
  ],
1258
+ "DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED": [
1259
+ {
1260
+ "implementation": "A",
1261
+ "type": "boolean",
1262
+ "default": "true"
1263
+ }
1264
+ ],
1265
+ "DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED": [
1266
+ {
1267
+ "implementation": "A",
1268
+ "type": "boolean",
1269
+ "default": "true"
1270
+ }
1271
+ ],
1250
1272
  "DD_TRACE_LOG_LEVEL": [
1251
1273
  {
1252
1274
  "implementation": "C",
@@ -2633,9 +2655,9 @@
2633
2655
  ],
2634
2656
  "DD_TRACE_FS_ENABLED": [
2635
2657
  {
2636
- "implementation": "A",
2658
+ "implementation": "B",
2637
2659
  "type": "boolean",
2638
- "default": "true"
2660
+ "default": "false"
2639
2661
  }
2640
2662
  ],
2641
2663
  "DD_TRACE_GCP_PUBSUB_PUSH_ENABLED": [
@@ -269,6 +269,8 @@ async function reEvaluateProbe (probe) {
269
269
  if (probeToLocation.has(probe.id)) {
270
270
  await removeBreakpoint(probe)
271
271
  }
272
+ // TODO: Revisit diagnostic status handling for probes that recover during re-evaluation. A probe can initially
273
+ // report ERROR because no script matched, then attach successfully here without reporting INSTALLED.
272
274
  await addBreakpoint(probe)
273
275
  }
274
276
  }
@@ -3,11 +3,14 @@
3
3
  const dgram = require('dgram')
4
4
  const isIP = require('net').isIP
5
5
 
6
+ const { storage } = require('../../datadog-core')
6
7
  const request = require('./exporters/common/request')
7
8
  const log = require('./log')
8
9
  const Histogram = require('./histogram')
9
10
  const { entityId } = require('./exporters/common/docker')
10
11
 
12
+ const legacyStorage = storage('legacy')
13
+
11
14
  const MAX_BUFFER_SIZE = 1024 // limit from the agent
12
15
 
13
16
  const TYPE_COUNTER = 'c'
@@ -98,14 +101,18 @@ class DogStatsDClient {
98
101
  }
99
102
 
100
103
  _sendUdp (queue) {
101
- if (this._family === 0) {
102
- this.#lookup(this._host, (err, address, family) => {
103
- if (err) return log.error('DogStatsDClient: Host not found', err)
104
- this._sendUdpFromQueue(queue, address, family)
105
- })
106
- } else {
107
- this._sendUdpFromQueue(queue, this._host, this._family)
108
- }
104
+ // dgram resolves the local address via the instrumented dns.lookup when it
105
+ // binds on first send; the noop store keeps that self-traffic off the trace.
106
+ legacyStorage.run({ noop: true }, () => {
107
+ if (this._family === 0) {
108
+ this.#lookup(this._host, (error, address, family) => {
109
+ if (error) return log.error('DogStatsDClient: Host not found', error)
110
+ this._sendUdpFromQueue(queue, address, family)
111
+ })
112
+ } else {
113
+ this._sendUdpFromQueue(queue, this._host, this._family)
114
+ }
115
+ })
109
116
  }
110
117
 
111
118
  _sendUdpFromQueue (queue, address, family) {
@@ -7,6 +7,7 @@ const log = require('../../log')
7
7
  const { entityId } = require('../common/docker')
8
8
  const tracerVersion = require('../../../../../package.json').version
9
9
  const Writer = require('./writer')
10
+ const { computeIntakeUrl } = require('./intake')
10
11
 
11
12
  /**
12
13
  * Agentless exporter for APM trace intake.
@@ -15,6 +16,7 @@ const Writer = require('./writer')
15
16
  */
16
17
  class AgentlessExporter {
17
18
  #timer
19
+ #config
18
20
 
19
21
  /**
20
22
  * @param {object} config - Configuration object
@@ -24,13 +26,13 @@ class AgentlessExporter {
24
26
  * @param {object} [config.tags] - Tags including runtime-id
25
27
  */
26
28
  constructor (config) {
27
- this._config = config
29
+ this.#config = config
28
30
  const site = config.site ?? 'datadoghq.com'
29
31
 
30
32
  try {
31
- // Agentless traffic carries the Datadog API key, so the intake is always the public https
32
- // endpoint; never derive it from config.url (the agent's cleartext http) or the key leaks.
33
- this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
33
+ // Agentless traffic carries the Datadog API key, so the intake is always an https endpoint
34
+ // derived from the site; never config.url (the agent's cleartext http) or the key leaks.
35
+ this._url = new URL(computeIntakeUrl(site))
34
36
  } catch (err) {
35
37
  log.error('Invalid site for agentless exporter. site=%s. Error: %s', site, err.message)
36
38
  this._url = null
@@ -89,7 +91,7 @@ class AgentlessExporter {
89
91
  export (spans) {
90
92
  this._writer.append(spans)
91
93
 
92
- const { flushInterval } = this._config
94
+ const { flushInterval } = this.#config
93
95
 
94
96
  if (flushInterval === 0) {
95
97
  try {
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ // Per-site hosts for the agentless JSON span intake. Regional data centers serve it from
4
+ // browser-intake-* hosts rather than public-trace-http-intake.logs.<site>, so a single template
5
+ // silently drops spans on us3/us5/ap1/ap2. Mirrors dd-trace-py's AgentlessTraceWriter.INTAKE_URLS
6
+ // (DataDog/dd-trace-py#18514).
7
+ const INTAKE_URLS = {
8
+ 'datadoghq.com': 'https://public-trace-http-intake.logs.datadoghq.com',
9
+ 'datadoghq.eu': 'https://public-trace-http-intake.logs.datadoghq.eu',
10
+ 'us3.datadoghq.com': 'https://trace.browser-intake-us3-datadoghq.com',
11
+ 'us5.datadoghq.com': 'https://trace.browser-intake-us5-datadoghq.com',
12
+ 'ap1.datadoghq.com': 'https://browser-intake-ap1-datadoghq.com',
13
+ 'ap2.datadoghq.com': 'https://browser-intake-ap2-datadoghq.com',
14
+ 'uk1.datadoghq.com': 'https://browser-intake-uk1-datadoghq.com',
15
+ 'datad0g.com': 'https://public-trace-http-intake.logs.datad0g.com',
16
+ }
17
+
18
+ // Path of the JSON span intake on every intake host.
19
+ const INTAKE_PATH = '/api/v2/spans'
20
+
21
+ /**
22
+ * Resolves the agentless intake origin for a Datadog site.
23
+ *
24
+ * Unknown sites fall back to the browser-intake naming: strip the TLD, dash-join the rest, then
25
+ * reattach the TLD, e.g. 'us2.ddog-gov.com' -> 'https://browser-intake-us2-ddog-gov.com'.
26
+ *
27
+ * @param {string} [site] - The Datadog site, e.g. 'us3.datadoghq.com'. Defaults to 'datadoghq.com'.
28
+ * @returns {string} The intake origin, without a path.
29
+ */
30
+ function computeIntakeUrl (site = 'datadoghq.com') {
31
+ const normalized = site.toLowerCase()
32
+ const known = INTAKE_URLS[normalized]
33
+ if (known !== undefined) {
34
+ return known
35
+ }
36
+
37
+ const lastDot = normalized.lastIndexOf('.')
38
+ const prefix = lastDot === -1 ? '' : normalized.slice(0, lastDot)
39
+ const tld = lastDot === -1 ? normalized : normalized.slice(lastDot + 1)
40
+ return `https://browser-intake-${prefix.replaceAll('.', '-')}.${tld}`
41
+ }
42
+
43
+ module.exports = { INTAKE_URLS, INTAKE_PATH, computeIntakeUrl }