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.
Files changed (84) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/ext/types.d.ts +1 -0
  3. package/ext/types.js +1 -0
  4. package/index.d.ts +26 -0
  5. package/package.json +6 -7
  6. package/packages/datadog-code-origin/index.js +38 -0
  7. package/packages/datadog-core/index.js +2 -2
  8. package/packages/datadog-instrumentations/src/avsc.js +37 -0
  9. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  10. package/packages/datadog-instrumentations/src/child_process.js +17 -8
  11. package/packages/datadog-instrumentations/src/express.js +37 -4
  12. package/packages/datadog-instrumentations/src/fastify.js +12 -1
  13. package/packages/datadog-instrumentations/src/fs.js +27 -7
  14. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  15. package/packages/datadog-instrumentations/src/jest.js +2 -1
  16. package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
  17. package/packages/datadog-instrumentations/src/mysql2.js +220 -1
  18. package/packages/datadog-instrumentations/src/protobufjs.js +127 -0
  19. package/packages/datadog-instrumentations/src/winston.js +22 -0
  20. package/packages/datadog-plugin-avsc/src/index.js +9 -0
  21. package/packages/datadog-plugin-avsc/src/schema_iterator.js +169 -0
  22. package/packages/datadog-plugin-azure-functions/src/index.js +77 -0
  23. package/packages/datadog-plugin-fastify/src/code_origin.js +31 -0
  24. package/packages/datadog-plugin-fastify/src/index.js +10 -12
  25. package/packages/datadog-plugin-fastify/src/tracing.js +19 -0
  26. package/packages/datadog-plugin-protobufjs/src/index.js +14 -0
  27. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +180 -0
  28. package/packages/dd-trace/src/appsec/addresses.js +6 -1
  29. package/packages/dd-trace/src/appsec/channels.js +5 -1
  30. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +13 -1
  31. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +8 -1
  32. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/index.js +3 -0
  34. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  35. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +15 -0
  36. package/packages/dd-trace/src/appsec/index.js +58 -43
  37. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +99 -0
  38. package/packages/dd-trace/src/appsec/rasp/index.js +24 -10
  39. package/packages/dd-trace/src/appsec/rasp/lfi.js +112 -0
  40. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +24 -4
  41. package/packages/dd-trace/src/appsec/rasp/utils.js +2 -1
  42. package/packages/dd-trace/src/appsec/recommended.json +2 -4
  43. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +5 -1
  44. package/packages/dd-trace/src/appsec/remote_config/index.js +8 -0
  45. package/packages/dd-trace/src/appsec/reporter.js +12 -5
  46. package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -0
  47. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  48. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -14
  49. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +53 -0
  50. package/packages/dd-trace/src/config.js +12 -1
  51. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +25 -17
  52. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  53. package/packages/dd-trace/src/debugger/devtools_client/index.js +56 -5
  54. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +4 -4
  55. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -1
  56. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +153 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +30 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +241 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/state.js +10 -4
  60. package/packages/dd-trace/src/exporters/common/request.js +8 -34
  61. package/packages/dd-trace/src/exporters/common/url-to-http-options-polyfill.js +31 -0
  62. package/packages/dd-trace/src/payload-tagging/index.js +1 -1
  63. package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +2094 -0
  64. package/packages/dd-trace/src/plugin_manager.js +4 -2
  65. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -0
  66. package/packages/dd-trace/src/plugins/index.js +3 -0
  67. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  68. package/packages/dd-trace/src/plugins/schema.js +35 -0
  69. package/packages/dd-trace/src/plugins/util/ci.js +23 -1
  70. package/packages/dd-trace/src/plugins/util/serverless.js +7 -0
  71. package/packages/dd-trace/src/plugins/util/stacktrace.js +94 -0
  72. package/packages/dd-trace/src/plugins/util/tags.js +7 -0
  73. package/packages/dd-trace/src/plugins/util/test.js +20 -22
  74. package/packages/dd-trace/src/plugins/util/web.js +6 -4
  75. package/packages/dd-trace/src/profiling/profiler.js +24 -14
  76. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  77. package/packages/dd-trace/src/profiling/profilers/wall.js +94 -66
  78. package/packages/dd-trace/src/proxy.js +12 -0
  79. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  80. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  81. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  82. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  83. package/packages/datadog-core/src/storage/async_resource.js +0 -108
  84. 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 { ackEmitting } = require('./status')
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
- const probes = params.hitBreakpoints.map((id) => breakpoints.get(id))
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: Should this be recored as telemetry?
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
- // TODO: Send multiple probes in one HTTP request as an array
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
- // TODO: Process template
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 { getScript, probes, breakpoints } = require('./state')
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: Can we modify in place?
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 = getScript(file)
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: '/debugger/v1/input',
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 scripts = []
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
- getScript (path) {
29
- return scripts
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
- scripts.push([params.url, params.scriptId])
51
+ scriptIds.push([params.url, params.scriptId])
46
52
  }
47
53
  })