dd-trace 5.52.0 → 5.53.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 (55) hide show
  1. package/README.md +5 -0
  2. package/index.d.ts +54 -6
  3. package/package.json +1 -1
  4. package/packages/datadog-instrumentations/src/amqplib.js +8 -5
  5. package/packages/datadog-instrumentations/src/child_process.js +2 -1
  6. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +16 -1
  7. package/packages/datadog-instrumentations/src/couchbase.js +2 -1
  8. package/packages/datadog-instrumentations/src/cucumber.js +41 -46
  9. package/packages/datadog-instrumentations/src/express.js +2 -6
  10. package/packages/datadog-instrumentations/src/fs.js +6 -5
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  12. package/packages/datadog-instrumentations/src/helpers/register.js +17 -12
  13. package/packages/datadog-instrumentations/src/http/client.js +2 -1
  14. package/packages/datadog-instrumentations/src/iovalkey.js +51 -0
  15. package/packages/datadog-instrumentations/src/jest.js +49 -41
  16. package/packages/datadog-instrumentations/src/kafkajs.js +21 -8
  17. package/packages/datadog-instrumentations/src/mocha/main.js +33 -46
  18. package/packages/datadog-instrumentations/src/mocha/utils.js +72 -75
  19. package/packages/datadog-instrumentations/src/mysql2.js +3 -1
  20. package/packages/datadog-instrumentations/src/net.js +3 -1
  21. package/packages/datadog-instrumentations/src/next.js +6 -14
  22. package/packages/datadog-instrumentations/src/pg.js +5 -11
  23. package/packages/datadog-instrumentations/src/playwright.js +60 -69
  24. package/packages/datadog-instrumentations/src/url.js +9 -17
  25. package/packages/datadog-instrumentations/src/vitest.js +55 -75
  26. package/packages/datadog-plugin-cucumber/src/index.js +29 -18
  27. package/packages/datadog-plugin-iovalkey/src/index.js +18 -0
  28. package/packages/datadog-plugin-jest/src/index.js +14 -8
  29. package/packages/datadog-plugin-kafkajs/src/producer.js +8 -5
  30. package/packages/datadog-plugin-mocha/src/index.js +55 -35
  31. package/packages/datadog-plugin-playwright/src/index.js +26 -20
  32. package/packages/datadog-plugin-redis/src/index.js +8 -3
  33. package/packages/datadog-plugin-vitest/src/index.js +53 -42
  34. package/packages/datadog-shimmer/src/shimmer.js +164 -33
  35. package/packages/dd-trace/src/appsec/graphql.js +2 -2
  36. package/packages/dd-trace/src/appsec/index.js +14 -11
  37. package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
  38. package/packages/dd-trace/src/appsec/rasp/utils.js +11 -6
  39. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +2 -2
  40. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -2
  41. package/packages/dd-trace/src/appsec/telemetry/rasp.js +0 -9
  42. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +6 -6
  43. package/packages/dd-trace/src/config.js +1 -1
  44. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +59 -7
  45. package/packages/dd-trace/src/debugger/devtools_client/index.js +10 -26
  46. package/packages/dd-trace/src/debugger/devtools_client/send.js +8 -7
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +15 -7
  48. package/packages/dd-trace/src/debugger/devtools_client/state.js +21 -1
  49. package/packages/dd-trace/src/dogstatsd.js +2 -0
  50. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  51. package/packages/dd-trace/src/plugins/index.js +1 -0
  52. package/packages/dd-trace/src/proxy.js +0 -4
  53. package/packages/dd-trace/src/serverless.js +0 -48
  54. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +8 -0
  55. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +4 -0
@@ -85,10 +85,12 @@ function blockOnDatadogRaspAbortError ({ error }) {
85
85
  const abortError = findDatadogRaspAbortError(error)
86
86
  if (!abortError) return false
87
87
 
88
- const { req, res, blockingAction, raspRule } = abortError
88
+ const { req, res, blockingAction, raspRule, ruleTriggered } = abortError
89
89
  if (!isBlocked(res)) {
90
90
  const blocked = block(req, res, web.root(req), null, blockingAction)
91
- updateRaspRuleMatchMetricTags(req, raspRule, true, blocked)
91
+ if (ruleTriggered) {
92
+ updateRaspRuleMatchMetricTags(req, raspRule, true, blocked)
93
+ }
92
94
  }
93
95
 
94
96
  return true
@@ -20,23 +20,26 @@ const RULE_TYPES = {
20
20
  }
21
21
 
22
22
  class DatadogRaspAbortError extends Error {
23
- constructor (req, res, blockingAction, raspRule) {
23
+ constructor (req, res, blockingAction, raspRule, ruleTriggered) {
24
24
  super('DatadogRaspAbortError')
25
25
  this.name = 'DatadogRaspAbortError'
26
26
  this.req = req
27
27
  this.res = res
28
28
  this.blockingAction = blockingAction
29
29
  this.raspRule = raspRule
30
+ this.ruleTriggered = ruleTriggered
30
31
  }
31
32
  }
32
33
 
33
- function handleResult (actions, req, res, abortController, config, raspRule) {
34
- const generateStackTraceAction = actions?.generate_stack
34
+ function handleResult (result, req, res, abortController, config, raspRule) {
35
+ const generateStackTraceAction = result?.actions?.generate_stack
35
36
 
36
37
  const { enabled, maxDepth, maxStackTraces } = config.appsec.stackTrace
37
38
 
38
39
  const rootSpan = web.root(req)
39
40
 
41
+ const ruleTriggered = !!result?.events?.length
42
+
40
43
  if (generateStackTraceAction && enabled && canReportStackTrace(rootSpan, maxStackTraces)) {
41
44
  const frames = getCallsiteFrames(maxDepth)
42
45
 
@@ -48,11 +51,11 @@ function handleResult (actions, req, res, abortController, config, raspRule) {
48
51
  }
49
52
 
50
53
  if (abortController && !abortOnUncaughtException) {
51
- const blockingAction = getBlockingAction(actions)
54
+ const blockingAction = getBlockingAction(result?.actions)
52
55
 
53
56
  // Should block only in express
54
57
  if (blockingAction && rootSpan?.context()._name === 'express.request') {
55
- const abortError = new DatadogRaspAbortError(req, res, blockingAction, raspRule)
58
+ const abortError = new DatadogRaspAbortError(req, res, blockingAction, raspRule, ruleTriggered)
56
59
  abortController.abort(abortError)
57
60
 
58
61
  // TODO Delete this when support for node 16 is removed
@@ -64,7 +67,9 @@ function handleResult (actions, req, res, abortController, config, raspRule) {
64
67
  }
65
68
  }
66
69
 
67
- updateRaspRuleMatchMetricTags(req, raspRule, false, false)
70
+ if (ruleTriggered) {
71
+ updateRaspRuleMatchMetricTags(req, raspRule, false, false)
72
+ }
68
73
  }
69
74
 
70
75
  module.exports = {
@@ -9,8 +9,8 @@ const { setUserTags } = require('./set_user')
9
9
  const log = require('../../log')
10
10
 
11
11
  function isUserBlocked (user) {
12
- const actions = waf.run({ persistent: { [USER_ID]: user.id } })
13
- return !!getBlockingAction(actions)
12
+ const results = waf.run({ persistent: { [USER_ID]: user.id } })
13
+ return !!getBlockingAction(results?.actions)
14
14
  }
15
15
 
16
16
  function checkUserAndSetUser (tracer, user) {
@@ -41,8 +41,7 @@ function newStore () {
41
41
  wafErrorCode: null,
42
42
  raspErrorCode: null,
43
43
  wafVersion: null,
44
- rulesVersion: null,
45
- ruleTriggered: null
44
+ rulesVersion: null
46
45
  }
47
46
  }
48
47
  }
@@ -49,10 +49,6 @@ function trackRaspMetrics (store, metrics, raspRule) {
49
49
  telemetryMetrics.rulesVersion = metrics.rulesVersion
50
50
  }
51
51
 
52
- if (metrics.ruleTriggered) {
53
- telemetryMetrics.ruleTriggered = true
54
- }
55
-
56
52
  appsecMetrics.count('rasp.rule.eval', tags).inc(1)
57
53
 
58
54
  if (metrics.errorCode) {
@@ -68,7 +64,6 @@ function trackRaspMetrics (store, metrics, raspRule) {
68
64
 
69
65
  function trackRaspRuleMatch (store, raspRule, blockTriggered, blocked) {
70
66
  const telemetryMetrics = store[DD_TELEMETRY_REQUEST_METRICS]
71
- if (!telemetryMetrics.ruleTriggered) return
72
67
 
73
68
  const tags = {
74
69
  waf_version: telemetryMetrics.wafVersion,
@@ -82,10 +77,6 @@ function trackRaspRuleMatch (store, raspRule, blockTriggered, blocked) {
82
77
  }
83
78
 
84
79
  appsecMetrics.count('rasp.rule.match', tags).inc(1)
85
-
86
- // this is needed to not count it twice for the same match
87
- // but it also means it can only be called once per waf call even if there are multiple rasp match
88
- telemetryMetrics.ruleTriggered = null
89
80
  }
90
81
 
91
82
  function trackRaspRuleSkipped (raspRule, reason) {
@@ -19,7 +19,7 @@ class WAFContextWrapper {
19
19
  this.rulesVersion = rulesVersion
20
20
  this.knownAddresses = knownAddresses
21
21
  this.addressesToSkip = new Set()
22
- this.cachedUserIdActions = new Map()
22
+ this.cachedUserIdResults = new Map()
23
23
  }
24
24
 
25
25
  run ({ persistent, ephemeral }, raspRule) {
@@ -36,9 +36,9 @@ class WAFContextWrapper {
36
36
  // TODO: make this universal
37
37
  const userId = persistent?.[addresses.USER_ID] || ephemeral?.[addresses.USER_ID]
38
38
  if (userId) {
39
- const cachedAction = this.cachedUserIdActions.get(userId)
40
- if (cachedAction) {
41
- return cachedAction
39
+ const cachedResults = this.cachedUserIdResults.get(userId)
40
+ if (cachedResults) {
41
+ return cachedResults
42
42
  }
43
43
  }
44
44
 
@@ -142,7 +142,7 @@ class WAFContextWrapper {
142
142
 
143
143
  Reporter.reportDerivatives(result.derivatives)
144
144
 
145
- return result.actions
145
+ return result
146
146
  } catch (err) {
147
147
  log.error('[ASM] Error while running the AppSec WAF', err)
148
148
 
@@ -168,7 +168,7 @@ class WAFContextWrapper {
168
168
  const parameter = match.parameters[k]
169
169
 
170
170
  if (parameter?.address === addresses.USER_ID) {
171
- this.cachedUserIdActions.set(userId, result.actions)
171
+ this.cachedUserIdResults.set(userId, result)
172
172
  return
173
173
  }
174
174
  }
@@ -139,7 +139,7 @@ const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private
139
139
  // eslint-disable-next-line @stylistic/js/max-len
140
140
  const defaultWafObfuscatorKeyRegex = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\\.net[_-]sessionid|sid|jwt'
141
141
  // eslint-disable-next-line @stylistic/js/max-len
142
- const defaultWafObfuscatorValueRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}'
142
+ const defaultWafObfuscatorValueRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=([^;&]+)|"\\s*:\\s*("[^"]+"|\\d+))|bearer\\s+([a-z0-9\\._\\-]+)|token\\s*:\\s*([a-z0-9]{13})|gh[opsu]_([0-9a-zA-Z]{36})|ey[I-L][\\w=-]+\\.(ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?)|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}([^\\-]+)[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*([a-z0-9\\/\\.+]{100,})'
143
143
  const runtimeId = uuid()
144
144
 
145
145
  function maybeFile (filepath) {
@@ -5,10 +5,35 @@ const { getGeneratedPosition } = require('./source-maps')
5
5
  const session = require('./session')
6
6
  const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
7
7
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
8
- const { findScriptFromPartialPath, locationToBreakpoint, breakpointToProbes, probeToLocation } = require('./state')
8
+ const {
9
+ findScriptFromPartialPath,
10
+ clearState,
11
+ locationToBreakpoint,
12
+ breakpointToProbes,
13
+ probeToLocation
14
+ } = require('./state')
9
15
  const log = require('../../log')
10
16
 
11
17
  let sessionStarted = false
18
+ const probes = new Map()
19
+ let scriptLoadingStabilizedResolve
20
+ const scriptLoadingStabilized = new Promise((resolve) => { scriptLoadingStabilizedResolve = resolve })
21
+
22
+ // There's a race condition when a probe is first added, where the actual script that the probe is supposed to match
23
+ // hasn't been loaded yet. This will result in either the probe not being added at all, or an incorrect script being
24
+ // matched as the probe target.
25
+ //
26
+ // Therefore, once new scripts has been loaded, all probes are re-evaluated. If the matched `scriptId` has changed, we
27
+ // simply remove the old probe (if it was added to the wrong script) and apply it again.
28
+ session.on('scriptLoadingStabilized', () => {
29
+ log.debug('[debugger:devtools_client] Re-evaluating probes')
30
+ scriptLoadingStabilizedResolve()
31
+ for (const probe of probes.values()) {
32
+ reEvaluateProbe(probe).catch(err => {
33
+ log.error('[debugger:devtools_client] Error re-evaluating probe %s', probe.id, err)
34
+ })
35
+ }
36
+ })
12
37
 
13
38
  module.exports = {
14
39
  addBreakpoint,
@@ -18,13 +43,14 @@ module.exports = {
18
43
  async function addBreakpoint (probe) {
19
44
  if (!sessionStarted) await start()
20
45
 
46
+ probes.set(probe.id, probe)
47
+
21
48
  const file = probe.where.sourceFile
22
49
  let lineNumber = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints
23
50
  let columnNumber = 0 // Probes do not contain/support column information
24
51
 
25
52
  // Optimize for sending data to /debugger/v1/input endpoint
26
53
  probe.location = { file, lines: [String(lineNumber)] }
27
- delete probe.where
28
54
 
29
55
  // Optimize for fast calculations when probe is hit
30
56
  probe.templateRequiresEvaluation = templateRequiresEvaluation(probe.segments)
@@ -47,6 +73,8 @@ async function addBreakpoint (probe) {
47
73
  if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`)
48
74
  const { url, scriptId, sourceMapURL, source } = script
49
75
 
76
+ probe.scriptId = scriptId // Needed for detecting script changes during re-evaluation
77
+
50
78
  if (sourceMapURL) {
51
79
  log.debug(
52
80
  '[debugger:devtools_client] Translating location using source map for %s:%d:%d (probe: %s, version: %d)',
@@ -109,6 +137,8 @@ async function removeBreakpoint ({ id }) {
109
137
  throw Error(`Unknown probe id: ${id}`)
110
138
  }
111
139
 
140
+ probes.delete(id)
141
+
112
142
  const release = await lock()
113
143
 
114
144
  try {
@@ -173,25 +203,47 @@ async function updateBreakpoint (breakpoint, probe) {
173
203
  }
174
204
  }
175
205
 
176
- function start () {
206
+ async function reEvaluateProbe (probe) {
207
+ const script = findScriptFromPartialPath(probe.where.sourceFile)
208
+ log.debug('[debugger:devtools_client] re-evaluating probe %s: %s => %s', probe.id, probe.scriptId, script?.scriptId)
209
+
210
+ if (probe.scriptId !== script?.scriptId) {
211
+ log.debug('[debugger:devtools_client] Better match found for probe %s, re-evaluating', probe.id)
212
+ if (probeToLocation.has(probe.id)) {
213
+ await removeBreakpoint(probe)
214
+ }
215
+ await addBreakpoint(probe)
216
+ }
217
+ }
218
+
219
+ async function start () {
177
220
  sessionStarted = true
178
221
  log.debug('[debugger:devtools_client] Starting debugger')
179
- return session.post('Debugger.enable')
222
+ await session.post('Debugger.enable')
223
+
224
+ // Wait until there's a pause in script-loading to avoid accidentally adding probes to incorrect scripts. This is not
225
+ // a guarantee, but best effort.
226
+ log.debug('[debugger:devtools_client] Waiting for script-loading to stabilize')
227
+ await scriptLoadingStabilized
228
+ log.debug('[debugger:devtools_client] Script loading stabilized')
180
229
  }
181
230
 
182
231
  function stop () {
183
232
  sessionStarted = false
233
+ clearState()
184
234
  log.debug('[debugger:devtools_client] Stopping debugger')
185
235
  return session.post('Debugger.disable')
186
236
  }
187
237
 
188
238
  // Only if all probes have a condition can we use a compound condition.
189
239
  // Otherwise, we need to evaluate each probe individually once the breakpoint is hit.
190
- // TODO: Handle errors - if there's 2 conditons, and one fails but the other returns true, we should still pause the
191
- // breakpoint
192
240
  function compileCompoundCondition (probes) {
241
+ if (probes.length === 1) return probes[0].condition
242
+
193
243
  return probes.every(p => p.condition)
194
- ? probes.map(p => p.condition).filter(Boolean).join(' || ')
244
+ ? probes
245
+ .map((p) => `(() => { try { return ${p.condition} } catch { return false } })()`)
246
+ .join(' || ')
195
247
  : undefined
196
248
  }
197
249
 
@@ -6,7 +6,7 @@ const session = require('./session')
6
6
  const { getLocalStateForCallFrame } = require('./snapshot')
7
7
  const send = require('./send')
8
8
  const { getStackFromCallFrames } = require('./state')
9
- const { ackEmitting, ackError } = require('./status')
9
+ const { ackEmitting } = require('./status')
10
10
  const { parentThreadId } = require('./config')
11
11
  const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./defaults')
12
12
  const log = require('../../log')
@@ -36,7 +36,6 @@ const getDDTagsExpression = `(() => {
36
36
  const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}`
37
37
  const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentThreadId}`
38
38
 
39
- const SUPPORT_ITERATOR_METHODS = NODE_MAJOR >= 22
40
39
  const SUPPORT_ARRAY_BUFFER_RESIZE = NODE_MAJOR >= 20
41
40
  const oneSecondNs = 1_000_000_000n
42
41
  let globalSnapshotSamplingRateWindowStart = 0n
@@ -77,14 +76,6 @@ session.on('Debugger.paused', async ({ params }) => {
77
76
  }
78
77
  }
79
78
 
80
- // If all the probes have a condition, we know that it triggered. If at least one probe doesn't have a condition, we
81
- // need to verify which conditions are met.
82
- const shouldVerifyConditions = (
83
- SUPPORT_ITERATOR_METHODS
84
- ? probesAtLocation.values()
85
- : Array.from(probesAtLocation.values())
86
- ).some((probe) => probe.condition === undefined)
87
-
88
79
  for (const probe of probesAtLocation.values()) {
89
80
  if (start - probe.lastCaptureNs < probe.nsBetweenSampling) {
90
81
  continue
@@ -109,7 +100,7 @@ session.on('Debugger.paused', async ({ params }) => {
109
100
  maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
110
101
  }
111
102
 
112
- if (shouldVerifyConditions && probe.condition !== undefined) {
103
+ if (probe.condition !== undefined) {
113
104
  // TODO: Bundle all conditions and evaluate them in a single call
114
105
  // TODO: Handle errors
115
106
  const { result } = await session.post('Debugger.evaluateOnCallFrame', {
@@ -153,20 +144,11 @@ session.on('Debugger.paused', async ({ params }) => {
153
144
  evalResults = result?.value ?? []
154
145
  }
155
146
 
156
- let processLocalState
157
- if (numberOfProbesWithSnapshots !== 0) {
158
- try {
159
- // TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
160
- processLocalState = await getLocalStateForCallFrame(
161
- params.callFrames[0],
162
- { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
163
- )
164
- } catch (err) {
165
- for (let i = 0; i < numberOfProbesWithSnapshots; i++) {
166
- ackError(err, probes[snapshotProbeIndex[i]]) // TODO: Ok to continue after sending ackError?
167
- }
168
- }
169
- }
147
+ // TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
148
+ const processLocalState = numberOfProbesWithSnapshots !== 0 && await getLocalStateForCallFrame(
149
+ params.callFrames[0],
150
+ { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
151
+ )
170
152
 
171
153
  await session.post('Debugger.resume')
172
154
  const diff = process.hrtime.bigint() - start // TODO: Recored as telemetry (DEBUG-2858)
@@ -206,7 +188,9 @@ session.on('Debugger.paused', async ({ params }) => {
206
188
 
207
189
  if (probe.captureSnapshot) {
208
190
  const state = processLocalState()
209
- if (state) {
191
+ if (state instanceof Error) {
192
+ snapshot.captureError = state.message
193
+ } else if (state) {
210
194
  snapshot.captures = {
211
195
  lines: { [probe.location.lines[0]]: { locals: state } }
212
196
  }
@@ -13,7 +13,8 @@ const { version } = require('../../../../../package.json')
13
13
  module.exports = send
14
14
 
15
15
  const MAX_MESSAGE_LENGTH = 8 * 1024 // 8KB
16
- const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB
16
+ const MAX_LOG_PAYLOAD_SIZE_MB = 1
17
+ const MAX_LOG_PAYLOAD_SIZE_BYTES = MAX_LOG_PAYLOAD_SIZE_MB * 1024 * 1024
17
18
 
18
19
  const ddsource = 'dd_debugger'
19
20
  const hostname = getHostname()
@@ -48,13 +49,13 @@ function send (message, logger, dd, snapshot) {
48
49
  let json = JSON.stringify(payload)
49
50
  let size = Buffer.byteLength(json)
50
51
 
51
- if (size > MAX_LOG_PAYLOAD_SIZE) {
52
+ if (size > MAX_LOG_PAYLOAD_SIZE_BYTES) {
52
53
  // TODO: This is a very crude way to handle large payloads. Proper pruning will be implemented later (DEBUG-2624)
53
- const line = Object.values(payload.debugger.snapshot.captures.lines)[0]
54
- line.locals = {
55
- notCapturedReason: 'Snapshot was too large',
56
- size: Object.keys(line.locals).length
57
- }
54
+ delete payload.debugger.snapshot.captures
55
+ payload.debugger.snapshot.captureError =
56
+ `Snapshot was too large (max allowed size is ${MAX_LOG_PAYLOAD_SIZE_MB} MiB). ` +
57
+ 'Consider reducing the capture depth or turn off "Capture Variables" completely, ' +
58
+ 'and instead include the variables of interest directly in the message template.'
58
59
  json = JSON.stringify(payload)
59
60
  size = Buffer.byteLength(json)
60
61
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { getRuntimeObject } = require('./collector')
4
4
  const { processRawState } = require('./processor')
5
+ const log = require('../../../log')
5
6
 
6
7
  const DEFAULT_MAX_REFERENCE_DEPTH = 3
7
8
  const DEFAULT_MAX_COLLECTION_SIZE = 100
@@ -24,13 +25,20 @@ async function getLocalStateForCallFrame (
24
25
  const rawState = []
25
26
  let processedState = null
26
27
 
27
- await Promise.all(callFrame.scopeChain.map(async (scope) => {
28
- if (scope.type === 'global') return // The global scope is too noisy
29
- rawState.push(...await getRuntimeObject(
30
- scope.object.objectId,
31
- { maxReferenceDepth, maxCollectionSize, maxFieldCount }
32
- ))
33
- }))
28
+ try {
29
+ await Promise.all(callFrame.scopeChain.map(async (scope) => {
30
+ if (scope.type === 'global') return // The global scope is too noisy
31
+ rawState.push(...await getRuntimeObject(
32
+ scope.object.objectId,
33
+ { maxReferenceDepth, maxCollectionSize, maxFieldCount }
34
+ ))
35
+ }))
36
+ } catch (err) {
37
+ // TODO: We might be able to get part of the scope chain.
38
+ // Consider if we could set errors just for the part of the scope chain that throws during collection.
39
+ log.error('[debugger:devtools_client] Error getting local state for call frame', err)
40
+ return () => new Error('Error getting local state')
41
+ }
34
42
 
35
43
  // Deplay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
36
44
  return () => {
@@ -9,12 +9,16 @@ const WINDOWS_DRIVE_LETTER_REGEX = /[a-zA-Z]/
9
9
 
10
10
  const loadedScripts = []
11
11
  const scriptUrls = new Map()
12
+ let reEvaluateProbesTimer = null
12
13
 
13
14
  module.exports = {
14
15
  locationToBreakpoint: new Map(),
15
16
  breakpointToProbes: new Map(),
16
17
  probeToLocation: new Map(),
17
18
 
19
+ _loadedScripts: loadedScripts, // Only exposed for testing
20
+ _scriptUrls: scriptUrls, // Only exposed for testing
21
+
18
22
  /**
19
23
  * Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths.
20
24
  *
@@ -93,6 +97,10 @@ module.exports = {
93
97
 
94
98
  getStackFromCallFrames (callFrames) {
95
99
  return callFrames.map((frame) => {
100
+ // TODO: Possible race condition: If the breakpoint is in the process of being removed, and this is the last
101
+ // breakpoint, it will also stop the debugging session, which in turn will clear the state, which means clearing
102
+ // the `scriptUrls` map. That might result in this the `scriptUrls.get` call above returning `undefined`, which
103
+ // will throw when `startsWith` is called on it.
96
104
  let fileName = scriptUrls.get(frame.location.scriptId)
97
105
  if (fileName.startsWith('file://')) fileName = fileName.substr(7) // TODO: This might not be required
98
106
  return {
@@ -102,6 +110,14 @@ module.exports = {
102
110
  columnNumber: frame.location.columnNumber + 1 // Beware! columnNumber is zero-indexed
103
111
  }
104
112
  })
113
+ },
114
+
115
+ // The maps locationToBreakpoint, breakpointToProbes, and probeToLocation are always updated when breakpoints are
116
+ // removed. Therefore they do not need to get manually cleared. Only the state internal to this file needs to be
117
+ // cleared.
118
+ clearState () {
119
+ loadedScripts.length = 0
120
+ scriptUrls.clear()
105
121
  }
106
122
  }
107
123
 
@@ -112,7 +128,6 @@ module.exports = {
112
128
  // Unknown params.url values:
113
129
  // - `structured-stack` - Not sure what this is, but should just be ignored
114
130
  // - `` - Not sure what this is, but should just be ignored
115
- // TODO: Event fired for all files, every time debugger is enabled. So when we disable it, we need to reset the state
116
131
  session.on('Debugger.scriptParsed', ({ params }) => {
117
132
  scriptUrls.set(params.scriptId, params.url)
118
133
  if (params.url.startsWith('file:')) {
@@ -142,5 +157,10 @@ session.on('Debugger.scriptParsed', ({ params }) => {
142
157
  } else {
143
158
  loadedScripts.push(params)
144
159
  }
160
+
161
+ clearTimeout(reEvaluateProbesTimer)
162
+ reEvaluateProbesTimer = setTimeout(() => {
163
+ session.emit('scriptLoadingStabilized')
164
+ }, 500)
145
165
  }
146
166
  })
@@ -392,6 +392,8 @@ class CustomMetrics {
392
392
  * These are translated into [ 'tagName:tagValue' ] for internal use
393
393
  */
394
394
  static tagTranslator (objTags) {
395
+ if (Array.isArray(objTags)) return objTags
396
+
395
397
  const arrTags = []
396
398
 
397
399
  if (!objTags) return arrTags
@@ -148,12 +148,12 @@ class LLMObsTagger {
148
148
  }
149
149
 
150
150
  tagSpanTags (span, tags) {
151
- // new tags will be merged with existing tags
152
151
  const currentTags = registry.get(span)?.[TAGS]
153
152
  if (currentTags) {
154
- Object.assign(tags, currentTags)
153
+ Object.assign(currentTags, tags)
154
+ } else {
155
+ this._setTag(span, TAGS, tags)
155
156
  }
156
- this._setTag(span, TAGS, tags)
157
157
  }
158
158
 
159
159
  changeKind (span, newKind) {
@@ -48,6 +48,7 @@ module.exports = {
48
48
  get http2 () { return require('../../../datadog-plugin-http2/src') },
49
49
  get https () { return require('../../../datadog-plugin-http/src') },
50
50
  get ioredis () { return require('../../../datadog-plugin-ioredis/src') },
51
+ get iovalkey () { return require('../../../datadog-plugin-iovalkey/src') },
51
52
  get 'jest-circus' () { return require('../../../datadog-plugin-jest/src') },
52
53
  get 'jest-config' () { return require('../../../datadog-plugin-jest/src') },
53
54
  get 'jest-environment-node' () { return require('../../../datadog-plugin-jest/src') },
@@ -152,10 +152,6 @@ class Tracer extends NoopProxy {
152
152
  }
153
153
  }
154
154
 
155
- if (config.isGCPFunction || config.isAzureFunction) {
156
- require('./serverless').maybeStartServerlessMiniAgent(config)
157
- }
158
-
159
155
  if (config.profiling.enabled !== 'false') {
160
156
  const { SSIHeuristics } = require('./profiling/ssi-heuristics')
161
157
  const ssiHeuristics = new SSIHeuristics(config)
@@ -1,51 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const log = require('./log')
4
-
5
- function maybeStartServerlessMiniAgent (config) {
6
- if (process.platform !== 'win32' && process.platform !== 'linux') {
7
- log.error('Serverless Mini Agent is only supported on Windows and Linux.')
8
- return
9
- }
10
-
11
- const rustBinaryPath = getRustBinaryPath(config)
12
-
13
- const fs = require('fs')
14
-
15
- log.debug(`Trying to spawn the Serverless Mini Agent at path: ${rustBinaryPath}`)
16
-
17
- // trying to spawn with an invalid path will return a non-descriptive error, so we want to catch
18
- // invalid paths and log our own error.
19
- if (!fs.existsSync(rustBinaryPath)) {
20
- log.error('Serverless Mini Agent did not start. Could not find mini agent binary.')
21
- return
22
- }
23
- try {
24
- require('child_process').spawn(rustBinaryPath, { stdio: 'inherit' })
25
- } catch (err) {
26
- log.error('Error spawning mini agent process: %s', err.message)
27
- }
28
- }
29
-
30
- function getRustBinaryPath (config) {
31
- if (process.env.DD_MINI_AGENT_PATH !== undefined) {
32
- return process.env.DD_MINI_AGENT_PATH
33
- }
34
-
35
- const rustBinaryPathRoot = config.isGCPFunction ? '/workspace' : '/home/site/wwwroot'
36
- const rustBinaryPathOsFolder = process.platform === 'win32'
37
- ? 'datadog-serverless-agent-windows-amd64'
38
- : 'datadog-serverless-agent-linux-amd64'
39
-
40
- const rustBinaryExtension = process.platform === 'win32' ? '.exe' : ''
41
-
42
- const rustBinaryPath =
43
- `${rustBinaryPathRoot}/node_modules/@datadog/sma/${rustBinaryPathOsFolder}/\
44
- datadog-serverless-trace-mini-agent${rustBinaryExtension}`
45
-
46
- return rustBinaryPath
47
- }
48
-
49
3
  function getIsGCPFunction () {
50
4
  const isDeprecatedGCPFunction = process.env.FUNCTION_NAME !== undefined && process.env.GCP_PROJECT !== undefined
51
5
  const isNewerGCPFunction = process.env.K_SERVICE !== undefined && process.env.FUNCTION_TARGET !== undefined
@@ -69,9 +23,7 @@ function isInServerlessEnvironment () {
69
23
  }
70
24
 
71
25
  module.exports = {
72
- maybeStartServerlessMiniAgent,
73
26
  getIsGCPFunction,
74
27
  getIsAzureFunction,
75
- getRustBinaryPath,
76
28
  isInServerlessEnvironment
77
29
  }
@@ -35,6 +35,13 @@ const redisConfig = {
35
35
  }
36
36
  }
37
37
 
38
+ const valkeyConfig = {
39
+ opName: () => 'valkey.command',
40
+ serviceName: ({ tracerService, pluginConfig, system, connectionName }) => {
41
+ return getRedisService(pluginConfig, connectionName) || fromSystem(tracerService, system)
42
+ }
43
+ }
44
+
38
45
  const storage = {
39
46
  client: {
40
47
  aerospike: {
@@ -57,6 +64,7 @@ const storage = {
57
64
  pluginConfig.service || `${tracerService}-elasticsearch`
58
65
  },
59
66
  ioredis: redisConfig,
67
+ iovalkey: valkeyConfig,
60
68
  mariadb: {
61
69
  opName: () => 'mariadb.query',
62
70
  serviceName: mysqlServiceName
@@ -38,6 +38,10 @@ const storage = {
38
38
  serviceName: configWithFallback
39
39
  },
40
40
  ioredis: redisNaming,
41
+ iovalkey: {
42
+ opName: () => 'valkey.command',
43
+ serviceName: configWithFallback
44
+ },
41
45
  mariadb: {
42
46
  opName: () => 'mariadb.query',
43
47
  serviceName: withFunction