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.
- package/README.md +5 -0
- package/index.d.ts +54 -6
- package/package.json +1 -1
- package/packages/datadog-instrumentations/src/amqplib.js +8 -5
- package/packages/datadog-instrumentations/src/child_process.js +2 -1
- package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +16 -1
- package/packages/datadog-instrumentations/src/couchbase.js +2 -1
- package/packages/datadog-instrumentations/src/cucumber.js +41 -46
- package/packages/datadog-instrumentations/src/express.js +2 -6
- package/packages/datadog-instrumentations/src/fs.js +6 -5
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +17 -12
- package/packages/datadog-instrumentations/src/http/client.js +2 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +51 -0
- package/packages/datadog-instrumentations/src/jest.js +49 -41
- package/packages/datadog-instrumentations/src/kafkajs.js +21 -8
- package/packages/datadog-instrumentations/src/mocha/main.js +33 -46
- package/packages/datadog-instrumentations/src/mocha/utils.js +72 -75
- package/packages/datadog-instrumentations/src/mysql2.js +3 -1
- package/packages/datadog-instrumentations/src/net.js +3 -1
- package/packages/datadog-instrumentations/src/next.js +6 -14
- package/packages/datadog-instrumentations/src/pg.js +5 -11
- package/packages/datadog-instrumentations/src/playwright.js +60 -69
- package/packages/datadog-instrumentations/src/url.js +9 -17
- package/packages/datadog-instrumentations/src/vitest.js +55 -75
- package/packages/datadog-plugin-cucumber/src/index.js +29 -18
- package/packages/datadog-plugin-iovalkey/src/index.js +18 -0
- package/packages/datadog-plugin-jest/src/index.js +14 -8
- package/packages/datadog-plugin-kafkajs/src/producer.js +8 -5
- package/packages/datadog-plugin-mocha/src/index.js +55 -35
- package/packages/datadog-plugin-playwright/src/index.js +26 -20
- package/packages/datadog-plugin-redis/src/index.js +8 -3
- package/packages/datadog-plugin-vitest/src/index.js +53 -42
- package/packages/datadog-shimmer/src/shimmer.js +164 -33
- package/packages/dd-trace/src/appsec/graphql.js +2 -2
- package/packages/dd-trace/src/appsec/index.js +14 -11
- package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
- package/packages/dd-trace/src/appsec/rasp/utils.js +11 -6
- package/packages/dd-trace/src/appsec/sdk/user_blocking.js +2 -2
- package/packages/dd-trace/src/appsec/telemetry/index.js +1 -2
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +0 -9
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +6 -6
- package/packages/dd-trace/src/config.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +59 -7
- package/packages/dd-trace/src/debugger/devtools_client/index.js +10 -26
- package/packages/dd-trace/src/debugger/devtools_client/send.js +8 -7
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +15 -7
- package/packages/dd-trace/src/debugger/devtools_client/state.js +21 -1
- package/packages/dd-trace/src/dogstatsd.js +2 -0
- package/packages/dd-trace/src/llmobs/tagger.js +3 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/proxy.js +0 -4
- package/packages/dd-trace/src/serverless.js +0 -48
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +8 -0
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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) {
|
|
@@ -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.
|
|
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
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
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
|
|
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.
|
|
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*=[
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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 >
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
})
|
|
@@ -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(
|
|
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
|