dd-trace 6.19.0 → 6.20.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 (77) hide show
  1. package/index.d.ts +163 -22
  2. package/package.json +2 -2
  3. package/packages/datadog-esbuild/index.js +9 -6
  4. package/packages/datadog-instrumentations/src/ai.js +26 -0
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  9. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  10. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  11. package/packages/dd-trace/src/config/remote_config.js +12 -0
  12. package/packages/dd-trace/src/config/supported-configurations.json +21 -0
  13. package/packages/dd-trace/src/debugger/constants.js +13 -4
  14. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  15. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  16. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  17. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  18. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  19. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  20. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  21. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  22. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  23. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  24. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  25. package/packages/dd-trace/src/debugger/index.js +60 -28
  26. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  27. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  28. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  29. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  30. package/packages/dd-trace/src/knuth-hash.js +17 -0
  31. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  32. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  33. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  34. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  35. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  36. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  37. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  38. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  39. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +2 -2
  40. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  41. package/packages/dd-trace/src/llmobs/span_processor.js +42 -26
  42. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  43. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  44. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  45. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  46. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  47. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  48. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  49. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  50. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  51. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  52. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  53. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  54. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  55. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  56. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  57. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  58. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  59. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  60. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  61. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  62. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  63. package/packages/dd-trace/src/opentracing/span.js +14 -9
  64. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  65. package/packages/dd-trace/src/otel-sampling.js +171 -0
  66. package/packages/dd-trace/src/plugins/index.js +0 -1
  67. package/packages/dd-trace/src/priority_sampler.js +155 -33
  68. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  69. package/packages/dd-trace/src/sampler.js +2 -5
  70. package/packages/dd-trace/src/sampling_rule.js +4 -8
  71. package/packages/dd-trace/src/span_format.js +6 -0
  72. package/packages/dd-trace/src/span_processor.js +26 -2
  73. package/packages/dd-trace/src/standalone/index.js +5 -4
  74. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  75. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  76. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  77. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -1,128 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const config = require('../config')
4
-
5
- const excludedIdentifiers = new Set(
6
- config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS
7
- .map((name) => normalizeName(name))
8
- )
9
-
10
- const REDACTED_IDENTIFIERS = new Set(
11
- [
12
- '2fa',
13
- '_csrf',
14
- '_csrf_token',
15
- '_session',
16
- '_xsrf',
17
- 'access_token',
18
- 'aiohttp_session',
19
- 'api_key',
20
- 'apisecret',
21
- 'apisignature',
22
- 'applicationkey',
23
- 'appkey',
24
- 'auth',
25
- 'authtoken',
26
- 'authorization',
27
- 'cc_number',
28
- 'certificatepin',
29
- 'cipher',
30
- 'client_secret',
31
- 'clientid',
32
- 'connect.sid',
33
- 'connectionstring',
34
- 'cookie',
35
- 'credentials',
36
- 'creditcard',
37
- 'csrf',
38
- 'csrf_token',
39
- 'cvv',
40
- 'databaseurl',
41
- 'db_url',
42
- 'encryption_key',
43
- 'encryptionkeyid',
44
- 'geo_location',
45
- 'gpg_key',
46
- 'ip_address',
47
- 'jti',
48
- 'jwt',
49
- 'license_key',
50
- 'masterkey',
51
- 'mysql_pwd',
52
- 'nonce',
53
- 'oauth',
54
- 'oauthtoken',
55
- 'otp',
56
- 'passhash',
57
- 'passwd',
58
- 'password',
59
- 'passwordb',
60
- 'pem_file',
61
- 'pgp_key',
62
- 'PHPSESSID',
63
- 'pin',
64
- 'pincode',
65
- 'pkcs8',
66
- 'private_key',
67
- 'publickey',
68
- 'pwd',
69
- 'recaptcha_key',
70
- 'refresh_token',
71
- 'routingnumber',
72
- 'salt',
73
- 'secret',
74
- 'secretKey',
75
- 'secrettoken',
76
- 'securitycode',
77
- 'security_answer',
78
- 'security_question',
79
- 'serviceaccountcredentials',
80
- 'session',
81
- 'sessionid',
82
- 'sessionkey',
83
- 'set_cookie',
84
- 'signature',
85
- 'signaturekey',
86
- 'ssh_key',
87
- 'ssn',
88
- 'symfony',
89
- 'token',
90
- 'transactionid',
91
- 'twilio_token',
92
- 'user_session',
93
- 'voterid',
94
- 'x-auth-token',
95
- 'x_api_key',
96
- 'x_csrftoken',
97
- 'x_forwarded_for',
98
- 'x_real_ip',
99
- 'XSRF-TOKEN',
100
- ...config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS,
101
- ]
102
- .map(normalizeIdentifier)
103
- .filter(isIncludedIdentifier)
104
- )
105
-
106
- /**
107
- * @param {string} name
108
- */
109
- function normalizeIdentifier (name) {
110
- return normalizeName(name)
111
- }
112
-
113
- /**
114
- * @param {string} name
115
- */
116
- function isIncludedIdentifier (name) {
117
- return !excludedIdentifiers.has(name)
118
- }
119
-
120
- function normalizeName (name, isSymbol) {
121
- if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)`
122
- return name.toLowerCase().replaceAll(/[-_@$.]/g, '')
123
- }
4
+ const { createIsRedactedIdentifier } = require('../../redaction')
124
5
 
125
6
  module.exports = {
126
- REDACTED_IDENTIFIERS,
127
- normalizeName,
7
+ isRedactedIdentifier: createIsRedactedIdentifier(config.dynamicInstrumentation),
128
8
  }
@@ -32,8 +32,6 @@ const SKIPPED_REASON_NAMES = ['rateLimitGlobal', 'rateLimitProbe', 'evaluationTi
32
32
  /** Complete events dropped before transport (`events.dropped`) */
33
33
  const DROPPED_REASON = Object.freeze({
34
34
  QUEUE_FULL: 0,
35
- // The spec allows either trimming oversized payloads or dropping them whole. This tracer trims, and reports
36
- // `capture.incomplete{payloadTooLarge}` instead, so this reason exists for spec parity but is never emitted.
37
35
  PAYLOAD_TOO_LARGE: 1,
38
36
  })
39
37
  const DROPPED_REASON_NAMES = ['queueFull', 'payloadTooLarge']
@@ -13,11 +13,12 @@ const {
13
13
  DEBUGGER_DIAGNOSTICS_V1,
14
14
  DEBUGGER_INPUT_DIRECT,
15
15
  DEBUGGER_INPUT_V2,
16
- GUARDRAIL_METRICS_FLUSH_INTERVAL_MS,
17
16
  INSPECT_SEGMENT_GLOBAL_PROPERTY,
17
+ METRICS_FLUSH_INTERVAL_MS,
18
18
  } = require('./constants')
19
19
  const { GuardrailMetrics, TELEMETRY_NAMESPACE } = require('./guardrail-metrics')
20
- const { installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
20
+ const { PauseDurationHistogram } = require('./pause-duration-histogram')
21
+ const { configureProbeSampler, installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
21
22
 
22
23
  /**
23
24
  * @typedef {ReturnType<import('../config')>} Config
@@ -40,7 +41,9 @@ let rc = null
40
41
  let inputPath = null
41
42
  /** @type {GuardrailMetrics | null} */
42
43
  let guardrailMetrics = null
43
- let guardrailMetricsTimer = null
44
+ /** @type {PauseDurationHistogram | null} */
45
+ let pauseDurations = null
46
+ let metricsFlushTimer = null
44
47
 
45
48
  // eslint-disable-next-line eslint-rules/eslint-process-env
46
49
  const { NODE_OPTIONS, ...env } = process.env
@@ -86,15 +89,20 @@ function start (config, rcInstance) {
86
89
 
87
90
  const debuggerGlobals = globalThis[Symbol.for('dd-trace')]
88
91
  debuggerGlobals.utilTypes = types
89
- debuggerGlobals[INSPECT_SEGMENT_GLOBAL_PROPERTY] = require('./inspect-segment')
92
+ const createInspectSegment = require('./inspect-segment')
93
+ const { createIsRedactedIdentifier } = require('./redaction')
94
+ debuggerGlobals[INSPECT_SEGMENT_GLOBAL_PROPERTY] =
95
+ createInspectSegment(createIsRedactedIdentifier(config.dynamicInstrumentation))
90
96
 
91
97
  const guardrailMetricsBuffer = GuardrailMetrics.createBuffer()
92
98
  guardrailMetrics = new GuardrailMetrics(guardrailMetricsBuffer)
93
- guardrailMetricsTimer = setInterval(flushGuardrailMetrics, GUARDRAIL_METRICS_FLUSH_INTERVAL_MS)
94
- guardrailMetricsTimer.unref?.()
95
- dc.subscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushGuardrailMetrics)
99
+ const pauseDurationBuffer = PauseDurationHistogram.createBuffer()
100
+ pauseDurations = new PauseDurationHistogram(pauseDurationBuffer)
101
+ metricsFlushTimer = setInterval(flushMetrics, METRICS_FLUSH_INTERVAL_MS)
102
+ metricsFlushTimer.unref?.()
103
+ dc.subscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushMetrics)
96
104
 
97
- const probeSamplerBuffer = installProbeSampler(guardrailMetrics)
105
+ const probeSamplerBuffer = installProbeSampler(guardrailMetrics, config)
98
106
 
99
107
  readProbeFile(config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE, (probes) => {
100
108
  const action = 'apply'
@@ -108,7 +116,8 @@ function start (config, rcInstance) {
108
116
  probeChannel.port2.postMessage({ action, probe, ackId })
109
117
  })
110
118
 
111
- probeChannel.port2.on('message', ({ ackId, error }) => {
119
+ probeChannel.port2.on('message', ({ ackId, error, reason }) => {
120
+ if (error && reason !== undefined) logWorkerError(error, reason)
112
121
  const ack = rcAckCallbacks.get(ackId)
113
122
  if (ack === undefined) {
114
123
  // This should never happen, but just in case something changes in the future, we should guard against it
@@ -143,6 +152,7 @@ function start (config, rcInstance) {
143
152
  configPort: configChannel.port1,
144
153
  probeSamplerBuffer,
145
154
  guardrailMetricsBuffer,
155
+ pauseDurationBuffer,
146
156
  },
147
157
  transferList: [probeChannel.port1, logChannel.port1, configChannel.port1],
148
158
  }
@@ -155,17 +165,14 @@ function start (config, rcInstance) {
155
165
  )
156
166
  })
157
167
 
158
- const threadPausedMetric = telemetryMetrics.manager.namespace(TELEMETRY_NAMESPACE)
159
- .distribution('execution.pause.duration')
160
- worker.on('message', (/** @type {{ type: string, durationMs: number }} */ { type, durationMs }) => {
161
- if (type === 'thread-paused') threadPausedMetric.track(durationMs)
162
- })
163
- worker.on('error', (err) => log.error('[debugger] worker thread error', err))
168
+ worker.on('error', (err) => logWorkerError(err))
164
169
  worker.on('messageerror', (err) => log.error('[debugger] received "messageerror" from worker', err))
165
170
 
166
171
  worker.once('exit', (code) => {
167
172
  const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`)
168
- log.error('[debugger] worker thread exited unexpectedly', error)
173
+ // Telemetry omits printf arguments, so the numeric exit code must be part of the message.
174
+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
175
+ log.error(() => `[debugger] worker thread exited unexpectedly exit_code=${code}`, error)
169
176
  cleanup(error) // Be nice, clean up now that the worker thread encountered an issue and we can't continue
170
177
  })
171
178
 
@@ -179,6 +186,24 @@ function start (config, rcInstance) {
179
186
  })
180
187
  }
181
188
 
189
+ /**
190
+ * Failure metadata belongs in the telemetry message; exception messages remain in the redacted cause.
191
+ *
192
+ * @param {Error & { code?: unknown, reason?: unknown }} error - The worker failure
193
+ * @param {unknown} [reason] - Explicit reason preserved across a probe acknowledgement's structured clone
194
+ */
195
+ function logWorkerError (error, reason = error.reason) {
196
+ // Telemetry omits printf arguments, so the failure metadata must be part of the message.
197
+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
198
+ log.error(() => `[debugger] worker thread error name=${
199
+ typeof error.name === 'string' ? error.name : 'unknown'
200
+ } code=${
201
+ typeof error.code === 'string' ? error.code : 'unknown'
202
+ } reason=${
203
+ typeof reason === 'string' ? reason : 'unknown'
204
+ }`, error)
205
+ }
206
+
182
207
  /**
183
208
  * Reconfigure the Debugger worker with updated settings.
184
209
  * Sends the new configuration to the worker thread via the config channel.
@@ -193,6 +218,7 @@ function configure (config) {
193
218
  log.error('[debugger] Invalid DD_SITE for agentless Dynamic Instrumentation: %s', config.site)
194
219
  return
195
220
  }
221
+ configureProbeSampler(config)
196
222
  configChannel.port2.postMessage(debuggerConfig)
197
223
  }
198
224
 
@@ -234,18 +260,19 @@ function cleanup (error) {
234
260
  configChannel = null
235
261
  inputPath = null
236
262
 
237
- if (guardrailMetricsTimer !== null) {
238
- clearInterval(guardrailMetricsTimer)
239
- guardrailMetricsTimer = null
263
+ if (metricsFlushTimer !== null) {
264
+ clearInterval(metricsFlushTimer)
265
+ metricsFlushTimer = null
240
266
  }
241
267
  if (guardrailMetrics !== null) {
242
- dc.unsubscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushGuardrailMetrics)
243
- // Report what the worker counted up until it was stopped. Known limitation: `Worker#terminate()` interrupts the
244
- // worker asynchronously, so anything it counts between this drain and its actual termination is lost. That only
245
- // concerns events still sitting in the worker's upload buffer, which die with the worker anyway, so it isn't worth
246
- // deferring the drain until the worker has exited.
247
- flushGuardrailMetrics()
268
+ dc.unsubscribe(TELEMETRY_APP_CLOSING_CHANNEL, flushMetrics)
269
+ // Report what the worker recorded up until it was stopped. Known limitation: `Worker#terminate()` interrupts the
270
+ // worker asynchronously, so anything it records between this drain and its actual termination is lost. That only
271
+ // concerns events still sitting in the worker's upload buffer, which die with the worker anyway, and a pause that
272
+ // might still be in progress, so it isn't worth deferring the drain until the worker has exited.
273
+ flushMetrics()
248
274
  guardrailMetrics = null
275
+ pauseDurations = null
249
276
  }
250
277
 
251
278
  // Call any pending ack callbacks
@@ -261,14 +288,19 @@ function cleanup (error) {
261
288
  }
262
289
 
263
290
  /**
264
- * Convert the guardrail counters accumulated by the probe sampler and the worker into telemetry metrics.
291
+ * Convert the guardrail counters accumulated by the probe sampler and the worker, and the pause durations recorded by
292
+ * the worker, into telemetry metrics.
265
293
  */
266
- function flushGuardrailMetrics () {
267
- if (guardrailMetrics === null) return
294
+ function flushMetrics () {
295
+ if (guardrailMetrics === null || pauseDurations === null) return
268
296
  const namespace = telemetryMetrics.manager.namespace(TELEMETRY_NAMESPACE)
269
297
  guardrailMetrics.drain((metric, tags, count) => {
270
298
  namespace.count(metric, tags).inc(count)
271
299
  })
300
+ const pauseDurationMetric = namespace.distribution('execution.pause.duration')
301
+ pauseDurations.drain((durationMs, count) => {
302
+ pauseDurationMetric.track(durationMs, count)
303
+ })
272
304
  }
273
305
 
274
306
  /**
@@ -3,11 +3,14 @@
3
3
  const { inspect, types } = require('node:util')
4
4
 
5
5
  const { NODE_MAJOR } = require('../../../../version')
6
+ const { REDACTED_PLACEHOLDER } = require('./redaction')
6
7
 
7
8
  /** @typedef {NonNullable<ReturnType<typeof globalThis.Object.getOwnPropertyDescriptor>>} PropertyDescriptor */
8
9
  /** @typedef {Map<unknown, unknown> | Set<unknown>} Collection */
10
+ /** @typedef {(name: string) => boolean} IsRedactedIdentifier */
9
11
 
10
12
  const mapEntries = Map.prototype.entries
13
+ const mapKeys = Map.prototype.keys
11
14
  const mapSet = Map.prototype.set
12
15
  const mapSizeGetter = Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
13
16
  const setAdd = Set.prototype.add
@@ -25,23 +28,34 @@ const segmentInspectOptions = {
25
28
  maxStringLength: 8 * 1024,
26
29
  breakLength: Infinity,
27
30
  }
31
+ const redactedDescriptor = { value: REDACTED_PLACEHOLDER, enumerable: true }
28
32
 
29
- module.exports = inspectSegment
33
+ module.exports = createInspectSegment
34
+
35
+ /**
36
+ * @param {IsRedactedIdentifier} isRedactedIdentifier - Whether the value of a property or Map entry with the given key
37
+ * must be redacted.
38
+ */
39
+ function createInspectSegment (isRedactedIdentifier) {
40
+ return (/** @type {unknown} */ value) => inspectSegment(value, isRedactedIdentifier)
41
+ }
30
42
 
31
43
  /**
32
44
  * Inspect a dynamic-instrumentation template value without invoking user code.
33
45
  * Unlike collections, `util.inspect` has no option for limiting the number of object properties, so this function
34
- * truncates objects before inspecting them.
46
+ * truncates objects before inspecting them. It also replaces the values of redacted properties and Map entries, the
47
+ * same way they are redacted from snapshots.
35
48
  *
36
49
  * @param {unknown} value
50
+ * @param {IsRedactedIdentifier} isRedactedIdentifier
37
51
  */
38
- function inspectSegment (value) {
52
+ function inspectSegment (value, isRedactedIdentifier) {
39
53
  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
40
54
  return inspect(value, segmentInspectOptions)
41
55
  }
42
56
  if (types.isProxy(value)) return '[Proxy]'
43
- if (types.isMap(value)) return inspectCollection(value, true)
44
- if (types.isSet(value)) return inspectCollection(value, false)
57
+ if (types.isMap(value)) return inspectCollection(value, true, isRedactedIdentifier)
58
+ if (types.isSet(value)) return inspectCollection(value, false, isRedactedIdentifier)
45
59
  if (
46
60
  Array.isArray(value) ||
47
61
  types.isTypedArray(value) ||
@@ -72,11 +86,18 @@ function inspectSegment (value) {
72
86
  if (inspectionCanRunUserCode(value)) {
73
87
  return '[Value omitted: inspection may execute user code]'
74
88
  }
75
- return inspect(value, segmentInspectOptions)
89
+ if (!hasRedactedKey(keys, isRedactedIdentifier)) return inspect(value, segmentInspectOptions)
76
90
  }
77
91
 
78
- const truncated = {}
79
- for (let i = 0; i < maxProperties; i++) {
92
+ // Inspect a plain object holding the rendered properties instead, with the values of redacted properties replaced.
93
+ // The copy doesn't keep the prototype, as built-ins like `URL` can't be inspected without their internal slots.
94
+ const copy = {}
95
+ const copiedCount = Math.min(propertyCount, maxProperties)
96
+ for (let i = 0; i < copiedCount; i++) {
97
+ if (isRedactedKey(keys[i], isRedactedIdentifier)) {
98
+ Object.defineProperty(copy, keys[i], redactedDescriptor)
99
+ continue
100
+ }
80
101
  const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, keys[i]))
81
102
  if (
82
103
  (keys[i] === Symbol.toStringTag && descriptor.get !== undefined) ||
@@ -84,28 +105,34 @@ function inspectSegment (value) {
84
105
  ) {
85
106
  return '[Value omitted: inspection may execute user code]'
86
107
  }
87
- if (descriptor.value === value) descriptor.value = truncated
88
- Object.defineProperty(truncated, keys[i], descriptor)
108
+ if (descriptor.value === value) descriptor.value = copy
109
+ Object.defineProperty(copy, keys[i], descriptor)
89
110
  }
90
111
 
112
+ const inspected = inspect(copy, segmentInspectOptions)
113
+ if (propertyCount <= maxProperties) return inspected
114
+
91
115
  const omitted = propertyCount - maxProperties
92
- const inspected = inspect(truncated, segmentInspectOptions)
93
116
  return `${inspected.slice(0, -2)}, ... ${omitted} more ${omitted === 1 ? 'property' : 'properties'} }`
94
117
  }
95
118
 
96
119
  /**
97
- * Inspect a Map or Set while bounding the number of entries on Node.js 18, where `util.inspect` does not.
120
+ * Inspect a Map or Set while bounding the number of entries on Node.js 18, where `util.inspect` does not, and replacing
121
+ * the values of redacted Map entries.
98
122
  *
99
123
  * @param {Collection} value
100
124
  * @param {boolean} isMap
125
+ * @param {IsRedactedIdentifier} isRedactedIdentifier
101
126
  */
102
- function inspectCollection (value, isMap) {
103
- if (NODE_MAJOR !== 18) return inspect(value, segmentInspectOptions)
104
-
127
+ function inspectCollection (value, isMap, isRedactedIdentifier) {
105
128
  const size = (isMap ? mapSizeGetter : setSizeGetter).call(value)
106
- if (size <= maxCollectionEntries) return inspect(value, segmentInspectOptions)
129
+ const truncate = NODE_MAJOR === 18 && size > maxCollectionEntries
130
+ if (!truncate && !(isMap && mapHasRedactedKey(/** @type {Map<unknown, unknown>} */ (value), isRedactedIdentifier))) {
131
+ return inspect(value, segmentInspectOptions)
132
+ }
107
133
 
108
- const truncated = isMap ? new Map() : new Set()
134
+ // Only the entries that are rendered are copied
135
+ const copy = isMap ? new Map() : new Set()
109
136
  const iterator = (isMap ? mapEntries : setValues).call(value)
110
137
  const iteratorNext = isMap ? mapIteratorNext : setIteratorNext
111
138
 
@@ -115,22 +142,64 @@ function inspectCollection (value, isMap) {
115
142
 
116
143
  if (isMap) {
117
144
  const entry = result.value
118
- const key = entry[0] === value ? truncated : entry[0]
119
- const entryValue = entry[1] === value ? truncated : entry[1]
120
- mapSet.call(truncated, key, entryValue)
145
+ const key = entry[0] === value ? copy : entry[0]
146
+ let entryValue = entry[1] === value ? copy : entry[1]
147
+ if (isRedactedKey(entry[0], isRedactedIdentifier)) entryValue = REDACTED_PLACEHOLDER
148
+ mapSet.call(copy, key, entryValue)
121
149
  } else {
122
- const entryValue = result.value === value ? truncated : result.value
123
- setAdd.call(truncated, entryValue)
150
+ const entryValue = result.value === value ? copy : result.value
151
+ setAdd.call(copy, entryValue)
124
152
  }
125
153
  }
126
154
 
155
+ const inspected = inspect(copy, segmentInspectOptions)
156
+ if (size <= maxCollectionEntries) return inspected
157
+
127
158
  const type = isMap ? 'Map' : 'Set'
128
- const inspected = inspect(truncated, segmentInspectOptions)
129
159
  const normalized = inspected.replace(`${type}(${maxCollectionEntries})`, `${type}(${size})`)
130
160
  const remaining = size - maxCollectionEntries
131
161
  return `${normalized.slice(0, -2)}, ... ${remaining} more item${remaining === 1 ? '' : 's'} }`
132
162
  }
133
163
 
164
+ /**
165
+ * Determine whether any of the Map entries rendered by `util.inspect` has a redacted key.
166
+ *
167
+ * @param {Map<unknown, unknown>} map
168
+ * @param {IsRedactedIdentifier} isRedactedIdentifier
169
+ */
170
+ function mapHasRedactedKey (map, isRedactedIdentifier) {
171
+ const iterator = mapKeys.call(map)
172
+ for (let i = 0; i < maxCollectionEntries; i++) {
173
+ const result = mapIteratorNext.call(iterator)
174
+ if (result.done) return false
175
+ if (isRedactedKey(result.value, isRedactedIdentifier)) return true
176
+ }
177
+ return false
178
+ }
179
+
180
+ /**
181
+ * @param {(string | symbol)[]} keys
182
+ * @param {IsRedactedIdentifier} isRedactedIdentifier
183
+ */
184
+ function hasRedactedKey (keys, isRedactedIdentifier) {
185
+ for (let i = 0; i < keys.length; i++) {
186
+ if (isRedactedKey(keys[i], isRedactedIdentifier)) return true
187
+ }
188
+ return false
189
+ }
190
+
191
+ /**
192
+ * Determine whether a property or Map key is redacted. Like in snapshots, only string and symbol keys can be redacted.
193
+ *
194
+ * @param {unknown} key
195
+ * @param {IsRedactedIdentifier} isRedactedIdentifier
196
+ */
197
+ function isRedactedKey (key, isRedactedIdentifier) {
198
+ if (typeof key === 'string') return isRedactedIdentifier(key)
199
+ if (typeof key === 'symbol') return isRedactedIdentifier(key.description ?? '')
200
+ return false
201
+ }
202
+
134
203
  /**
135
204
  * Determine whether inspecting a value could invoke a proxy trap or toStringTag getter.
136
205
  *
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
4
+
5
+ /**
6
+ * Durations of the pauses caused by probes, shared between the debugger worker thread and the main thread.
7
+ *
8
+ * The worker records the duration of every pause, while the main thread converts them into a telemetry distribution.
9
+ * The main thread can't process a message while the application keeps it busy, for example in a synchronous loop that
10
+ * hits a probe on every iteration, so a message per pause would queue up without bound. Instead the worker counts each
11
+ * duration in a fixed set of buckets in a shared buffer using atomics, and the main thread periodically drains the
12
+ * buckets. Memory use is therefore constant no matter how many pauses happen between two drains.
13
+ *
14
+ * The buckets are the keys of the mapping used by the DDSketch backing telemetry distributions. Each drained bucket is
15
+ * reported as the value the sketch maps back to the same key, so the resulting distribution is as accurate as tracking
16
+ * each duration individually. Durations outside of the covered range are counted in the closest bucket.
17
+ */
18
+
19
+ // Created the same way as the sketch of telemetry distributions, so both map durations to the same keys
20
+ const { mapping } = new LogCollapsingLowestDenseDDSketch()
21
+
22
+ const MIN_KEY = mapping.key(0.001) // 1 µs
23
+ const MAX_KEY = mapping.key(60 * 60 * 1000) // 1 hour
24
+ const BUCKET_COUNT = MAX_KEY - MIN_KEY + 1
25
+
26
+ class PauseDurationHistogram {
27
+ /** @type {Uint32Array} */
28
+ #counts
29
+
30
+ /**
31
+ * @param {SharedArrayBuffer} buffer - A buffer created with {@link PauseDurationHistogram.createBuffer}
32
+ */
33
+ constructor (buffer) {
34
+ this.#counts = new Uint32Array(buffer)
35
+ }
36
+
37
+ /**
38
+ * Create the shared buffer backing the histogram.
39
+ *
40
+ * @returns {SharedArrayBuffer}
41
+ */
42
+ static createBuffer () {
43
+ return new SharedArrayBuffer(BUCKET_COUNT * Uint32Array.BYTES_PER_ELEMENT)
44
+ }
45
+
46
+ /**
47
+ * Record the duration of a single pause.
48
+ *
49
+ * @param {number} durationMs - The pause duration, in milliseconds
50
+ */
51
+ record (durationMs) {
52
+ const key = Math.min(Math.max(mapping.key(durationMs), MIN_KEY), MAX_KEY)
53
+ Atomics.add(this.#counts, key - MIN_KEY, 1)
54
+ }
55
+
56
+ /**
57
+ * Reset all buckets and report the ones that were non-empty.
58
+ *
59
+ * @param {(durationMs: number, count: number) => void} report - Called once per non-empty bucket with the duration
60
+ * representing the bucket and the number of pauses counted in it
61
+ */
62
+ drain (report) {
63
+ for (let i = 0; i < BUCKET_COUNT; i++) {
64
+ const count = Atomics.exchange(this.#counts, i, 0)
65
+ if (count !== 0) {
66
+ report(mapping.value(MIN_KEY + i), count)
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ module.exports = {
73
+ PauseDurationHistogram,
74
+ }