dd-trace 6.18.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 (123) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -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
+ }
@@ -20,28 +20,62 @@ const ddTraceGlobal = /** @type {Record<symbol, SharedArrayBuffer | object | und
20
20
  /** @type {Record<symbol, unknown>} */ (globalThis)[Symbol.for(DD_TRACE_SYMBOL)]
21
21
  )
22
22
 
23
+ /**
24
+ * @typedef {object} ProbeThrottle
25
+ * @property {bigint} untilNs - The probe is skipped at entry until this point in time
26
+ * @property {boolean} timedOut - Whether the throttle was caused by an evaluation exceeding its time budget
27
+ * @property {string | undefined} error - A condition error not yet handed over to the worker
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} ProbeSamplerConfig
32
+ * @property {{ DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS: number }} dynamicInstrumentation
33
+ */
34
+
35
+ let evaluationTimeoutNs = 0n
36
+
23
37
  module.exports = {
38
+ configureProbeSampler,
24
39
  installProbeSampler,
25
40
  uninstallProbeSampler,
26
41
  }
27
42
 
43
+ /**
44
+ * Apply the evaluation time budget from the tracer configuration.
45
+ *
46
+ * @param {ProbeSamplerConfig} config - The tracer configuration.
47
+ */
48
+ function configureProbeSampler (config) {
49
+ evaluationTimeoutNs = BigInt(config.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS) *
50
+ 1_000_000n
51
+ }
52
+
28
53
  /**
29
54
  * Install the runtime sampler in the debuggee context.
30
55
  *
31
56
  * @param {import('./guardrail-metrics').GuardrailMetrics} guardrailMetrics - Counters for skipped probe hits.
57
+ * @param {ProbeSamplerConfig} config - The tracer configuration.
32
58
  * @returns {SharedArrayBuffer} The shared sampler buffer to pass to the debugger worker.
33
59
  */
34
- function installProbeSampler (guardrailMetrics) {
60
+ function installProbeSampler (guardrailMetrics, config) {
61
+ configureProbeSampler(config)
62
+
35
63
  const buffer = createProbeSamplerBuffer()
36
64
 
37
65
  const lastCaptureNsByProbeId = new Map()
38
66
  /**
39
- * Probes whose condition recently failed to evaluate, keyed by probe id. The error is kept until the worker picks it
40
- * up for the error result.
67
+ * Probes that are skipped at entry because a recent evaluation failed or exceeded its time budget. One error result
68
+ * is reported per throttle window, so the probe stays visible without repeatedly paying for the evaluation.
41
69
  *
42
- * @type {Map<string, { throttledUntilNs: bigint, error: string | undefined }>}
70
+ * @type {Map<string, ProbeThrottle>}
71
+ */
72
+ const throttleByProbeId = new Map()
73
+ /**
74
+ * When the condition currently being evaluated started. V8 doesn't evaluate breakpoint conditions re-entrantly, and
75
+ * the conditions of probes at the same location are evaluated one after another, so one start time is enough. It is
76
+ * kept here rather than in the compiled condition so the condition can't shadow a variable of the paused frame.
43
77
  */
44
- const conditionErrorByProbeId = new Map()
78
+ let conditionStartNs = 0n
45
79
  const sampledProbeIndexes = new Int32Array(buffer)
46
80
  Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0)
47
81
  Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0)
@@ -52,7 +86,7 @@ function installProbeSampler (guardrailMetrics) {
52
86
 
53
87
  ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)] = {
54
88
  /**
55
- * Decide if a probe should be sampled and store sampled probe indexes for the debugger worker.
89
+ * Decide if a probe without a condition should be sampled and store sampled probe indexes for the debugger worker.
56
90
  *
57
91
  * @param {number} probeIndex - The worker-side probe sampling index.
58
92
  * @param {string} probeId - The probe id.
@@ -61,55 +95,46 @@ function installProbeSampler (guardrailMetrics) {
61
95
  */
62
96
  makeSampleDecision (probeIndex, probeId, nsBetweenSampling, isSnapshotProducingProbe) {
63
97
  const now = process.hrtime.bigint()
64
- const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
65
- if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) {
66
- guardrailMetrics.eventSkipped(
67
- SKIPPED_REASON.RATE_LIMIT_PROBE,
68
- isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
69
- )
70
- return false
71
- }
72
-
73
- let shouldResetGlobalSnapshotRateWindow = false
74
- if (isSnapshotProducingProbe === true) {
75
- if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
76
- shouldResetGlobalSnapshotRateWindow = true
77
- } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
78
- guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
79
- return false
80
- }
81
- }
82
-
83
- if (!storeSampledProbeIndex(probeIndex)) return false
84
-
85
- if (isSnapshotProducingProbe === true) {
86
- if (shouldResetGlobalSnapshotRateWindow === true) {
87
- snapshotsSampledWithinTheLastSecond = 1
88
- globalSnapshotSamplingRateWindowStart = now
89
- } else {
90
- snapshotsSampledWithinTheLastSecond++
91
- }
92
- }
98
+ if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
99
+ return sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
100
+ },
93
101
 
94
- lastCaptureNsByProbeId.set(probeId, now)
102
+ /**
103
+ * Decide if a probe's condition should be evaluated, or skipped because a recent evaluation error throttled it. If
104
+ * it should be evaluated, this starts timing the evaluation against the evaluation time budget.
105
+ *
106
+ * @param {string} probeId - The probe id.
107
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
108
+ */
109
+ shouldEvaluateCondition (probeId, isSnapshotProducingProbe) {
110
+ const now = process.hrtime.bigint()
111
+ if (isThrottled(probeId, now, isSnapshotProducingProbe)) return false
112
+ conditionStartNs = now
95
113
  return true
96
114
  },
97
115
 
98
116
  /**
99
- * Decide if a probe's condition should be evaluated, or skipped because a recent evaluation error throttled it.
117
+ * Decide if a probe should be sampled now that its condition has been evaluated. A condition that exceeded the
118
+ * evaluation time budget is reported like a condition error instead, regardless of its result.
100
119
  *
120
+ * @param {number} probeIndex - The worker-side probe sampling index.
101
121
  * @param {string} probeId - The probe id.
122
+ * @param {boolean} matched - Whether the condition evaluated to `true`.
123
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
124
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
102
125
  */
103
- shouldEvaluateCondition (probeId) {
104
- const state = conditionErrorByProbeId.get(probeId)
105
- if (state === undefined) return true
106
- if (process.hrtime.bigint() < state.throttledUntilNs) return false
107
- conditionErrorByProbeId.delete(probeId)
108
- return true
126
+ conditionEvaluated (probeIndex, probeId, matched, nsBetweenSampling, isSnapshotProducingProbe) {
127
+ const now = process.hrtime.bigint()
128
+ const elapsedNs = now - conditionStartNs
129
+ if (elapsedNs > evaluationTimeoutNs) {
130
+ return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
131
+ }
132
+ return matched === true && sample(probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe)
109
133
  },
110
134
 
111
135
  /**
112
- * Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported.
136
+ * Record that a probe's condition threw, throttle the probe, and request a pause if the error can be reported. A
137
+ * condition that exceeded the evaluation time budget before throwing is reported as a timeout instead.
113
138
  *
114
139
  * Error results bypass the per-probe and global rate limits: they are rate limited by the throttle instead, which
115
140
  * allows one error result per probe per window. If the shared buffer is full, the error is dropped but the throttle
@@ -120,12 +145,12 @@ function installProbeSampler (guardrailMetrics) {
120
145
  * @param {unknown} error - The value thrown by the condition.
121
146
  */
122
147
  conditionError (probeIndex, probeId, error) {
123
- const stored = storeSampledProbeIndex(probeIndex | CONDITION_ERROR_FLAG)
124
- conditionErrorByProbeId.set(probeId, {
125
- throttledUntilNs: process.hrtime.bigint() + CONDITION_ERROR_THROTTLE_NS,
126
- error: stored ? describeError(error) : undefined,
127
- })
128
- return stored
148
+ const now = process.hrtime.bigint()
149
+ const elapsedNs = now - conditionStartNs
150
+ if (elapsedNs > evaluationTimeoutNs) {
151
+ return recordConditionError(probeIndex, probeId, now, describeTimeout(elapsedNs), true)
152
+ }
153
+ return recordConditionError(probeIndex, probeId, now, describeError(error), false)
129
154
  },
130
155
 
131
156
  /**
@@ -135,13 +160,28 @@ function installProbeSampler (guardrailMetrics) {
135
160
  * @returns {string | undefined} The error description, if any.
136
161
  */
137
162
  takeConditionError (probeId) {
138
- const state = conditionErrorByProbeId.get(probeId)
163
+ const state = throttleByProbeId.get(probeId)
139
164
  if (state === undefined) return
140
165
  const { error } = state
141
166
  state.error = undefined
142
167
  return error
143
168
  },
144
169
 
170
+ /**
171
+ * Throttle a probe whose evaluation exceeded its time budget in the worker, e.g. while evaluating its template.
172
+ * Called by the worker after the result has been reported.
173
+ *
174
+ * @param {string} probeId - The probe id.
175
+ */
176
+ evaluationTimedOut (probeId) {
177
+ throttleByProbeId.set(probeId, {
178
+ untilNs: process.hrtime.bigint() + CONDITION_ERROR_THROTTLE_NS,
179
+ timedOut: true,
180
+ // A hit that raced this request may have recorded a condition error that its pause has yet to report
181
+ error: throttleByProbeId.get(probeId)?.error,
182
+ })
183
+ },
184
+
145
185
  /**
146
186
  * Remove cached sampling state for a probe.
147
187
  *
@@ -149,10 +189,100 @@ function installProbeSampler (guardrailMetrics) {
149
189
  */
150
190
  remove (probeId) {
151
191
  lastCaptureNsByProbeId.delete(probeId)
152
- conditionErrorByProbeId.delete(probeId)
192
+ throttleByProbeId.delete(probeId)
153
193
  },
154
194
  }
155
195
 
196
+ /**
197
+ * Check if a probe is skipped at entry because of a recent evaluation error, recording the skip when the error was
198
+ * an exceeded time budget.
199
+ *
200
+ * @param {string} probeId - The probe id.
201
+ * @param {bigint | undefined} now - The current time, if already available.
202
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe produces snapshots.
203
+ */
204
+ function isThrottled (probeId, now, isSnapshotProducingProbe) {
205
+ const state = throttleByProbeId.get(probeId)
206
+ if (state === undefined) return false
207
+ now ??= process.hrtime.bigint()
208
+ if (now >= state.untilNs) {
209
+ throttleByProbeId.delete(probeId)
210
+ return false
211
+ }
212
+ if (state.timedOut) {
213
+ guardrailMetrics.eventSkipped(
214
+ SKIPPED_REASON.EVALUATION_TIMEOUT,
215
+ isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
216
+ )
217
+ }
218
+ return true
219
+ }
220
+
221
+ /**
222
+ * Apply the per-probe and global rate limits and store the sampled probe index for the debugger worker.
223
+ *
224
+ * @param {number} probeIndex - The worker-side probe sampling index.
225
+ * @param {string} probeId - The probe id.
226
+ * @param {bigint} now - The current time.
227
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
228
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
229
+ */
230
+ function sample (probeIndex, probeId, now, nsBetweenSampling, isSnapshotProducingProbe) {
231
+ const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
232
+ if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) {
233
+ guardrailMetrics.eventSkipped(
234
+ SKIPPED_REASON.RATE_LIMIT_PROBE,
235
+ isSnapshotProducingProbe === true ? EVENT_TYPE.SNAPSHOT : EVENT_TYPE.LOG
236
+ )
237
+ return false
238
+ }
239
+
240
+ let shouldResetGlobalSnapshotRateWindow = false
241
+ if (isSnapshotProducingProbe === true) {
242
+ if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
243
+ shouldResetGlobalSnapshotRateWindow = true
244
+ } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
245
+ guardrailMetrics.eventSkipped(SKIPPED_REASON.RATE_LIMIT_GLOBAL, EVENT_TYPE.SNAPSHOT)
246
+ return false
247
+ }
248
+ }
249
+
250
+ if (!storeSampledProbeIndex(probeIndex)) return false
251
+
252
+ if (isSnapshotProducingProbe === true) {
253
+ if (shouldResetGlobalSnapshotRateWindow === true) {
254
+ snapshotsSampledWithinTheLastSecond = 1
255
+ globalSnapshotSamplingRateWindowStart = now
256
+ } else {
257
+ snapshotsSampledWithinTheLastSecond++
258
+ }
259
+ }
260
+
261
+ lastCaptureNsByProbeId.set(probeId, now)
262
+ return true
263
+ }
264
+
265
+ /**
266
+ * Throttle a probe whose condition failed and request a pause so the error can be reported. Error results bypass the
267
+ * per-probe and global rate limits: they are rate limited by the throttle instead, which allows one error result per
268
+ * probe per window.
269
+ *
270
+ * @param {number} probeIndex - The worker-side probe sampling index.
271
+ * @param {string} probeId - The probe id.
272
+ * @param {bigint} now - The current time.
273
+ * @param {string} error - The error description.
274
+ * @param {boolean} timedOut - Whether the error is an exceeded time budget.
275
+ */
276
+ function recordConditionError (probeIndex, probeId, now, error, timedOut) {
277
+ const stored = storeSampledProbeIndex(probeIndex | CONDITION_ERROR_FLAG)
278
+ throttleByProbeId.set(probeId, {
279
+ untilNs: now + CONDITION_ERROR_THROTTLE_NS,
280
+ timedOut,
281
+ error: stored ? error : undefined,
282
+ })
283
+ return stored
284
+ }
285
+
156
286
  /**
157
287
  * Hand a sampled probe index over to the worker for the upcoming pause.
158
288
  *
@@ -224,6 +354,16 @@ function getErrorStringProperty (error, property) {
224
354
  }
225
355
  }
226
356
 
357
+ /**
358
+ * Describe a condition evaluation that exceeded its time budget.
359
+ *
360
+ * @param {bigint} elapsedNs - The time the evaluation took.
361
+ */
362
+ function describeTimeout (elapsedNs) {
363
+ return `Condition evaluation exceeded its time budget of ${evaluationTimeoutNs / 1_000_000n}ms ` +
364
+ `(took ${(Number(elapsedNs) / 1_000_000).toFixed(1)}ms)`
365
+ }
366
+
227
367
  /**
228
368
  * Create the shared buffer used to hand sampled probe indexes from breakpoint conditions to the debugger worker.
229
369
  *