dd-trace 4.47.1 → 4.48.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/LICENSE-3rdparty.csv +0 -1
- package/ext/types.d.ts +1 -0
- package/ext/types.js +1 -0
- package/index.d.ts +26 -0
- package/package.json +6 -7
- package/packages/datadog-code-origin/index.js +38 -0
- package/packages/datadog-core/index.js +2 -2
- package/packages/datadog-instrumentations/src/avsc.js +37 -0
- package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
- package/packages/datadog-instrumentations/src/child_process.js +17 -8
- package/packages/datadog-instrumentations/src/express.js +37 -4
- package/packages/datadog-instrumentations/src/fastify.js +12 -1
- package/packages/datadog-instrumentations/src/fs.js +27 -7
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/jest.js +2 -1
- package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
- package/packages/datadog-instrumentations/src/mysql2.js +220 -1
- package/packages/datadog-instrumentations/src/protobufjs.js +127 -0
- package/packages/datadog-instrumentations/src/winston.js +22 -0
- package/packages/datadog-plugin-avsc/src/index.js +9 -0
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +169 -0
- package/packages/datadog-plugin-azure-functions/src/index.js +77 -0
- package/packages/datadog-plugin-fastify/src/code_origin.js +31 -0
- package/packages/datadog-plugin-fastify/src/index.js +10 -12
- package/packages/datadog-plugin-fastify/src/tracing.js +19 -0
- package/packages/datadog-plugin-protobufjs/src/index.js +14 -0
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +180 -0
- package/packages/dd-trace/src/appsec/addresses.js +6 -1
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +13 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +8 -1
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
- package/packages/dd-trace/src/appsec/iast/index.js +3 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +15 -0
- package/packages/dd-trace/src/appsec/index.js +58 -43
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +99 -0
- package/packages/dd-trace/src/appsec/rasp/index.js +24 -10
- package/packages/dd-trace/src/appsec/rasp/lfi.js +112 -0
- package/packages/dd-trace/src/appsec/rasp/sql_injection.js +24 -4
- package/packages/dd-trace/src/appsec/rasp/utils.js +2 -1
- package/packages/dd-trace/src/appsec/recommended.json +2 -4
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +5 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +8 -0
- package/packages/dd-trace/src/appsec/reporter.js +12 -5
- package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -0
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -14
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +53 -0
- package/packages/dd-trace/src/config.js +12 -1
- package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +25 -17
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +56 -5
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +4 -4
- package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +153 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +30 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +241 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +10 -4
- package/packages/dd-trace/src/exporters/common/request.js +8 -34
- package/packages/dd-trace/src/exporters/common/url-to-http-options-polyfill.js +31 -0
- package/packages/dd-trace/src/payload-tagging/index.js +1 -1
- package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +2094 -0
- package/packages/dd-trace/src/plugin_manager.js +4 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +2 -0
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
- package/packages/dd-trace/src/plugins/schema.js +35 -0
- package/packages/dd-trace/src/plugins/util/ci.js +23 -1
- package/packages/dd-trace/src/plugins/util/serverless.js +7 -0
- package/packages/dd-trace/src/plugins/util/stacktrace.js +94 -0
- package/packages/dd-trace/src/plugins/util/tags.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +20 -22
- package/packages/dd-trace/src/plugins/util/web.js +6 -4
- package/packages/dd-trace/src/profiling/profiler.js +24 -14
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +94 -66
- package/packages/dd-trace/src/proxy.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-core/src/storage/async_resource.js +0 -108
- package/packages/datadog-core/src/storage/index.js +0 -5
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const { randomUUID } = require('crypto')
|
|
4
4
|
const { breakpoints } = require('./state')
|
|
5
5
|
const session = require('./session')
|
|
6
|
+
const { getLocalStateForCallFrame } = require('./snapshot')
|
|
6
7
|
const send = require('./send')
|
|
7
|
-
const {
|
|
8
|
+
const { getScriptUrlFromId } = require('./state')
|
|
9
|
+
const { ackEmitting, ackError } = require('./status')
|
|
8
10
|
const { parentThreadId } = require('./config')
|
|
9
11
|
const log = require('../../log')
|
|
10
12
|
const { version } = require('../../../../../package.json')
|
|
@@ -19,9 +21,33 @@ const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentT
|
|
|
19
21
|
session.on('Debugger.paused', async ({ params }) => {
|
|
20
22
|
const start = process.hrtime.bigint()
|
|
21
23
|
const timestamp = Date.now()
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
let captureSnapshotForProbe = null
|
|
26
|
+
let maxReferenceDepth, maxLength
|
|
27
|
+
const probes = params.hitBreakpoints.map((id) => {
|
|
28
|
+
const probe = breakpoints.get(id)
|
|
29
|
+
if (probe.captureSnapshot) {
|
|
30
|
+
captureSnapshotForProbe = probe
|
|
31
|
+
maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth)
|
|
32
|
+
maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
|
|
33
|
+
}
|
|
34
|
+
return probe
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
let processLocalState
|
|
38
|
+
if (captureSnapshotForProbe !== null) {
|
|
39
|
+
try {
|
|
40
|
+
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
|
|
41
|
+
processLocalState = await getLocalStateForCallFrame(params.callFrames[0], { maxReferenceDepth, maxLength })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// TODO: This error is not tied to a specific probe, but to all probes with `captureSnapshot: true`.
|
|
44
|
+
// However, in 99,99% of cases, there will be just a single probe, so I guess this simplification is ok?
|
|
45
|
+
ackError(err, captureSnapshotForProbe) // TODO: Ok to continue after sending ackError?
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
await session.post('Debugger.resume')
|
|
24
|
-
const diff = process.hrtime.bigint() - start // TODO:
|
|
50
|
+
const diff = process.hrtime.bigint() - start // TODO: Recored as telemetry (DEBUG-2858)
|
|
25
51
|
|
|
26
52
|
log.debug(`Finished processing breakpoints - main thread paused for: ${Number(diff) / 1000000} ms`)
|
|
27
53
|
|
|
@@ -35,7 +61,18 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
35
61
|
thread_name: threadName
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
|
|
64
|
+
const stack = params.callFrames.map((frame) => {
|
|
65
|
+
let fileName = getScriptUrlFromId(frame.location.scriptId)
|
|
66
|
+
if (fileName.startsWith('file://')) fileName = fileName.substr(7) // TODO: This might not be required
|
|
67
|
+
return {
|
|
68
|
+
fileName,
|
|
69
|
+
function: frame.functionName,
|
|
70
|
+
lineNumber: frame.location.lineNumber + 1, // Beware! lineNumber is zero-indexed
|
|
71
|
+
columnNumber: frame.location.columnNumber + 1 // Beware! columnNumber is zero-indexed
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// TODO: Send multiple probes in one HTTP request as an array (DEBUG-2848)
|
|
39
76
|
for (const probe of probes) {
|
|
40
77
|
const snapshot = {
|
|
41
78
|
id: randomUUID(),
|
|
@@ -45,13 +82,27 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
45
82
|
version: probe.version,
|
|
46
83
|
location: probe.location
|
|
47
84
|
},
|
|
85
|
+
stack,
|
|
48
86
|
language: 'javascript'
|
|
49
87
|
}
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
if (probe.captureSnapshot) {
|
|
90
|
+
const state = processLocalState()
|
|
91
|
+
if (state) {
|
|
92
|
+
snapshot.captures = {
|
|
93
|
+
lines: { [probe.location.lines[0]]: { locals: state } }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// TODO: Process template (DEBUG-2628)
|
|
52
99
|
send(probe.template, logger, snapshot, (err) => {
|
|
53
100
|
if (err) log.error(err)
|
|
54
101
|
else ackEmitting(probe)
|
|
55
102
|
})
|
|
56
103
|
}
|
|
57
104
|
})
|
|
105
|
+
|
|
106
|
+
function highestOrUndefined (num, max) {
|
|
107
|
+
return num === undefined ? max : Math.max(num, max ?? 0)
|
|
108
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { workerData: { rcPort } } = require('node:worker_threads')
|
|
4
|
-
const {
|
|
4
|
+
const { findScriptFromPartialPath, probes, breakpoints } = require('./state')
|
|
5
5
|
const session = require('./session')
|
|
6
6
|
const { ackReceived, ackInstalled, ackError } = require('./status')
|
|
7
7
|
const log = require('../../log')
|
|
@@ -92,7 +92,7 @@ async function processMsg (action, probe) {
|
|
|
92
92
|
await addBreakpoint(probe)
|
|
93
93
|
break
|
|
94
94
|
case 'modify':
|
|
95
|
-
// TODO:
|
|
95
|
+
// TODO: Modify existing probe instead of removing it (DEBUG-2817)
|
|
96
96
|
await removeBreakpoint(probe)
|
|
97
97
|
await addBreakpoint(probe)
|
|
98
98
|
break
|
|
@@ -114,13 +114,13 @@ async function addBreakpoint (probe) {
|
|
|
114
114
|
const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints
|
|
115
115
|
|
|
116
116
|
// Optimize for sending data to /debugger/v1/input endpoint
|
|
117
|
-
probe.location = { file, lines: [line] }
|
|
117
|
+
probe.location = { file, lines: [String(line)] }
|
|
118
118
|
delete probe.where
|
|
119
119
|
|
|
120
120
|
// TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached.
|
|
121
121
|
// Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will
|
|
122
122
|
// not continue untill all scripts have been parsed?
|
|
123
|
-
const script =
|
|
123
|
+
const script = findScriptFromPartialPath(file)
|
|
124
124
|
if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`)
|
|
125
125
|
const [path, scriptId] = script
|
|
126
126
|
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { hostname: getHostname } = require('os')
|
|
4
|
+
const { stringify } = require('querystring')
|
|
5
|
+
|
|
3
6
|
const config = require('./config')
|
|
4
7
|
const request = require('../../exporters/common/request')
|
|
8
|
+
const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags')
|
|
5
9
|
|
|
6
10
|
module.exports = send
|
|
7
11
|
|
|
8
12
|
const ddsource = 'dd_debugger'
|
|
13
|
+
const hostname = getHostname()
|
|
9
14
|
const service = config.service
|
|
10
15
|
|
|
16
|
+
const ddtags = [
|
|
17
|
+
[GIT_COMMIT_SHA, config.commitSHA],
|
|
18
|
+
[GIT_REPOSITORY_URL, config.repositoryUrl]
|
|
19
|
+
].map((pair) => pair.join(':')).join(',')
|
|
20
|
+
|
|
21
|
+
const path = `/debugger/v1/input?${stringify({ ddtags })}`
|
|
22
|
+
|
|
11
23
|
function send (message, logger, snapshot, cb) {
|
|
12
24
|
const opts = {
|
|
13
25
|
method: 'POST',
|
|
14
26
|
url: config.url,
|
|
15
|
-
path
|
|
27
|
+
path,
|
|
16
28
|
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
const payload = {
|
|
20
32
|
ddsource,
|
|
33
|
+
hostname,
|
|
21
34
|
service,
|
|
22
35
|
message,
|
|
23
36
|
logger,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const session = require('../session')
|
|
4
|
+
|
|
5
|
+
const LEAF_SUBTYPES = new Set(['date', 'regexp'])
|
|
6
|
+
const ITERABLE_SUBTYPES = new Set(['map', 'set', 'weakmap', 'weakset'])
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
getRuntimeObject: getObject
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// TODO: Can we speed up thread pause time by calling mutiple Runtime.getProperties in parallel when possible?
|
|
13
|
+
// The most simple solution would be to swich from an async/await approach to a callback based approach, in which case
|
|
14
|
+
// each lookup will just finish in its own time and traverse the child nodes when the event loop allows it.
|
|
15
|
+
// Alternatively, use `Promise.all` or something like that, but the code would probably be more complex.
|
|
16
|
+
|
|
17
|
+
async function getObject (objectId, maxDepth, depth = 0) {
|
|
18
|
+
const { result, privateProperties } = await session.post('Runtime.getProperties', {
|
|
19
|
+
objectId,
|
|
20
|
+
ownProperties: true // exclude inherited properties
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (privateProperties) result.push(...privateProperties)
|
|
24
|
+
|
|
25
|
+
return traverseGetPropertiesResult(result, maxDepth, depth)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function traverseGetPropertiesResult (props, maxDepth, depth) {
|
|
29
|
+
// TODO: Decide if we should filter out non-enumerable properties or not:
|
|
30
|
+
// props = props.filter((e) => e.enumerable)
|
|
31
|
+
|
|
32
|
+
if (depth >= maxDepth) return props
|
|
33
|
+
|
|
34
|
+
for (const prop of props) {
|
|
35
|
+
if (prop.value === undefined) continue
|
|
36
|
+
const { value: { type, objectId, subtype } } = prop
|
|
37
|
+
if (type === 'object') {
|
|
38
|
+
if (objectId === undefined) continue // if `subtype` is "null"
|
|
39
|
+
if (LEAF_SUBTYPES.has(subtype)) continue // don't waste time with these subtypes
|
|
40
|
+
prop.value.properties = await getObjectProperties(subtype, objectId, maxDepth, depth)
|
|
41
|
+
} else if (type === 'function') {
|
|
42
|
+
prop.value.properties = await getFunctionProperties(objectId, maxDepth, depth + 1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return props
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getObjectProperties (subtype, objectId, maxDepth, depth) {
|
|
50
|
+
if (ITERABLE_SUBTYPES.has(subtype)) {
|
|
51
|
+
return getIterable(objectId, maxDepth, depth)
|
|
52
|
+
} else if (subtype === 'promise') {
|
|
53
|
+
return getInternalProperties(objectId, maxDepth, depth)
|
|
54
|
+
} else if (subtype === 'proxy') {
|
|
55
|
+
return getProxy(objectId, maxDepth, depth)
|
|
56
|
+
} else if (subtype === 'arraybuffer') {
|
|
57
|
+
return getArrayBuffer(objectId, maxDepth, depth)
|
|
58
|
+
} else {
|
|
59
|
+
return getObject(objectId, maxDepth, depth + 1)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// TODO: The following extra information from `internalProperties` might be relevant to include for functions:
|
|
64
|
+
// - Bound function: `[[TargetFunction]]`, `[[BoundThis]]` and `[[BoundArgs]]`
|
|
65
|
+
// - Non-bound function: `[[FunctionLocation]]`, and `[[Scopes]]`
|
|
66
|
+
async function getFunctionProperties (objectId, maxDepth, depth) {
|
|
67
|
+
let { result } = await session.post('Runtime.getProperties', {
|
|
68
|
+
objectId,
|
|
69
|
+
ownProperties: true // exclude inherited properties
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// For legacy reasons (I assume) functions has a `prototype` property besides the internal `[[Prototype]]`
|
|
73
|
+
result = result.filter(({ name }) => name !== 'prototype')
|
|
74
|
+
|
|
75
|
+
return traverseGetPropertiesResult(result, maxDepth, depth)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getIterable (objectId, maxDepth, depth) {
|
|
79
|
+
const { internalProperties } = await session.post('Runtime.getProperties', {
|
|
80
|
+
objectId,
|
|
81
|
+
ownProperties: true // exclude inherited properties
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
let entry = internalProperties[1]
|
|
85
|
+
if (entry.name !== '[[Entries]]') {
|
|
86
|
+
// Currently `[[Entries]]` is the last of 2 elements, but in case this ever changes, fall back to searching
|
|
87
|
+
entry = internalProperties.findLast(({ name }) => name === '[[Entries]]')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip the `[[Entries]]` level and go directly to the content of the iterable
|
|
91
|
+
const { result } = await session.post('Runtime.getProperties', {
|
|
92
|
+
objectId: entry.value.objectId,
|
|
93
|
+
ownProperties: true // exclude inherited properties
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return traverseGetPropertiesResult(result, maxDepth, depth)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getInternalProperties (objectId, maxDepth, depth) {
|
|
100
|
+
const { internalProperties } = await session.post('Runtime.getProperties', {
|
|
101
|
+
objectId,
|
|
102
|
+
ownProperties: true // exclude inherited properties
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// We want all internal properties except the prototype
|
|
106
|
+
const props = internalProperties.filter(({ name }) => name !== '[[Prototype]]')
|
|
107
|
+
|
|
108
|
+
return traverseGetPropertiesResult(props, maxDepth, depth)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getProxy (objectId, maxDepth, depth) {
|
|
112
|
+
const { internalProperties } = await session.post('Runtime.getProperties', {
|
|
113
|
+
objectId,
|
|
114
|
+
ownProperties: true // exclude inherited properties
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// TODO: If we do not skip the proxy wrapper, we can add a `revoked` boolean
|
|
118
|
+
let entry = internalProperties[1]
|
|
119
|
+
if (entry.name !== '[[Target]]') {
|
|
120
|
+
// Currently `[[Target]]` is the last of 2 elements, but in case this ever changes, fall back to searching
|
|
121
|
+
entry = internalProperties.findLast(({ name }) => name === '[[Target]]')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Skip the `[[Target]]` level and go directly to the target of the Proxy
|
|
125
|
+
const { result } = await session.post('Runtime.getProperties', {
|
|
126
|
+
objectId: entry.value.objectId,
|
|
127
|
+
ownProperties: true // exclude inherited properties
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return traverseGetPropertiesResult(result, maxDepth, depth)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Support for ArrayBuffer is a bit trickly because the internal structure stored in `internalProperties` is not
|
|
134
|
+
// documented and is not straight forward. E.g. ArrayBuffer(3) will internally contain both Int8Array(3) and
|
|
135
|
+
// UInt8Array(3), whereas ArrayBuffer(8) internally contains both Int8Array(8), Uint8Array(8), Int16Array(4), and
|
|
136
|
+
// Int32Array(2) - all representing the same data in different ways.
|
|
137
|
+
async function getArrayBuffer (objectId, maxDepth, depth) {
|
|
138
|
+
const { internalProperties } = await session.post('Runtime.getProperties', {
|
|
139
|
+
objectId,
|
|
140
|
+
ownProperties: true // exclude inherited properties
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Use Uint8 to make it easy to convert to a string later.
|
|
144
|
+
const entry = internalProperties.find(({ name }) => name === '[[Uint8Array]]')
|
|
145
|
+
|
|
146
|
+
// Skip the `[[Uint8Array]]` level and go directly to the content of the ArrayBuffer
|
|
147
|
+
const { result } = await session.post('Runtime.getProperties', {
|
|
148
|
+
objectId: entry.value.objectId,
|
|
149
|
+
ownProperties: true // exclude inherited properties
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return traverseGetPropertiesResult(result, maxDepth, depth)
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getRuntimeObject } = require('./collector')
|
|
4
|
+
const { processRawState } = require('./processor')
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_REFERENCE_DEPTH = 3
|
|
7
|
+
const DEFAULT_MAX_LENGTH = 255
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
getLocalStateForCallFrame
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function getLocalStateForCallFrame (
|
|
14
|
+
callFrame,
|
|
15
|
+
{ maxReferenceDepth = DEFAULT_MAX_REFERENCE_DEPTH, maxLength = DEFAULT_MAX_LENGTH } = {}
|
|
16
|
+
) {
|
|
17
|
+
const rawState = []
|
|
18
|
+
let processedState = null
|
|
19
|
+
|
|
20
|
+
for (const scope of callFrame.scopeChain) {
|
|
21
|
+
if (scope.type === 'global') continue // The global scope is too noisy
|
|
22
|
+
rawState.push(...await getRuntimeObject(scope.object.objectId, maxReferenceDepth))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Deplay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
|
|
26
|
+
return () => {
|
|
27
|
+
processedState = processedState ?? processRawState(rawState, maxLength)
|
|
28
|
+
return processedState
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
processRawState: processProperties
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Matches classes in source code, no matter how it's written:
|
|
8
|
+
// - Named: class MyClass {}
|
|
9
|
+
// - Anonymous: class {}
|
|
10
|
+
// - Named, with odd whitespace: class\n\t MyClass\n{}
|
|
11
|
+
// - Anonymous, with odd whitespace: class\n{}
|
|
12
|
+
const CLASS_REGEX = /^class\s([^{]*)/
|
|
13
|
+
|
|
14
|
+
function processProperties (props, maxLength) {
|
|
15
|
+
const result = {}
|
|
16
|
+
|
|
17
|
+
for (const prop of props) {
|
|
18
|
+
// TODO: Hack to avoid periods in keys, as EVP doesn't support that. A better solution can be implemented later
|
|
19
|
+
result[prop.name.replaceAll('.', '_')] = getPropertyValue(prop, maxLength)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getPropertyValue (prop, maxLength) {
|
|
26
|
+
// Special case for getters and setters which does not have a value property
|
|
27
|
+
if ('get' in prop) {
|
|
28
|
+
const hasGet = prop.get.type !== 'undefined'
|
|
29
|
+
const hasSet = prop.set.type !== 'undefined'
|
|
30
|
+
if (hasGet && hasSet) return { type: 'getter/setter' }
|
|
31
|
+
if (hasGet) return { type: 'getter' }
|
|
32
|
+
if (hasSet) return { type: 'setter' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (prop.value?.type) {
|
|
36
|
+
case 'object':
|
|
37
|
+
return getObjectValue(prop.value, maxLength)
|
|
38
|
+
case 'function':
|
|
39
|
+
return toFunctionOrClass(prop.value, maxLength)
|
|
40
|
+
case undefined: // TODO: Add test for when a prop has no value. I think it's if it's defined after the breakpoint?
|
|
41
|
+
case 'undefined':
|
|
42
|
+
return { type: 'undefined' }
|
|
43
|
+
case 'string':
|
|
44
|
+
return toString(prop.value.value, maxLength)
|
|
45
|
+
case 'number':
|
|
46
|
+
return { type: 'number', value: prop.value.description } // use `descripton` to get it as string
|
|
47
|
+
case 'boolean':
|
|
48
|
+
return { type: 'boolean', value: prop.value.value === true ? 'true' : 'false' }
|
|
49
|
+
case 'symbol':
|
|
50
|
+
return { type: 'symbol', value: prop.value.description }
|
|
51
|
+
case 'bigint':
|
|
52
|
+
return { type: 'bigint', value: prop.value.description.slice(0, -1) } // remove trailing `n`
|
|
53
|
+
default:
|
|
54
|
+
// As of this writing, the Chrome DevTools Protocol doesn't allow any other types than the ones listed above, but
|
|
55
|
+
// in the future new ones might be added.
|
|
56
|
+
return { type: prop.value.type, notCapturedReason: 'Unsupported property type' }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getObjectValue (obj, maxLength) {
|
|
61
|
+
switch (obj.subtype) {
|
|
62
|
+
case undefined:
|
|
63
|
+
return toObject(obj.className, obj.properties, maxLength)
|
|
64
|
+
case 'array':
|
|
65
|
+
return toArray(obj.className, obj.properties, maxLength)
|
|
66
|
+
case 'null':
|
|
67
|
+
return { type: 'null', isNull: true }
|
|
68
|
+
// case 'node': // TODO: What does this subtype represent?
|
|
69
|
+
case 'regexp':
|
|
70
|
+
return { type: obj.className, value: obj.description }
|
|
71
|
+
case 'date':
|
|
72
|
+
// TODO: This looses millisecond resolution, as that's not retained in the `.toString()` representation contained
|
|
73
|
+
// in the `description` field. Unfortunately that's all we get from the Chrome DevTools Protocol.
|
|
74
|
+
return { type: obj.className, value: `${new Date(obj.description).toISOString().slice(0, -5)}Z` }
|
|
75
|
+
case 'map':
|
|
76
|
+
return toMap(obj.className, obj.properties, maxLength)
|
|
77
|
+
case 'set':
|
|
78
|
+
return toSet(obj.className, obj.properties, maxLength)
|
|
79
|
+
case 'weakmap':
|
|
80
|
+
return toMap(obj.className, obj.properties, maxLength)
|
|
81
|
+
case 'weakset':
|
|
82
|
+
return toSet(obj.className, obj.properties, maxLength)
|
|
83
|
+
// case 'iterator': // TODO: I've not been able to trigger this subtype
|
|
84
|
+
case 'generator':
|
|
85
|
+
// Use `subtype` instead of `className` to make it obvious it's a generator
|
|
86
|
+
return toObject(obj.subtype, obj.properties, maxLength)
|
|
87
|
+
case 'error':
|
|
88
|
+
// TODO: Convert stack trace to array to avoid string trunctation or disable truncation in this case?
|
|
89
|
+
return toObject(obj.className, obj.properties, maxLength)
|
|
90
|
+
case 'proxy':
|
|
91
|
+
// Use `desciption` instead of `className` as the `type` to get type of target object (`Proxy(Error)` vs `proxy`)
|
|
92
|
+
return toObject(obj.description, obj.properties, maxLength)
|
|
93
|
+
case 'promise':
|
|
94
|
+
return toObject(obj.className, obj.properties, maxLength)
|
|
95
|
+
case 'typedarray':
|
|
96
|
+
return toArray(obj.className, obj.properties, maxLength)
|
|
97
|
+
case 'arraybuffer':
|
|
98
|
+
return toArrayBuffer(obj.className, obj.properties, maxLength)
|
|
99
|
+
// case 'dataview': // TODO: Looks like the internal ArrayBuffer is only accessible via the `buffer` getter
|
|
100
|
+
// case 'webassemblymemory': // TODO: Looks like the internal ArrayBuffer is only accessible via the `buffer` getter
|
|
101
|
+
// case 'wasmvalue': // TODO: I've not been able to trigger this subtype
|
|
102
|
+
default:
|
|
103
|
+
// As of this writing, the Chrome DevTools Protocol doesn't allow any other subtypes than the ones listed above,
|
|
104
|
+
// but in the future new ones might be added.
|
|
105
|
+
return { type: obj.subtype, notCapturedReason: 'Unsupported object type' }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toFunctionOrClass (value, maxLength) {
|
|
110
|
+
const classMatch = value.description.match(CLASS_REGEX)
|
|
111
|
+
|
|
112
|
+
if (classMatch === null) {
|
|
113
|
+
// This is a function
|
|
114
|
+
// TODO: Would it make sense to detect if it's an arrow function or not?
|
|
115
|
+
return toObject(value.className, value.properties, maxLength)
|
|
116
|
+
} else {
|
|
117
|
+
// This is a class
|
|
118
|
+
const className = classMatch[1].trim()
|
|
119
|
+
return { type: className ? `class ${className}` : 'class' }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toString (str, maxLength) {
|
|
124
|
+
const size = str.length
|
|
125
|
+
|
|
126
|
+
if (size <= maxLength) {
|
|
127
|
+
return { type: 'string', value: str }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
type: 'string',
|
|
132
|
+
value: str.substr(0, maxLength),
|
|
133
|
+
truncated: true,
|
|
134
|
+
size
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toObject (type, props, maxLength) {
|
|
139
|
+
if (props === undefined) return notCapturedDepth(type)
|
|
140
|
+
return { type, fields: processProperties(props, maxLength) }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toArray (type, elements, maxLength) {
|
|
144
|
+
if (elements === undefined) return notCapturedDepth(type)
|
|
145
|
+
|
|
146
|
+
// Perf: Create array of expected size in advance (expect that it contains only one non-enumrable element)
|
|
147
|
+
const expectedLength = elements.length - 1
|
|
148
|
+
const result = { type, elements: new Array(expectedLength) }
|
|
149
|
+
|
|
150
|
+
let i = 0
|
|
151
|
+
for (const elm of elements) {
|
|
152
|
+
if (elm.enumerable === false) continue // the value of the `length` property should not be part of the array
|
|
153
|
+
result.elements[i++] = getPropertyValue(elm, maxLength)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Safe-guard in case there were more than one non-enumerable element
|
|
157
|
+
if (i < expectedLength) result.elements.length = i
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toMap (type, pairs, maxLength) {
|
|
163
|
+
if (pairs === undefined) return notCapturedDepth(type)
|
|
164
|
+
|
|
165
|
+
// Perf: Create array of expected size in advance (expect that it contains only one non-enumrable element)
|
|
166
|
+
const expectedLength = pairs.length - 1
|
|
167
|
+
const result = { type, entries: new Array(expectedLength) }
|
|
168
|
+
|
|
169
|
+
let i = 0
|
|
170
|
+
for (const pair of pairs) {
|
|
171
|
+
if (pair.enumerable === false) continue // the value of the `length` property should not be part of the map
|
|
172
|
+
// The following code is based on assumptions made when researching the output of the Chrome DevTools Protocol.
|
|
173
|
+
// There doesn't seem to be any documentation to back it up:
|
|
174
|
+
//
|
|
175
|
+
// `pair.value` is a special wrapper-object with subtype `internal#entry`. This can be skipped and we can go
|
|
176
|
+
// directly to its children, of which there will always be exactly two, the first containing the key, and the
|
|
177
|
+
// second containing the value of this entry of the Map.
|
|
178
|
+
const key = getPropertyValue(pair.value.properties[0], maxLength)
|
|
179
|
+
const val = getPropertyValue(pair.value.properties[1], maxLength)
|
|
180
|
+
result.entries[i++] = [key, val]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Safe-guard in case there were more than one non-enumerable element
|
|
184
|
+
if (i < expectedLength) result.entries.length = i
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toSet (type, values, maxLength) {
|
|
190
|
+
if (values === undefined) return notCapturedDepth(type)
|
|
191
|
+
|
|
192
|
+
// Perf: Create array of expected size in advance (expect that it contains only one non-enumrable element)
|
|
193
|
+
const expectedLength = values.length - 1
|
|
194
|
+
const result = { type, elements: new Array(expectedLength) }
|
|
195
|
+
|
|
196
|
+
let i = 0
|
|
197
|
+
for (const value of values) {
|
|
198
|
+
if (value.enumerable === false) continue // the value of the `length` property should not be part of the set
|
|
199
|
+
// The following code is based on assumptions made when researching the output of the Chrome DevTools Protocol.
|
|
200
|
+
// There doesn't seem to be any documentation to back it up:
|
|
201
|
+
//
|
|
202
|
+
// `value.value` is a special wrapper-object with subtype `internal#entry`. This can be skipped and we can go
|
|
203
|
+
// directly to its children, of which there will always be exactly one, which contain the actual value in this entry
|
|
204
|
+
// of the Set.
|
|
205
|
+
result.elements[i++] = getPropertyValue(value.value.properties[0], maxLength)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Safe-guard in case there were more than one non-enumerable element
|
|
209
|
+
if (i < expectedLength) result.elements.length = i
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function toArrayBuffer (type, bytes, maxLength) {
|
|
215
|
+
if (bytes === undefined) return notCapturedDepth(type)
|
|
216
|
+
|
|
217
|
+
const size = bytes.length
|
|
218
|
+
|
|
219
|
+
if (size > maxLength) {
|
|
220
|
+
return {
|
|
221
|
+
type,
|
|
222
|
+
value: arrayBufferToString(bytes, maxLength),
|
|
223
|
+
truncated: true,
|
|
224
|
+
size: bytes.length
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
return { type, value: arrayBufferToString(bytes, size) }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function arrayBufferToString (bytes, size) {
|
|
232
|
+
const buf = Buffer.allocUnsafe(size)
|
|
233
|
+
for (let i = 0; i < size; i++) {
|
|
234
|
+
buf[i] = bytes[i].value.value
|
|
235
|
+
}
|
|
236
|
+
return buf.toString()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function notCapturedDepth (type) {
|
|
240
|
+
return { type, notCapturedReason: 'depth' }
|
|
241
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const session = require('./session')
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const scriptIds = []
|
|
6
|
+
const scriptUrls = new Map()
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
probes: new Map(),
|
|
@@ -25,10 +26,14 @@ module.exports = {
|
|
|
25
26
|
* @param {string} path
|
|
26
27
|
* @returns {[string, string] | undefined}
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
-
return
|
|
29
|
+
findScriptFromPartialPath (path) {
|
|
30
|
+
return scriptIds
|
|
30
31
|
.filter(([url]) => url.endsWith(path))
|
|
31
32
|
.sort(([a], [b]) => a.length - b.length)[0]
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getScriptUrlFromId (id) {
|
|
36
|
+
return scriptUrls.get(id)
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -41,7 +46,8 @@ module.exports = {
|
|
|
41
46
|
// - `` - Not sure what this is, but should just be ignored
|
|
42
47
|
// TODO: Event fired for all files, every time debugger is enabled. So when we disable it, we need to reset the state
|
|
43
48
|
session.on('Debugger.scriptParsed', ({ params }) => {
|
|
49
|
+
scriptUrls.set(params.scriptId, params.url)
|
|
44
50
|
if (params.url.startsWith('file:')) {
|
|
45
|
-
|
|
51
|
+
scriptIds.push([params.url, params.scriptId])
|
|
46
52
|
}
|
|
47
53
|
})
|