dd-trace 5.80.0 → 5.81.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 (213) hide show
  1. package/LICENSE-3rdparty.csv +79 -88
  2. package/ext/tags.d.ts +1 -0
  3. package/ext/tags.js +1 -0
  4. package/index.d.ts +35 -35
  5. package/loader-hook.mjs +10 -3
  6. package/package.json +22 -40
  7. package/packages/datadog-esbuild/index.js +36 -19
  8. package/packages/datadog-instrumentations/index.js +1 -0
  9. package/packages/datadog-instrumentations/src/anthropic.js +12 -0
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +5 -1
  11. package/packages/datadog-instrumentations/src/cucumber.js +2 -2
  12. package/packages/datadog-instrumentations/src/find-my-way.js +6 -5
  13. package/packages/datadog-instrumentations/src/google-genai.js +120 -0
  14. package/packages/datadog-instrumentations/src/graphql.js +20 -0
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +10 -0
  17. package/packages/datadog-instrumentations/src/helpers/register.js +6 -1
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +27 -0
  19. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +152 -0
  20. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +5 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langchain.js +237 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.js +9 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +11 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +139 -0
  25. package/packages/datadog-instrumentations/src/langchain.js +3 -109
  26. package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
  27. package/packages/datadog-instrumentations/src/mysql2.js +1 -1
  28. package/packages/datadog-instrumentations/src/playwright.js +45 -16
  29. package/packages/datadog-instrumentations/src/router.js +1 -1
  30. package/packages/datadog-instrumentations/src/selenium.js +3 -1
  31. package/packages/datadog-instrumentations/src/ws.js +35 -17
  32. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -1
  33. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +23 -2
  34. package/packages/datadog-plugin-cypress/src/plugin.js +1 -1
  35. package/packages/datadog-plugin-cypress/src/support.js +73 -31
  36. package/packages/datadog-plugin-google-genai/src/index.js +17 -0
  37. package/packages/datadog-plugin-google-genai/src/tracing.js +41 -0
  38. package/packages/datadog-plugin-graphql/src/tools/transforms.js +5 -4
  39. package/packages/datadog-plugin-jest/src/util.js +1 -1
  40. package/packages/datadog-plugin-langchain/src/tracing.js +7 -3
  41. package/packages/datadog-plugin-next/src/index.js +11 -3
  42. package/packages/dd-trace/src/aiguard/sdk.js +18 -10
  43. package/packages/dd-trace/src/appsec/api_security_sampler.js +1 -1
  44. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +1 -1
  45. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -1
  46. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -2
  47. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
  48. package/packages/dd-trace/src/appsec/reporter.js +0 -4
  49. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +4 -8
  50. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +4 -2
  51. package/packages/dd-trace/src/config.js +81 -7
  52. package/packages/dd-trace/src/config_defaults.js +14 -2
  53. package/packages/dd-trace/src/datastreams/encoding.js +23 -6
  54. package/packages/dd-trace/src/datastreams/pathway.js +40 -1
  55. package/packages/dd-trace/src/datastreams/processor.js +1 -1
  56. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -1
  57. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +15 -5
  58. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  59. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  60. package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -15
  61. package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +2 -0
  62. package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +24 -18
  63. package/packages/dd-trace/src/debugger/devtools_client/send.js +18 -8
  64. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +103 -15
  65. package/packages/dd-trace/src/debugger/devtools_client/snapshot/constants.js +25 -0
  66. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +56 -25
  67. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +64 -23
  68. package/packages/dd-trace/src/debugger/devtools_client/snapshot/symbols.js +3 -1
  69. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +404 -0
  70. package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +1 -1
  71. package/packages/dd-trace/src/debugger/devtools_client/state.js +7 -2
  72. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  73. package/packages/dd-trace/src/debugger/index.js +1 -1
  74. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  75. package/packages/dd-trace/src/histogram.js +1 -1
  76. package/packages/dd-trace/src/id.js +60 -0
  77. package/packages/dd-trace/src/lambda/runtime/ritm.js +1 -1
  78. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  79. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +104 -0
  80. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +486 -0
  81. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +2 -2
  82. package/packages/dd-trace/src/llmobs/plugins/{openai.js → openai/index.js} +48 -6
  83. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +114 -0
  84. package/packages/dd-trace/src/llmobs/sdk.js +5 -0
  85. package/packages/dd-trace/src/llmobs/span_processor.js +6 -1
  86. package/packages/dd-trace/src/llmobs/tagger.js +4 -0
  87. package/packages/dd-trace/src/opentelemetry/logs/index.js +2 -2
  88. package/packages/dd-trace/src/opentelemetry/logs/logger.js +3 -2
  89. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +5 -3
  90. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +8 -8
  91. package/packages/dd-trace/src/opentelemetry/metrics/constants.js +34 -0
  92. package/packages/dd-trace/src/opentelemetry/metrics/index.js +81 -0
  93. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +225 -0
  94. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +171 -0
  95. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +54 -0
  96. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +62 -0
  97. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +251 -0
  98. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +532 -0
  99. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +10 -18
  100. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +36 -22
  101. package/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js +1 -1
  102. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  103. package/packages/dd-trace/src/opentelemetry/tracer.js +1 -1
  104. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  105. package/packages/dd-trace/src/payload-tagging/index.js +2 -2
  106. package/packages/dd-trace/src/plugin_manager.js +4 -2
  107. package/packages/dd-trace/src/plugins/index.js +1 -0
  108. package/packages/dd-trace/src/plugins/util/test.js +3 -3
  109. package/packages/dd-trace/src/plugins/util/url.js +119 -1
  110. package/packages/dd-trace/src/plugins/util/web.js +10 -41
  111. package/packages/dd-trace/src/process-tags/index.js +81 -0
  112. package/packages/dd-trace/src/profiling/config.js +1 -1
  113. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  114. package/packages/dd-trace/src/profiling/profilers/events.js +10 -1
  115. package/packages/dd-trace/src/proxy.js +5 -0
  116. package/packages/dd-trace/src/rate_limiter.js +1 -1
  117. package/packages/dd-trace/src/remote_config/manager.js +1 -1
  118. package/packages/dd-trace/src/ritm.js +1 -1
  119. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  120. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  121. package/packages/dd-trace/src/span_format.js +9 -4
  122. package/packages/dd-trace/src/span_processor.js +8 -3
  123. package/packages/dd-trace/src/span_stats.js +15 -4
  124. package/packages/dd-trace/src/spanleak.js +1 -1
  125. package/packages/dd-trace/src/supported-configurations.json +13 -0
  126. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  127. package/packages/dd-trace/src/telemetry/telemetry.js +11 -2
  128. package/vendor/dist/@datadog/sketches-js/LICENSE +39 -0
  129. package/vendor/dist/@datadog/sketches-js/index.js +1 -0
  130. package/vendor/dist/@datadog/source-map/LICENSE +28 -0
  131. package/vendor/dist/@datadog/source-map/index.js +1 -0
  132. package/vendor/dist/@isaacs/ttlcache/LICENSE +55 -0
  133. package/vendor/dist/@isaacs/ttlcache/index.js +1 -0
  134. package/vendor/dist/@opentelemetry/core/LICENSE +201 -0
  135. package/vendor/dist/@opentelemetry/core/index.js +1 -0
  136. package/vendor/dist/@opentelemetry/resources/LICENSE +201 -0
  137. package/vendor/dist/@opentelemetry/resources/index.js +1 -0
  138. package/vendor/dist/astring/LICENSE +19 -0
  139. package/vendor/dist/astring/index.js +1 -0
  140. package/vendor/dist/crypto-randomuuid/index.js +1 -0
  141. package/vendor/dist/escape-string-regexp/LICENSE +9 -0
  142. package/vendor/dist/escape-string-regexp/index.js +1 -0
  143. package/vendor/dist/esquery/LICENSE +24 -0
  144. package/vendor/dist/esquery/index.js +1 -0
  145. package/vendor/dist/ignore/LICENSE +21 -0
  146. package/vendor/dist/ignore/index.js +1 -0
  147. package/vendor/dist/istanbul-lib-coverage/LICENSE +24 -0
  148. package/vendor/dist/istanbul-lib-coverage/index.js +1 -0
  149. package/vendor/dist/jest-docblock/LICENSE +21 -0
  150. package/vendor/dist/jest-docblock/index.js +1 -0
  151. package/vendor/dist/jsonpath-plus/LICENSE +22 -0
  152. package/vendor/dist/jsonpath-plus/index.js +1 -0
  153. package/vendor/dist/limiter/LICENSE +19 -0
  154. package/vendor/dist/limiter/index.js +1 -0
  155. package/vendor/dist/lodash.sortby/LICENSE +47 -0
  156. package/vendor/dist/lodash.sortby/index.js +1 -0
  157. package/vendor/dist/lru-cache/LICENSE +15 -0
  158. package/vendor/dist/lru-cache/index.js +1 -0
  159. package/vendor/dist/meriyah/LICENSE +7 -0
  160. package/vendor/dist/meriyah/index.js +1 -0
  161. package/vendor/dist/module-details-from-path/LICENSE +21 -0
  162. package/vendor/dist/module-details-from-path/index.js +1 -0
  163. package/vendor/dist/mutexify/promise/LICENSE +21 -0
  164. package/vendor/dist/mutexify/promise/index.js +1 -0
  165. package/vendor/dist/opentracing/LICENSE +201 -0
  166. package/vendor/dist/opentracing/binary_carrier.d.ts +11 -0
  167. package/vendor/dist/opentracing/constants.d.ts +61 -0
  168. package/vendor/dist/opentracing/examples/demo/demo.d.ts +2 -0
  169. package/vendor/dist/opentracing/ext/tags.d.ts +90 -0
  170. package/vendor/dist/opentracing/functions.d.ts +20 -0
  171. package/vendor/dist/opentracing/global_tracer.d.ts +14 -0
  172. package/vendor/dist/opentracing/index.d.ts +12 -0
  173. package/vendor/dist/opentracing/index.js +1 -0
  174. package/vendor/dist/opentracing/mock_tracer/index.d.ts +5 -0
  175. package/vendor/dist/opentracing/mock_tracer/mock_context.d.ts +13 -0
  176. package/vendor/dist/opentracing/mock_tracer/mock_report.d.ts +16 -0
  177. package/vendor/dist/opentracing/mock_tracer/mock_span.d.ts +50 -0
  178. package/vendor/dist/opentracing/mock_tracer/mock_tracer.d.ts +26 -0
  179. package/vendor/dist/opentracing/noop.d.ts +8 -0
  180. package/vendor/dist/opentracing/reference.d.ts +33 -0
  181. package/vendor/dist/opentracing/span.d.ts +147 -0
  182. package/vendor/dist/opentracing/span_context.d.ts +26 -0
  183. package/vendor/dist/opentracing/test/api_compatibility.d.ts +16 -0
  184. package/vendor/dist/opentracing/test/mocktracer_implemenation.d.ts +3 -0
  185. package/vendor/dist/opentracing/test/noop_implementation.d.ts +4 -0
  186. package/vendor/dist/opentracing/test/opentracing_api.d.ts +3 -0
  187. package/vendor/dist/opentracing/test/unittest.d.ts +2 -0
  188. package/vendor/dist/opentracing/tracer.d.ts +127 -0
  189. package/vendor/dist/path-to-regexp/LICENSE +21 -0
  190. package/vendor/dist/path-to-regexp/index.js +1 -0
  191. package/vendor/dist/pprof-format/LICENSE +8 -0
  192. package/vendor/dist/pprof-format/index.js +1 -0
  193. package/vendor/dist/protobufjs/LICENSE +39 -0
  194. package/vendor/dist/protobufjs/index.js +1 -0
  195. package/vendor/dist/protobufjs/minimal/LICENSE +39 -0
  196. package/vendor/dist/protobufjs/minimal/index.js +1 -0
  197. package/vendor/dist/retry/LICENSE +21 -0
  198. package/vendor/dist/retry/index.js +1 -0
  199. package/vendor/dist/rfdc/LICENSE +15 -0
  200. package/vendor/dist/rfdc/index.js +1 -0
  201. package/vendor/dist/semifies/LICENSE +201 -0
  202. package/vendor/dist/semifies/index.js +1 -0
  203. package/vendor/dist/shell-quote/LICENSE +24 -0
  204. package/vendor/dist/shell-quote/index.js +1 -0
  205. package/vendor/dist/source-map/LICENSE +28 -0
  206. package/vendor/dist/source-map/index.js +1 -0
  207. package/vendor/dist/source-map/lib/util/LICENSE +28 -0
  208. package/vendor/dist/source-map/lib/util/index.js +1 -0
  209. package/vendor/dist/source-map/mappings.wasm +0 -0
  210. package/vendor/dist/tlhunter-sorted-set/LICENSE +21 -0
  211. package/vendor/dist/tlhunter-sorted-set/index.js +1 -0
  212. package/vendor/dist/ttl-set/LICENSE +21 -0
  213. package/vendor/dist/ttl-set/index.js +1 -0
@@ -1,16 +1,42 @@
1
1
  'use strict'
2
2
 
3
- const { collectionSizeSym, fieldCountSym } = require('./symbols')
3
+ const { collectionSizeSym, largeCollectionSkipThresholdSym, fieldCountSym, timeBudgetSym } = require('./symbols')
4
+ const { LARGE_OBJECT_SKIP_THRESHOLD } = require('./constants')
4
5
  const session = require('../session')
5
6
 
6
7
  const LEAF_SUBTYPES = new Set(['date', 'regexp'])
7
8
  const ITERABLE_SUBTYPES = new Set(['map', 'set', 'weakmap', 'weakset'])
9
+ const SIZE_IN_DESCRIPTION_SUBTYPES = new Set(['array', 'typedarray', 'arraybuffer', 'dataview', 'map', 'set'])
8
10
 
9
11
  module.exports = {
10
- getRuntimeObject: getObject
12
+ collectObjectProperties
11
13
  }
12
14
 
13
- async function getObject (objectId, opts, depth = 0, collection = false) {
15
+ /**
16
+ * @typedef {Object} GetObjectOptions
17
+ * @property {Object} maxReferenceDepth - The maximum depth of the object to traverse
18
+ * @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot
19
+ * @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot
20
+ * @property {bigint} deadlineNs - The deadline in nanoseconds compared to `process.hrtime.bigint()`
21
+ * @property {Object} ctx - A context object to track the state/progress of the snapshot collection.
22
+ * @property {boolean} ctx.deadlineReached - Will be set to `true` if the deadline has been reached.
23
+ * @property {Error[]} ctx.captureErrors - An array on which errors can be pushed if an issue is detected while
24
+ * collecting the snapshot.
25
+ */
26
+
27
+ /**
28
+ * Collect the properties of an object using the Chrome DevTools Protocol.
29
+ *
30
+ * @param {string} objectId - The ID of the object to get the properties of
31
+ * @param {GetObjectOptions} opts - The options for the snapshot. Also used to track the deadline and communicate the
32
+ * deadline overrun to the caller using the `deadlineReached` flag.
33
+ * @param {number} [depth=0] - The depth of the object. Only used internally by this module to track the current depth
34
+ * and should not be set by the caller.
35
+ * @param {boolean} [collection=false] - Whether the object is a collection. Only used internally by this module to
36
+ * track the current object type and should not be set by the caller.
37
+ * @returns {Promise<Object[]>} The properties of the object
38
+ */
39
+ async function collectObjectProperties (objectId, opts, depth = 0, collection = false) {
14
40
  const { result, privateProperties } = await session.post('Runtime.getProperties', {
15
41
  objectId,
16
42
  ownProperties: true // exclude inherited properties
@@ -28,6 +54,13 @@ async function getObject (objectId, opts, depth = 0, collection = false) {
28
54
  } else if (result.length > opts.maxFieldCount) {
29
55
  // Trim the number of properties on the object if there's too many.
30
56
  const size = result.length
57
+ if (size > LARGE_OBJECT_SKIP_THRESHOLD) {
58
+ opts.ctx.captureErrors.push(new Error(
59
+ `An object with ${size} properties was detected while collecting a snapshot. ` +
60
+ `This exceeds the maximum number of allowed properties of ${LARGE_OBJECT_SKIP_THRESHOLD}. ` +
61
+ 'Future snapshots for existing probes in this location will be skipped until the Node.js process is restarted'
62
+ ))
63
+ }
31
64
  result.length = opts.maxFieldCount
32
65
  result[fieldCountSym] = size
33
66
  } else if (privateProperties) {
@@ -43,32 +76,61 @@ async function traverseGetPropertiesResult (props, opts, depth) {
43
76
 
44
77
  if (depth >= opts.maxReferenceDepth) return props
45
78
 
46
- const promises = []
79
+ const work = []
47
80
 
48
81
  for (const prop of props) {
49
82
  if (prop.value === undefined) continue
50
- const { value: { type, objectId, subtype } } = prop
83
+ const { value: { type, objectId, subtype, description } } = prop
51
84
  if (type === 'object') {
52
85
  if (objectId === undefined) continue // if `subtype` is "null"
53
86
  if (LEAF_SUBTYPES.has(subtype)) continue // don't waste time with these subtypes
54
- promises.push(getObjectProperties(subtype, objectId, opts, depth).then((properties) => {
55
- prop.value.properties = properties
56
- }))
87
+ const size = parseLengthFromDescription(description, subtype)
88
+ if (size !== null && size >= LARGE_OBJECT_SKIP_THRESHOLD) {
89
+ const empty = []
90
+ empty[largeCollectionSkipThresholdSym] = size
91
+ prop.value.properties = empty
92
+ continue
93
+ }
94
+ work.push([
95
+ prop.value,
96
+ () => collectPropertiesBySubtype(subtype, objectId, opts, depth).then((properties) => {
97
+ prop.value.properties = properties
98
+ })
99
+ ])
57
100
  } else if (type === 'function') {
58
- promises.push(getFunctionProperties(objectId, opts, depth + 1).then((properties) => {
59
- prop.value.properties = properties
60
- }))
101
+ work.push([
102
+ prop.value,
103
+ () => getFunctionProperties(objectId, opts, depth + 1).then((properties) => {
104
+ prop.value.properties = properties
105
+ })
106
+ ])
61
107
  }
62
108
  }
63
109
 
64
- if (promises.length) {
65
- await Promise.all(promises)
110
+ if (work.length) {
111
+ // Iterate over the work in chunks of 2. The closer to 1, the less we'll overshoot the deadline, but the longer it
112
+ // takes to complete. `2` seems to be the best compromise.
113
+ // Anecdotally, on my machine, with no deadline, a concurrency of `1` takes twice as long as a concurrency of `2`.
114
+ // From thereon, there's no real measurable savings with a higher concurrency.
115
+ for (let i = 0; i < work.length; i += 2) {
116
+ if (overBudget(opts)) {
117
+ for (let j = i; j < work.length; j++) {
118
+ work[j][0][timeBudgetSym] = true
119
+ }
120
+ break
121
+ }
122
+ // eslint-disable-next-line no-await-in-loop
123
+ await Promise.all([
124
+ work[i][1](),
125
+ work[i + 1]?.[1]()
126
+ ])
127
+ }
66
128
  }
67
129
 
68
130
  return props
69
131
  }
70
132
 
71
- function getObjectProperties (subtype, objectId, opts, depth) {
133
+ function collectPropertiesBySubtype (subtype, objectId, opts, depth) {
72
134
  if (ITERABLE_SUBTYPES.has(subtype)) {
73
135
  return getIterable(objectId, opts, depth)
74
136
  } else if (subtype === 'promise') {
@@ -78,7 +140,7 @@ function getObjectProperties (subtype, objectId, opts, depth) {
78
140
  } else if (subtype === 'arraybuffer') {
79
141
  return getArrayBuffer(objectId, opts, depth)
80
142
  }
81
- return getObject(objectId, opts, depth + 1, subtype === 'array' || subtype === 'typedarray')
143
+ return collectObjectProperties(objectId, opts, depth + 1, subtype === 'array' || subtype === 'typedarray')
82
144
  }
83
145
 
84
146
  // TODO: The following extra information from `internalProperties` might be relevant to include for functions:
@@ -189,3 +251,29 @@ function removeNonEnumerableProperties (props) {
189
251
  }
190
252
  }
191
253
  }
254
+
255
+ function parseLengthFromDescription (description, subtype) {
256
+ if (typeof description !== 'string') return null
257
+ if (!SIZE_IN_DESCRIPTION_SUBTYPES.has(subtype)) return null
258
+
259
+ const open = description.lastIndexOf('(')
260
+ if (open === -1) return null
261
+
262
+ const close = description.indexOf(')', open + 1)
263
+ if (close === -1) return null
264
+
265
+ const s = description.slice(open + 1, close)
266
+ if (s === '') return null
267
+
268
+ const n = Number(s)
269
+ if (!Number.isSafeInteger(n) || n < 0) return null
270
+ if (String(n) !== s) return null
271
+
272
+ return n
273
+ }
274
+
275
+ function overBudget (opts) {
276
+ if (opts.ctx.deadlineReached) return true
277
+ opts.ctx.deadlineReached = process.hrtime.bigint() >= opts.deadlineNs
278
+ return opts.ctx.deadlineReached
279
+ }
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const { getEnvironmentVariable } = require('../../../config-helper')
4
+
5
+ const largeObjectSkipThreshold = Number(
6
+ getEnvironmentVariable('_DD_DYNAMIC_INSTRUMENTATION_EXPERIMENTAL_LARGE_OBJECT_SKIP_THRESHOLD')
7
+ )
8
+
9
+ module.exports = {
10
+ /**
11
+ * When collecting a snapshot, this constant controls what happens when objects with a large number of properties or
12
+ * collections (arrays, maps, sets, etc.) with a large number of elements are detected:
13
+ *
14
+ * - If a collection is detected with more than this number of elements, none of its elements will be included in the
15
+ * snapshot.
16
+ * - If an object is detected with more than this number of properties, it will be included in the snapshot, but
17
+ * snapshotting will be turned off for that probe in the future, until the probe is either updated or the Node.js
18
+ * process is restarted.
19
+ */
20
+ LARGE_OBJECT_SKIP_THRESHOLD: Number.isNaN(largeObjectSkipThreshold) ? 500 : largeObjectSkipThreshold,
21
+ DEFAULT_MAX_COLLECTION_SIZE: 100,
22
+ DEFAULT_MAX_FIELD_COUNT: 20,
23
+ DEFAULT_MAX_LENGTH: 255,
24
+ DEFAULT_MAX_REFERENCE_DEPTH: 3,
25
+ }
@@ -1,52 +1,83 @@
1
1
  'use strict'
2
2
 
3
- const { getRuntimeObject } = require('./collector')
3
+ const {
4
+ DEFAULT_MAX_REFERENCE_DEPTH,
5
+ DEFAULT_MAX_COLLECTION_SIZE,
6
+ DEFAULT_MAX_FIELD_COUNT,
7
+ DEFAULT_MAX_LENGTH
8
+ } = require('./constants')
9
+ const { collectObjectProperties } = require('./collector')
4
10
  const { processRawState } = require('./processor')
5
- const log = require('../log')
6
11
 
7
- const DEFAULT_MAX_REFERENCE_DEPTH = 3
8
- const DEFAULT_MAX_COLLECTION_SIZE = 100
9
- const DEFAULT_MAX_FIELD_COUNT = 20
10
- const DEFAULT_MAX_LENGTH = 255
12
+ const BIGINT_MAX = (1n << 256n) - 1n
11
13
 
12
14
  module.exports = {
13
15
  getLocalStateForCallFrame
14
16
  }
15
17
 
16
- function returnError () {
17
- return new Error('Error getting local state')
18
- }
18
+ /**
19
+ * @typedef {Object} GetLocalStateForCallFrameOptions
20
+ * @property {number} [maxReferenceDepth] - The maximum depth of the object to traverse. Defaults to
21
+ * {@link DEFAULT_MAX_REFERENCE_DEPTH}.
22
+ * @property {number} [maxCollectionSize] - The maximum size of a collection to include in the snapshot. Defaults to
23
+ * {@link DEFAULT_MAX_COLLECTION_SIZE}.
24
+ * @property {number} [maxFieldCount] - The maximum number of properties on an object to include in the snapshot.
25
+ * Defaults to {@link DEFAULT_MAX_FIELD_COUNT}.
26
+ * @property {number} [maxLength] - The maximum length of a string to include in the snapshot. Defaults to
27
+ * {@link DEFAULT_MAX_LENGTH}.
28
+ * @property {bigint} [deadlineNs] - The deadline in nanoseconds compared to `process.hrtime.bigint()`. Defaults to
29
+ * {@link BIGINT_MAX}. If the deadline is reached, the snapshot will be truncated.
30
+ */
19
31
 
32
+ /**
33
+ * Get the local state for a call frame.
34
+ *
35
+ * @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to get the local state for
36
+ * @param {GetLocalStateForCallFrameOptions} [opts] - The options for the snapshot
37
+ * @returns {Promise<Object>} The local state for the call frame
38
+ */
20
39
  async function getLocalStateForCallFrame (
21
40
  callFrame,
22
41
  {
23
42
  maxReferenceDepth = DEFAULT_MAX_REFERENCE_DEPTH,
24
43
  maxCollectionSize = DEFAULT_MAX_COLLECTION_SIZE,
25
44
  maxFieldCount = DEFAULT_MAX_FIELD_COUNT,
26
- maxLength = DEFAULT_MAX_LENGTH
45
+ maxLength = DEFAULT_MAX_LENGTH,
46
+ deadlineNs = BIGINT_MAX
27
47
  } = {}
28
48
  ) {
49
+ /** @type {{ deadlineReached: boolean, captureErrors: Error[] }} */
50
+ const ctx = { deadlineReached: false, captureErrors: [] }
51
+ const opts = { maxReferenceDepth, maxCollectionSize, maxFieldCount, deadlineNs, ctx }
29
52
  const rawState = []
30
53
  let processedState = null
31
54
 
32
- try {
33
- await Promise.all(callFrame.scopeChain.map(async (scope) => {
34
- if (scope.type === 'global') return // The global scope is too noisy
35
- rawState.push(...await getRuntimeObject(
36
- scope.object.objectId,
37
- { maxReferenceDepth, maxCollectionSize, maxFieldCount }
55
+ for (const scope of callFrame.scopeChain) {
56
+ if (scope.type === 'global') continue // The global scope is too noisy
57
+ const { objectId } = scope.object
58
+ if (objectId === undefined) continue // I haven't seen this happen, but according to the types it's possible
59
+ try {
60
+ // The objectId for a scope points to a pseudo-object whose properties are the actual variables in the scope.
61
+ // This is why we can just call `collectObjectProperties` directly and expect it to return the in-scope variables
62
+ // as an array.
63
+ // eslint-disable-next-line no-await-in-loop
64
+ rawState.push(...await collectObjectProperties(objectId, opts))
65
+ } catch (err) {
66
+ ctx.captureErrors.push(new Error(
67
+ `Error getting local state for closure scope (type: ${scope.type}). ` +
68
+ 'Future snapshots for existing probes in this location will be skipped until the Node.js process is restarted',
69
+ { cause: err } // TODO: The cause is not used by the backend
38
70
  ))
39
- }))
40
- } catch (err) {
41
- // TODO: We might be able to get part of the scope chain.
42
- // Consider if we could set errors just for the part of the scope chain that throws during collection.
43
- log.error('[debugger:devtools_client] Error getting local state for call frame', err)
44
- return returnError
71
+ }
72
+ if (ctx.deadlineReached === true) break // TODO: Bad UX; Variables in remaining scopes are silently dropped
45
73
  }
46
74
 
47
75
  // Delay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
48
- return () => {
49
- processedState = processedState ?? processRawState(rawState, maxLength)
50
- return processedState
76
+ return {
77
+ processLocalState () {
78
+ processedState = processedState ?? processRawState(rawState, maxLength)
79
+ return processedState
80
+ },
81
+ captureErrors: ctx.captureErrors
51
82
  }
52
83
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { collectionSizeSym, fieldCountSym } = require('./symbols')
3
+ const { LARGE_OBJECT_SKIP_THRESHOLD } = require('./constants')
4
+ const { collectionSizeSym, largeCollectionSkipThresholdSym, fieldCountSym, timeBudgetSym } = require('./symbols')
4
5
  const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
5
6
 
6
7
  module.exports = {
@@ -38,7 +39,7 @@ function getPropertyValue (prop, maxLength) {
38
39
 
39
40
  function getPropertyValueRaw (prop, maxLength) {
40
41
  // Special case for getters and setters which does not have a value property
41
- if (Object.hasOwn(prop, 'get')) {
42
+ if (prop.get) {
42
43
  const hasGet = prop.get.type !== 'undefined'
43
44
  const hasSet = prop.set.type !== 'undefined'
44
45
  if (hasGet) {
@@ -74,11 +75,13 @@ function getPropertyValueRaw (prop, maxLength) {
74
75
  }
75
76
 
76
77
  function getObjectValue (obj, maxLength) {
78
+ const timeBudgetReached = obj[timeBudgetSym] === true
79
+
77
80
  switch (obj.subtype) {
78
81
  case undefined:
79
- return toObject(obj.className, obj.properties, maxLength)
82
+ return toObject(obj.className, obj.properties, maxLength, timeBudgetReached)
80
83
  case 'array':
81
- return toArray(obj.className, obj.properties, maxLength)
84
+ return toArray(obj.className, obj.properties, maxLength, timeBudgetReached)
82
85
  case 'null':
83
86
  return { type: 'null', isNull: true }
84
87
  // case 'node': // TODO: What does this subtype represent?
@@ -89,28 +92,28 @@ function getObjectValue (obj, maxLength) {
89
92
  // in the `description` field. Unfortunately that's all we get from the Chrome DevTools Protocol.
90
93
  return { type: obj.className, value: `${new Date(obj.description).toISOString().slice(0, -5)}Z` }
91
94
  case 'map':
92
- return toMap(obj.className, obj.properties, maxLength)
95
+ return toMap(obj.className, obj.properties, maxLength, timeBudgetReached)
93
96
  case 'set':
94
- return toSet(obj.className, obj.properties, maxLength)
97
+ return toSet(obj.className, obj.properties, maxLength, timeBudgetReached)
95
98
  case 'error':
96
99
  // TODO: Convert stack trace to array to avoid string truncation or disable truncation in this case?
97
- return toObject(obj.className, obj.properties, maxLength)
100
+ return toObject(obj.className, obj.properties, maxLength, timeBudgetReached)
98
101
  case 'proxy':
99
102
  // Use `description` instead of `className` as the `type` to get type of target object (`Proxy(Error)` vs `proxy`)
100
- return toObject(obj.description, obj.properties, maxLength)
103
+ return toObject(obj.description, obj.properties, maxLength, timeBudgetReached)
101
104
  case 'promise':
102
- return toObject(obj.className, obj.properties, maxLength)
105
+ return toObject(obj.className, obj.properties, maxLength, timeBudgetReached)
103
106
  case 'typedarray':
104
- return toArray(obj.className, obj.properties, maxLength)
107
+ return toArray(obj.className, obj.properties, maxLength, timeBudgetReached)
105
108
  case 'generator':
106
109
  // Use `subtype` instead of `className` to make it obvious it's a generator
107
- return toObject(obj.subtype, obj.properties, maxLength)
110
+ return toObject(obj.subtype, obj.properties, maxLength, timeBudgetReached)
108
111
  case 'arraybuffer':
109
- return toArrayBuffer(obj.className, obj.properties, maxLength)
112
+ return toArrayBuffer(obj.className, obj.properties, maxLength, timeBudgetReached)
110
113
  case 'weakmap':
111
- return toMap(obj.className, obj.properties, maxLength)
114
+ return toMap(obj.className, obj.properties, maxLength, timeBudgetReached)
112
115
  case 'weakset':
113
- return toSet(obj.className, obj.properties, maxLength)
116
+ return toSet(obj.className, obj.properties, maxLength, timeBudgetReached)
114
117
  // case 'iterator': // TODO: I've not been able to trigger this subtype
115
118
  // case 'dataview': // TODO: Looks like the internal ArrayBuffer is only accessible via the `buffer` getter
116
119
  // case 'webassemblymemory': // TODO: Looks like the internal ArrayBuffer is only accessible via the `buffer` getter
@@ -127,8 +130,9 @@ function toFunctionOrClass (value, maxLength) {
127
130
 
128
131
  if (classMatch === null) {
129
132
  // This is a function
133
+ const timeBudgetReached = value[timeBudgetSym] === true
130
134
  // TODO: Would it make sense to detect if it's an arrow function or not?
131
- return toObject(value.className, value.properties, maxLength)
135
+ return toObject(value.className, value.properties, maxLength, timeBudgetReached)
132
136
  }
133
137
  // This is a class
134
138
  const className = classMatch[1].trim()
@@ -150,7 +154,8 @@ function toString (str, maxLength) {
150
154
  }
151
155
  }
152
156
 
153
- function toObject (type, props, maxLength) {
157
+ function toObject (type, props, maxLength, timeBudgetReached) {
158
+ if (timeBudgetReached === true) return notCapturedTimeBudget(type)
154
159
  if (props === undefined) return notCapturedDepth(type)
155
160
 
156
161
  const result = {
@@ -158,7 +163,7 @@ function toObject (type, props, maxLength) {
158
163
  fields: processProperties(props, maxLength)
159
164
  }
160
165
 
161
- if (Object.hasOwn(props, fieldCountSym)) {
166
+ if (props[fieldCountSym] !== undefined) {
162
167
  result.notCapturedReason = 'fieldCount'
163
168
  result.size = props[fieldCountSym]
164
169
  }
@@ -166,7 +171,8 @@ function toObject (type, props, maxLength) {
166
171
  return result
167
172
  }
168
173
 
169
- function toArray (type, elements, maxLength) {
174
+ function toArray (type, elements, maxLength, timeBudgetReached) {
175
+ if (timeBudgetReached === true) return notCapturedTimeBudget(type)
170
176
  if (elements === undefined) return notCapturedDepth(type)
171
177
 
172
178
  const result = {
@@ -177,12 +183,15 @@ function toArray (type, elements, maxLength) {
177
183
  }
178
184
 
179
185
  setNotCaptureReasonOnCollection(result, elements)
186
+ setNotCaptureReasonOnTooLargeCollection(result, elements)
180
187
 
181
188
  return result
182
189
  }
183
190
 
184
- function toMap (type, pairs, maxLength) {
191
+ function toMap (type, pairs, maxLength, timeBudgetReached) {
192
+ if (timeBudgetReached === true) return notCapturedTimeBudget(type)
185
193
  if (pairs === undefined) return notCapturedDepth(type)
194
+ if (pairs.length > 0 && pairs.every(({ value }) => value[timeBudgetSym] === true)) return notCapturedTimeBudget(type)
186
195
 
187
196
  const result = {
188
197
  type,
@@ -195,6 +204,14 @@ function toMap (type, pairs, maxLength) {
195
204
  // This can be skipped and we can go directly to its children, of which
196
205
  // there will always be exactly two, the first containing the key, and the
197
206
  // second containing the value of this entry of the Map.
207
+
208
+ if (value[timeBudgetSym] === true) {
209
+ return [{ notCapturedReason: 'timeout' }, { notCapturedReason: 'timeout' }]
210
+ }
211
+ if (value.properties === undefined) {
212
+ return [{ notCapturedReason: 'unknown' }, { notCapturedReason: 'unknown' }]
213
+ }
214
+
198
215
  const shouldRedact = shouldRedactMapValue(value.properties[0])
199
216
  const key = getPropertyValue(value.properties[0], maxLength)
200
217
  const val = shouldRedact
@@ -205,12 +222,17 @@ function toMap (type, pairs, maxLength) {
205
222
  }
206
223
 
207
224
  setNotCaptureReasonOnCollection(result, pairs)
225
+ setNotCaptureReasonOnTooLargeCollection(result, pairs)
208
226
 
209
227
  return result
210
228
  }
211
229
 
212
- function toSet (type, values, maxLength) {
230
+ function toSet (type, values, maxLength, timeBudgetReached) {
231
+ if (timeBudgetReached === true) return notCapturedTimeBudget(type)
213
232
  if (values === undefined) return notCapturedDepth(type)
233
+ if (values.length > 0 && values.every(({ value }) => value[timeBudgetSym] === true)) {
234
+ return notCapturedTimeBudget(type)
235
+ }
214
236
 
215
237
  const result = {
216
238
  type,
@@ -223,16 +245,22 @@ function toSet (type, values, maxLength) {
223
245
  // `internal#entry`. This can be skipped and we can go directly to its
224
246
  // children, of which there will always be exactly one, which contain the
225
247
  // actual value in this entry of the Set.
248
+
249
+ if (value[timeBudgetSym] === true) return { notCapturedReason: 'timeout' }
250
+ if (value.properties === undefined) return { notCapturedReason: 'unknown' }
251
+
226
252
  return getPropertyValue(value.properties[0], maxLength)
227
253
  })
228
254
  }
229
255
 
230
256
  setNotCaptureReasonOnCollection(result, values)
257
+ setNotCaptureReasonOnTooLargeCollection(result, values)
231
258
 
232
259
  return result
233
260
  }
234
261
 
235
- function toArrayBuffer (type, bytes, maxLength) {
262
+ function toArrayBuffer (type, bytes, maxLength, timeBudgetReached) {
263
+ if (timeBudgetReached === true) return notCapturedTimeBudget(type)
236
264
  if (bytes === undefined) return notCapturedDepth(type)
237
265
 
238
266
  const size = bytes.length
@@ -271,16 +299,25 @@ function shouldRedactMapValue (key) {
271
299
  }
272
300
 
273
301
  function getNormalizedNameFromProp (prop) {
274
- return normalizeName(prop.name, Object.hasOwn(prop, 'symbol'))
302
+ return normalizeName(prop.name, prop.symbol !== undefined)
275
303
  }
276
304
 
277
305
  function setNotCaptureReasonOnCollection (result, collection) {
278
- if (Object.hasOwn(collection, collectionSizeSym)) {
306
+ if (collection[collectionSizeSym] !== undefined) {
279
307
  result.notCapturedReason = 'collectionSize'
280
308
  result.size = collection[collectionSizeSym]
281
309
  }
282
310
  }
283
311
 
312
+ function setNotCaptureReasonOnTooLargeCollection (result, collection) {
313
+ if (collection[largeCollectionSkipThresholdSym] !== undefined) {
314
+ result.notCapturedReason = `Large collection with too many elements (skip threshold: ${
315
+ LARGE_OBJECT_SKIP_THRESHOLD
316
+ })`
317
+ result.size = collection[largeCollectionSkipThresholdSym]
318
+ }
319
+ }
320
+
284
321
  function notCapturedDepth (type) {
285
322
  return { type, notCapturedReason: 'depth' }
286
323
  }
@@ -288,3 +325,7 @@ function notCapturedDepth (type) {
288
325
  function notCapturedRedacted (type) {
289
326
  return { type, notCapturedReason: 'redactedIdent' }
290
327
  }
328
+
329
+ function notCapturedTimeBudget (type) {
330
+ return { type, notCapturedReason: 'timeout' }
331
+ }
@@ -2,5 +2,7 @@
2
2
 
3
3
  module.exports = {
4
4
  collectionSizeSym: Symbol('datadog.collectionSize'),
5
- fieldCountSym: Symbol('datadog.fieldCount')
5
+ largeCollectionSkipThresholdSym: Symbol('datadog.largeCollectionSkipThresholdSym'),
6
+ fieldCountSym: Symbol('datadog.fieldCount'),
7
+ timeBudgetSym: Symbol('datadog.timeout')
6
8
  }