dd-trace 5.49.1 → 5.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,21 +69,11 @@ Changes associated with each individual release are documented on the [GitHub Re
69
69
  Please read the [CONTRIBUTING.md](https://github.com/DataDog/dd-trace-js/blob/master/CONTRIBUTING.md) document before contributing to this open source project.
70
70
 
71
71
 
72
- ## EcmaScript Modules (ESM) Support
72
+ ## ECMAScript Modules (ESM) Support
73
73
 
74
- ESM support requires an additional command-line argument. Use the following to enable experimental ESM support with your application:
74
+ ESM support requires an _additional_ command line argument when starting the Node.js process.
75
+ For more information, see the [section on ESM support](https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#esm-applications-only-import-the-loader) in the Node.js tracer documentation.
75
76
 
76
- Node.js < v20.6
77
-
78
- ```sh
79
- node --loader dd-trace/loader-hook.mjs entrypoint.js
80
- ```
81
-
82
- Node.js >= v20.6
83
-
84
- ```sh
85
- node --import dd-trace/register.js entrypoint.js
86
- ```
87
77
 
88
78
  ## Serverless / Lambda
89
79
 
package/index.d.ts CHANGED
@@ -2251,6 +2251,7 @@ declare namespace tracer {
2251
2251
  /**
2252
2252
  * Defines the pattern to ignore cookie names in the vulnerability hash calculation
2253
2253
  * @default ".{32,}"
2254
+ * @deprecated This property has no effect because hash calculation algorithm has been updated for cookie vulnerabilities
2254
2255
  */
2255
2256
  cookieFilterPattern?: string,
2256
2257
 
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.49.1",
3
+ "version": "5.50.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
7
7
  "scripts": {
8
8
  "env": "bash ./plugin-env",
9
9
  "preinstall": "node scripts/preinstall.js",
10
- "bench": "node benchmark",
11
- "bench:e2e": "SERVICES=mongo yarn services && cd benchmark/e2e && node benchmark-run.js --duration=30",
10
+ "bench": "node benchmark/index.js",
12
11
  "bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js",
13
12
  "type:doc": "cd docs && yarn && yarn build",
14
13
  "type:test": "cd docs && yarn && yarn test",
@@ -47,15 +47,16 @@ class DatadogStorage extends AsyncLocalStorage {
47
47
  * key. This is useful if you've stashed a handle somewhere and want to
48
48
  * retrieve the store with it.
49
49
  *
50
- * @param handle {{}}
50
+ * @param [handle] {{}}
51
51
  * @returns {T | undefined}
52
52
  */
53
53
  getStore (handle) {
54
54
  if (!handle) {
55
55
  handle = super.getStore()
56
56
  }
57
-
58
- return stores.get(handle)
57
+ if (handle) {
58
+ return stores.get(handle)
59
+ }
59
60
  }
60
61
 
61
62
  /**
@@ -1,85 +1,105 @@
1
1
  'use strict'
2
2
 
3
+ const skipMethods = new Set([
4
+ 'caller',
5
+ 'arguments',
6
+ 'name',
7
+ 'length'
8
+ ])
9
+
3
10
  function copyProperties (original, wrapped) {
4
- // TODO getPrototypeOf is not fast. Should we instead do this in specific
5
- // instrumentations where needed?
6
- const proto = Object.getPrototypeOf(original)
7
- if (proto !== Function.prototype) {
11
+ if (original.constructor !== wrapped.constructor) {
12
+ const proto = Object.getPrototypeOf(original)
8
13
  Object.setPrototypeOf(wrapped, proto)
9
14
  }
10
- const props = Object.getOwnPropertyDescriptors(original)
11
- const keys = Reflect.ownKeys(props)
12
-
13
- for (const key of keys) {
14
- try {
15
- Object.defineProperty(wrapped, key, props[key])
16
- } catch (e) {
17
- // TODO: figure out how to handle this without a try/catch
15
+
16
+ const ownKeys = Reflect.ownKeys(original)
17
+ if (original.length !== wrapped.length) {
18
+ Object.defineProperty(wrapped, 'length', { value: original.length, configurable: true })
19
+ }
20
+ if (original.name !== wrapped.name) {
21
+ Object.defineProperty(wrapped, 'name', { value: original.name, configurable: true })
22
+ }
23
+ if (ownKeys.length !== 2) {
24
+ for (const key of ownKeys) {
25
+ if (skipMethods.has(key)) continue
26
+ const descriptor = Object.getOwnPropertyDescriptor(original, key)
27
+ if (descriptor.writable && descriptor.enumerable && descriptor.configurable) {
28
+ wrapped[key] = original[key]
29
+ } else if (descriptor.writable || descriptor.configurable || !Object.hasOwn(wrapped, key)) {
30
+ Object.defineProperty(wrapped, key, descriptor)
31
+ }
18
32
  }
19
33
  }
20
34
  }
21
35
 
22
36
  function wrapFunction (original, wrapper) {
23
- if (typeof original === 'function') assertNotClass(original)
24
-
25
37
  const wrapped = wrapper(original)
26
38
 
27
- if (typeof original === 'function') copyProperties(original, wrapped)
39
+ if (typeof original === 'function') {
40
+ assertNotClass(original)
41
+ copyProperties(original, wrapped)
42
+ }
28
43
 
29
44
  return wrapped
30
45
  }
31
46
 
32
- const wrapFn = function (original, delegate) {
33
- throw new Error('calling `wrap()` with 2 args is deprecated. Use wrapFunction instead.')
34
- }
35
-
36
- function wrapMethod (target, name, wrapper, noAssert) {
37
- if (!noAssert) {
38
- assertMethod(target, name)
39
- assertFunction(wrapper)
47
+ function wrap (target, name, wrapper) {
48
+ assertMethod(target, name)
49
+ if (typeof wrapper !== 'function') {
50
+ throw new Error(wrapper ? 'Target is not a function' : 'No function provided')
40
51
  }
41
52
 
42
53
  const original = target[name]
43
54
  const wrapped = wrapper(original)
44
55
 
45
- const descriptor = Object.getOwnPropertyDescriptor(target, name)
46
-
47
- const attributes = {
48
- configurable: true,
49
- ...descriptor
50
- }
51
-
52
56
  if (typeof original === 'function') copyProperties(original, wrapped)
53
57
 
54
- if (descriptor) {
58
+ let descriptor = Object.getOwnPropertyDescriptor(target, name)
59
+
60
+ // No descriptor means original was on the prototype
61
+ if (descriptor === undefined) {
62
+ descriptor = {
63
+ value: wrapped,
64
+ writable: true,
65
+ configurable: true,
66
+ enumerable: false
67
+ }
68
+ } else if (descriptor.writable) {
69
+ // Fast path for assigned properties.
70
+ if (descriptor.configurable && descriptor.enumerable) {
71
+ target[name] = wrapped
72
+ return target
73
+ }
74
+ descriptor.value = wrapped
75
+ } else {
55
76
  if (descriptor.get || descriptor.set) {
56
- attributes.get = () => wrapped
77
+ // TODO(BridgeAR): What happens in case there is a setter? This seems wrong?
78
+ // What happens in case the user does indeed set this to a different value?
79
+ // In that case the getter would potentially return the wrong value?
80
+ descriptor.get = () => wrapped
57
81
  } else {
58
- attributes.value = wrapped
82
+ descriptor.value = wrapped
59
83
  }
60
84
 
61
- // TODO: create a single object for multiple wrapped methods
62
85
  if (descriptor.configurable === false) {
86
+ // TODO(BridgeAR): Bail out instead (throw). It is unclear if the newly
87
+ // created object is actually used. If it's not used, the wrapping would
88
+ // have had no effect without noticing. It is also unclear what would happen
89
+ // in case user code would check for properties to be own properties. That
90
+ // would fail with this code. A function being replaced with an object is
91
+ // also not possible.
63
92
  return Object.create(target, {
64
- [name]: attributes
93
+ [name]: descriptor
65
94
  })
66
95
  }
67
- } else { // no descriptor means original was on the prototype
68
- attributes.value = wrapped
69
- attributes.writable = true
70
96
  }
71
97
 
72
- Object.defineProperty(target, name, attributes)
98
+ Object.defineProperty(target, name, descriptor)
73
99
 
74
100
  return target
75
101
  }
76
102
 
77
- function wrap (target, name, wrapper) {
78
- return typeof name === 'function'
79
- ? wrapFn(target, name)
80
- : wrapMethod(target, name, wrapper)
81
- }
82
-
83
103
  function massWrap (targets, names, wrapper) {
84
104
  targets = toArray(targets)
85
105
  names = toArray(names)
@@ -96,30 +116,18 @@ function toArray (maybeArray) {
96
116
  }
97
117
 
98
118
  function assertMethod (target, name) {
99
- if (!target) {
100
- throw new Error('No target object provided.')
101
- }
102
-
103
- if (typeof target !== 'object' && typeof target !== 'function') {
104
- throw new Error('Invalid target.')
105
- }
106
-
107
- if (!target[name]) {
108
- throw new Error(`No original method ${name}.`)
109
- }
110
-
111
- if (typeof target[name] !== 'function') {
112
- throw new Error(`Original method ${name} is not a function.`)
113
- }
114
- }
115
-
116
- function assertFunction (target) {
117
- if (!target) {
118
- throw new Error('No function provided.')
119
- }
119
+ if (typeof target?.[name] !== 'function') {
120
+ let message = 'No target object provided'
121
+
122
+ if (target) {
123
+ if (typeof target !== 'object' && typeof target !== 'function') {
124
+ message = 'Invalid target'
125
+ } else {
126
+ message = target[name] ? `Original method ${name} is not a function` : `No original method ${name}`
127
+ }
128
+ }
120
129
 
121
- if (typeof target !== 'function') {
122
- throw new Error('Target is not a function.')
130
+ throw new Error(message)
123
131
  }
124
132
  }
125
133
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  const Analyzer = require('./vulnerability-analyzer')
4
4
  const { getNodeModulesPaths } = require('../path-line')
5
- const log = require('../../../log')
6
5
 
7
6
  const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
8
7
 
@@ -12,14 +11,7 @@ class CookieAnalyzer extends Analyzer {
12
11
  this.propertyToBeSafe = propertyToBeSafe.toLowerCase()
13
12
  }
14
13
 
15
- onConfigure (config) {
16
- try {
17
- this.cookieFilterRegExp = new RegExp(config.iast.cookieFilterPattern)
18
- } catch {
19
- log.error('[ASM] Invalid regex in cookieFilterPattern')
20
- this.cookieFilterRegExp = /.{32,}/
21
- }
22
-
14
+ onConfigure () {
23
15
  this.addSub(
24
16
  { channelName: 'datadog:iast:set-cookie', moduleName: 'http' },
25
17
  (cookieInfo) => this.analyze(cookieInfo)
@@ -35,14 +27,6 @@ class CookieAnalyzer extends Analyzer {
35
27
  return { value: cookieName }
36
28
  }
37
29
 
38
- _createHashSource (type, evidence, location) {
39
- if (typeof evidence.value === 'string' && evidence.value.match(this.cookieFilterRegExp)) {
40
- return 'FILTERED_' + this._type
41
- }
42
-
43
- return `${type}:${evidence.value}`
44
- }
45
-
46
30
  _getExcludedPaths () {
47
31
  return EXCLUDED_PATHS
48
32
  }
@@ -114,7 +114,7 @@ class IastPlugin extends Plugin {
114
114
  config = { enabled: config }
115
115
  }
116
116
  if (config.enabled && !this.configured) {
117
- this.onConfigure(config.tracerConfig)
117
+ this.onConfigure()
118
118
  this.configured = true
119
119
  }
120
120
 
@@ -181,8 +181,7 @@ function validateNamingVersion (versionString) {
181
181
  * If a blank path is provided a null is returned to signal that the feature is disabled.
182
182
  * An empty array means the feature is enabled but that no rules need to be applied.
183
183
  *
184
- * @param {string} input
185
- * @returns {[string]|null}
184
+ * @param {string | string[]} input
186
185
  */
187
186
  function splitJSONPathRules (input) {
188
187
  if (!input) return null
@@ -289,8 +288,7 @@ class Config {
289
288
  }
290
289
  const PROPAGATION_STYLE_INJECT = propagationStyle(
291
290
  'inject',
292
- options.tracePropagationStyle,
293
- this._getDefaultPropagationStyle(options)
291
+ options.tracePropagationStyle
294
292
  )
295
293
 
296
294
  validateOtelPropagators(PROPAGATION_STYLE_INJECT)
@@ -299,8 +297,6 @@ class Config {
299
297
  options.appsec = {
300
298
  enabled: options.appsec
301
299
  }
302
- } else if (options.appsec == null) {
303
- options.appsec = {}
304
300
  }
305
301
 
306
302
  const DD_INSTRUMENTATION_INSTALL_ID = coalesce(
@@ -505,7 +501,6 @@ class Config {
505
501
  this._setValue(defaults, 'grpc.server.error.statuses', GRPC_SERVER_ERROR_STATUSES)
506
502
  this._setValue(defaults, 'headerTags', [])
507
503
  this._setValue(defaults, 'hostname', '127.0.0.1')
508
- this._setValue(defaults, 'iast.cookieFilterPattern', '.{32,}')
509
504
  this._setValue(defaults, 'iast.dbRowsToTaint', 1)
510
505
  this._setValue(defaults, 'iast.deduplicationEnabled', true)
511
506
  this._setValue(defaults, 'iast.enabled', false)
@@ -680,7 +675,6 @@ class Config {
680
675
  DD_GRPC_CLIENT_ERROR_STATUSES,
681
676
  DD_GRPC_SERVER_ERROR_STATUSES,
682
677
  JEST_WORKER_ID,
683
- DD_IAST_COOKIE_FILTER_PATTERN,
684
678
  DD_IAST_DB_ROWS_TO_TAINT,
685
679
  DD_IAST_DEDUPLICATION_ENABLED,
686
680
  DD_IAST_ENABLED,
@@ -855,7 +849,6 @@ class Config {
855
849
  this._setIntegerRangeSet(env, 'grpc.server.error.statuses', DD_GRPC_SERVER_ERROR_STATUSES)
856
850
  this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
857
851
  this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME))
858
- this._setString(env, 'iast.cookieFilterPattern', DD_IAST_COOKIE_FILTER_PATTERN)
859
852
  this._setValue(env, 'iast.dbRowsToTaint', maybeInt(DD_IAST_DB_ROWS_TO_TAINT))
860
853
  this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED)
861
854
  this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED)
@@ -1010,27 +1003,27 @@ class Config {
1010
1003
  options.apmTracingEnabled,
1011
1004
  options.experimental?.appsec?.standalone && !options.experimental.appsec.standalone.enabled
1012
1005
  ))
1013
- this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec.apiSecurity?.enabled)
1014
- this._setValue(opts, 'appsec.blockedTemplateGraphql', maybeFile(options.appsec.blockedTemplateGraphql))
1015
- this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec.blockedTemplateHtml))
1016
- this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec.blockedTemplateHtml
1017
- this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec.blockedTemplateJson))
1018
- this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec.blockedTemplateJson
1019
- this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled)
1020
- this._setString(opts, 'appsec.eventTracking.mode', options.appsec.eventTracking?.mode)
1021
- this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex)
1022
- this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex)
1023
- this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled)
1024
- this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec.rateLimit))
1025
- this._optsUnprocessed['appsec.rateLimit'] = options.appsec.rateLimit
1026
- this._setString(opts, 'appsec.rules', options.appsec.rules)
1027
- this._setBoolean(opts, 'appsec.stackTrace.enabled', options.appsec.stackTrace?.enabled)
1028
- this._setValue(opts, 'appsec.stackTrace.maxDepth', maybeInt(options.appsec.stackTrace?.maxDepth))
1029
- this._optsUnprocessed['appsec.stackTrace.maxDepth'] = options.appsec.stackTrace?.maxDepth
1030
- this._setValue(opts, 'appsec.stackTrace.maxStackTraces', maybeInt(options.appsec.stackTrace?.maxStackTraces))
1031
- this._optsUnprocessed['appsec.stackTrace.maxStackTraces'] = options.appsec.stackTrace?.maxStackTraces
1032
- this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout))
1033
- this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout
1006
+ this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec?.apiSecurity?.enabled)
1007
+ this._setValue(opts, 'appsec.blockedTemplateGraphql', maybeFile(options.appsec?.blockedTemplateGraphql))
1008
+ this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec?.blockedTemplateHtml))
1009
+ this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec?.blockedTemplateHtml
1010
+ this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec?.blockedTemplateJson))
1011
+ this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec?.blockedTemplateJson
1012
+ this._setBoolean(opts, 'appsec.enabled', options.appsec?.enabled)
1013
+ this._setString(opts, 'appsec.eventTracking.mode', options.appsec?.eventTracking?.mode)
1014
+ this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec?.obfuscatorKeyRegex)
1015
+ this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec?.obfuscatorValueRegex)
1016
+ this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec?.rasp?.enabled)
1017
+ this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec?.rateLimit))
1018
+ this._optsUnprocessed['appsec.rateLimit'] = options.appsec?.rateLimit
1019
+ this._setString(opts, 'appsec.rules', options.appsec?.rules)
1020
+ this._setBoolean(opts, 'appsec.stackTrace.enabled', options.appsec?.stackTrace?.enabled)
1021
+ this._setValue(opts, 'appsec.stackTrace.maxDepth', maybeInt(options.appsec?.stackTrace?.maxDepth))
1022
+ this._optsUnprocessed['appsec.stackTrace.maxDepth'] = options.appsec?.stackTrace?.maxDepth
1023
+ this._setValue(opts, 'appsec.stackTrace.maxStackTraces', maybeInt(options.appsec?.stackTrace?.maxStackTraces))
1024
+ this._optsUnprocessed['appsec.stackTrace.maxStackTraces'] = options.appsec?.stackTrace?.maxStackTraces
1025
+ this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec?.wafTimeout))
1026
+ this._optsUnprocessed['appsec.wafTimeout'] = options.appsec?.wafTimeout
1034
1027
  this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
1035
1028
  this._setString(opts, 'clientIpHeader', options.clientIpHeader?.toLowerCase())
1036
1029
  this._setValue(opts, 'baggageMaxBytes', options.baggageMaxBytes)
@@ -1063,7 +1056,6 @@ class Config {
1063
1056
  this._optsUnprocessed.flushMinSpans = options.flushMinSpans
1064
1057
  this._setArray(opts, 'headerTags', options.headerTags)
1065
1058
  this._setString(opts, 'hostname', options.hostname)
1066
- this._setString(opts, 'iast.cookieFilterPattern', options.iast?.cookieFilterPattern)
1067
1059
  this._setValue(opts, 'iast.dbRowsToTaint', maybeInt(options.iast?.dbRowsToTaint))
1068
1060
  this._setBoolean(opts, 'iast.deduplicationEnabled', options.iast && options.iast.deduplicationEnabled)
1069
1061
  this._setBoolean(opts, 'iast.enabled',
@@ -13,16 +13,14 @@ const log = require('../log')
13
13
 
14
14
  const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex')
15
15
 
16
- const HIGH_ACCURACY_DISTRIBUTION = 0.0075
17
-
18
16
  class StatsPoint {
19
17
  constructor (hash, parentHash, edgeTags) {
20
18
  this.hash = hash.readBigUInt64BE()
21
19
  this.parentHash = parentHash.readBigUInt64BE()
22
20
  this.edgeTags = edgeTags
23
- this.edgeLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION)
24
- this.pathwayLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION)
25
- this.payloadSize = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION)
21
+ this.edgeLatency = new LogCollapsingLowestDenseDDSketch()
22
+ this.pathwayLatency = new LogCollapsingLowestDenseDDSketch()
23
+ this.payloadSize = new LogCollapsingLowestDenseDDSketch()
26
24
  }
27
25
 
28
26
  addLatencies (checkpoint) {
@@ -308,12 +308,16 @@ class MetricsAggregationClient {
308
308
  }
309
309
 
310
310
  for (const [tag, next] of node.nodes) {
311
- this._captureNode(next, name, tags.concat(tag), fn)
311
+ tags.push(tag)
312
+ this._captureNode(next, name, tags, fn)
313
+ tags.pop()
312
314
  }
313
315
  }
314
316
 
315
- _ensureTree (tree, name, tags, value) {
316
- tags = tags ? [].concat(tags) : []
317
+ _ensureTree (tree, name, tags = [], value) {
318
+ if (!Array.isArray(tags)) {
319
+ tags = [tags]
320
+ }
317
321
 
318
322
  let node = this._ensureNode(tree, name, value)
319
323
 
@@ -331,7 +335,10 @@ class MetricsAggregationClient {
331
335
 
332
336
  if (!node) {
333
337
  node = { nodes: new Map(), touched: false, value }
334
- container.set(key, node)
338
+
339
+ if (typeof key === 'string') {
340
+ container.set(key, node)
341
+ }
335
342
  }
336
343
 
337
344
  return node
@@ -4,9 +4,9 @@ const log = require('../log')
4
4
  const { PROPAGATED_PARENT_ID_KEY } = require('./constants/tags')
5
5
  const { storage } = require('./storage')
6
6
 
7
+ const telemetry = require('./telemetry')
7
8
  const LLMObsSpanProcessor = require('./span_processor')
8
9
 
9
- const telemetry = require('./telemetry')
10
10
  const { channel } = require('dc-polyfill')
11
11
  const spanProcessCh = channel('dd-trace:span:process')
12
12
  const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -94,12 +94,15 @@ function handleLLMObsParentIdInjection ({ carrier }) {
94
94
  }
95
95
 
96
96
  function handleFlush () {
97
+ let err = ''
97
98
  try {
98
99
  spanWriter.flush()
99
100
  evalWriter.flush()
100
101
  } catch (e) {
102
+ err = 'writer_flush_error'
101
103
  log.warn(`Failed to flush LLMObs spans and evaluation metrics: ${e.message}`)
102
104
  }
105
+ telemetry.recordUserFlush(err)
103
106
  }
104
107
 
105
108
  function handleSpanProcess (data) {
@@ -201,7 +201,7 @@ class LLMObs extends NoopLLMObs {
201
201
  return this._tracer.wrap(name, spanOptions, wrapped)
202
202
  }
203
203
 
204
- annotate (span, options) {
204
+ annotate (span, options, autoinstrumented = false) {
205
205
  if (!this.enabled) return
206
206
 
207
207
  if (!span) {
@@ -213,150 +213,184 @@ class LLMObs extends NoopLLMObs {
213
213
  span = this._active()
214
214
  }
215
215
 
216
- if (!span) {
217
- throw new Error('No span provided and no active LLMObs-generated span found')
218
- }
219
- if (!options) {
220
- throw new Error('No options provided for annotation.')
221
- }
216
+ let err = ''
222
217
 
223
- if (!LLMObsTagger.tagMap.has(span)) {
224
- throw new Error('Span must be an LLMObs-generated span')
225
- }
226
- if (span._duration !== undefined) {
227
- throw new Error('Cannot annotate a finished span')
228
- }
218
+ try {
219
+ if (!span) {
220
+ err = 'invalid_span_no_active_spans'
221
+ throw new Error('No span provided and no active LLMObs-generated span found')
222
+ }
223
+ if (!options) {
224
+ err = 'invalid_options'
225
+ throw new Error('No options provided for annotation.')
226
+ }
229
227
 
230
- const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
231
- if (!spanKind) {
232
- throw new Error('LLMObs span must have a span kind specified')
233
- }
228
+ if (!LLMObsTagger.tagMap.has(span)) {
229
+ err = 'invalid_span_type'
230
+ throw new Error('Span must be an LLMObs-generated span')
231
+ }
232
+ if (span._duration !== undefined) {
233
+ err = 'invalid_finished_span'
234
+ throw new Error('Cannot annotate a finished span')
235
+ }
234
236
 
235
- const { inputData, outputData, metadata, metrics, tags } = options
236
-
237
- if (inputData || outputData) {
238
- if (spanKind === 'llm') {
239
- this._tagger.tagLLMIO(span, inputData, outputData)
240
- } else if (spanKind === 'embedding') {
241
- this._tagger.tagEmbeddingIO(span, inputData, outputData)
242
- } else if (spanKind === 'retrieval') {
243
- this._tagger.tagRetrievalIO(span, inputData, outputData)
244
- } else {
245
- this._tagger.tagTextIO(span, inputData, outputData)
237
+ const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
238
+ if (!spanKind) {
239
+ err = 'invalid_no_span_kind'
240
+ throw new Error('LLMObs span must have a span kind specified')
246
241
  }
247
- }
248
242
 
249
- if (metadata) {
250
- this._tagger.tagMetadata(span, metadata)
251
- }
243
+ const { inputData, outputData, metadata, metrics, tags } = options
252
244
 
253
- if (metrics) {
254
- this._tagger.tagMetrics(span, metrics)
255
- }
245
+ if (inputData || outputData) {
246
+ if (spanKind === 'llm') {
247
+ this._tagger.tagLLMIO(span, inputData, outputData)
248
+ } else if (spanKind === 'embedding') {
249
+ this._tagger.tagEmbeddingIO(span, inputData, outputData)
250
+ } else if (spanKind === 'retrieval') {
251
+ this._tagger.tagRetrievalIO(span, inputData, outputData)
252
+ } else {
253
+ this._tagger.tagTextIO(span, inputData, outputData)
254
+ }
255
+ }
256
256
 
257
- if (tags) {
258
- this._tagger.tagSpanTags(span, tags)
257
+ if (metadata) {
258
+ this._tagger.tagMetadata(span, metadata)
259
+ }
260
+ if (metrics) {
261
+ this._tagger.tagMetrics(span, metrics)
262
+ }
263
+ if (tags) {
264
+ this._tagger.tagSpanTags(span, tags)
265
+ }
266
+ } catch (e) {
267
+ if (e.ddErrorTag) {
268
+ err = e.ddErrorTag
269
+ }
270
+ throw e
271
+ } finally {
272
+ if (autoinstrumented === false) {
273
+ telemetry.recordLLMObsAnnotate(span, err)
274
+ }
259
275
  }
260
276
  }
261
277
 
262
278
  exportSpan (span) {
263
279
  span = span || this._active()
264
-
265
- if (!span) {
266
- throw new Error('No span provided and no active LLMObs-generated span found')
267
- }
268
-
269
- if (!(span instanceof Span)) {
270
- throw new Error('Span must be a valid Span object.')
271
- }
272
-
273
- if (!LLMObsTagger.tagMap.has(span)) {
274
- throw new Error('Span must be an LLMObs-generated span')
280
+ let err = ''
281
+ try {
282
+ if (!span) {
283
+ err = 'no_active_span'
284
+ throw new Error('No span provided and no active LLMObs-generated span found')
285
+ }
286
+ if (!(span instanceof Span)) {
287
+ err = 'invalid_span'
288
+ throw new Error('Span must be a valid Span object.')
289
+ }
290
+ if (!LLMObsTagger.tagMap.has(span)) {
291
+ err = 'invalid_span'
292
+ throw new Error('Span must be an LLMObs-generated span')
293
+ }
294
+ } catch (e) {
295
+ telemetry.recordExportSpan(span, err)
296
+ throw e
275
297
  }
276
-
277
298
  try {
278
299
  return {
279
300
  traceId: span.context().toTraceId(true),
280
301
  spanId: span.context().toSpanId()
281
302
  }
282
303
  } catch {
283
- logger.warn('Faild to export span. Span must be a valid Span object.')
304
+ err = 'invalid_span'
305
+ logger.warn('Failed to export span. Span must be a valid Span object.')
306
+ } finally {
307
+ telemetry.recordExportSpan(span, err)
284
308
  }
285
309
  }
286
310
 
287
311
  submitEvaluation (llmobsSpanContext, options = {}) {
288
312
  if (!this.enabled) return
289
313
 
314
+ let err = ''
290
315
  const { traceId, spanId } = llmobsSpanContext
291
- if (!traceId || !spanId) {
292
- throw new Error(
293
- 'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
294
- )
295
- }
296
-
297
- const mlApp = options.mlApp || this._config.llmobs.mlApp
298
- if (!mlApp) {
299
- throw new Error(
300
- 'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
301
- )
302
- }
303
-
304
- const timestampMs = options.timestampMs || Date.now()
305
- if (typeof timestampMs !== 'number' || timestampMs < 0) {
306
- throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
307
- }
316
+ try {
317
+ if (!traceId || !spanId) {
318
+ err = 'invalid_span'
319
+ throw new Error(
320
+ 'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
321
+ )
322
+ }
323
+ const mlApp = options.mlApp || this._config.llmobs.mlApp
324
+ if (!mlApp) {
325
+ err = 'missing_ml_app'
326
+ throw new Error(
327
+ 'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
328
+ )
329
+ }
308
330
 
309
- const { label, value, tags } = options
310
- const metricType = options.metricType?.toLowerCase()
311
- if (!label) {
312
- throw new Error('label must be the specified name of the evaluation metric')
313
- }
314
- if (!metricType || !['categorical', 'score'].includes(metricType)) {
315
- throw new Error('metricType must be one of "categorical" or "score"')
316
- }
331
+ const timestampMs = options.timestampMs || Date.now()
332
+ if (typeof timestampMs !== 'number' || timestampMs < 0) {
333
+ err = 'invalid_timestamp'
334
+ throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
335
+ }
317
336
 
318
- if (metricType === 'categorical' && typeof value !== 'string') {
319
- throw new Error('value must be a string for a categorical metric.')
320
- }
321
- if (metricType === 'score' && typeof value !== 'number') {
322
- throw new Error('value must be a number for a score metric.')
323
- }
337
+ const { label, value, tags } = options
338
+ const metricType = options.metricType?.toLowerCase()
339
+ if (!label) {
340
+ err = 'invalid_metric_label'
341
+ throw new Error('label must be the specified name of the evaluation metric')
342
+ }
343
+ if (!metricType || !['categorical', 'score'].includes(metricType)) {
344
+ err = 'invalid_metric_type'
345
+ throw new Error('metricType must be one of "categorical" or "score"')
346
+ }
347
+ if (metricType === 'categorical' && typeof value !== 'string') {
348
+ err = 'invalid_metric_value'
349
+ throw new Error('value must be a string for a categorical metric.')
350
+ }
351
+ if (metricType === 'score' && typeof value !== 'number') {
352
+ err = 'invalid_metric_value'
353
+ throw new Error('value must be a number for a score metric.')
354
+ }
324
355
 
325
- const evaluationTags = {
326
- 'ddtrace.version': tracerVersion,
327
- ml_app: mlApp
328
- }
356
+ const evaluationTags = {
357
+ 'ddtrace.version': tracerVersion,
358
+ ml_app: mlApp
359
+ }
329
360
 
330
- if (tags) {
331
- for (const key in tags) {
332
- const tag = tags[key]
333
- if (typeof tag === 'string') {
334
- evaluationTags[key] = tag
335
- } else if (typeof tag.toString === 'function') {
336
- evaluationTags[key] = tag.toString()
337
- } else if (tag == null) {
338
- evaluationTags[key] = Object.prototype.toString.call(tag)
339
- } else {
340
- // should be a rare case
341
- // every object in JS has a toString, otherwise every primitive has its own toString
342
- // null and undefined are handled above
343
- throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
361
+ if (tags) {
362
+ for (const key in tags) {
363
+ const tag = tags[key]
364
+ if (typeof tag === 'string') {
365
+ evaluationTags[key] = tag
366
+ } else if (typeof tag.toString === 'function') {
367
+ evaluationTags[key] = tag.toString()
368
+ } else if (tag == null) {
369
+ evaluationTags[key] = Object.prototype.toString.call(tag)
370
+ } else {
371
+ // should be a rare case
372
+ // every object in JS has a toString, otherwise every primitive has its own toString
373
+ // null and undefined are handled above
374
+ err = 'invalid_tags'
375
+ throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
376
+ }
344
377
  }
345
378
  }
346
- }
347
379
 
348
- const payload = {
349
- span_id: spanId,
350
- trace_id: traceId,
351
- label,
352
- metric_type: metricType,
353
- ml_app: mlApp,
354
- [`${metricType}_value`]: value,
355
- timestamp_ms: timestampMs,
356
- tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
380
+ const payload = {
381
+ span_id: spanId,
382
+ trace_id: traceId,
383
+ label,
384
+ metric_type: metricType,
385
+ ml_app: mlApp,
386
+ [`${metricType}_value`]: value,
387
+ timestamp_ms: timestampMs,
388
+ tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
389
+ }
390
+ evalMetricAppendCh.publish(payload)
391
+ } finally {
392
+ telemetry.recordSubmitEvaluation(options, err)
357
393
  }
358
-
359
- evalMetricAppendCh.publish(payload)
360
394
  }
361
395
 
362
396
  flush () {
@@ -375,7 +409,7 @@ class LLMObs extends NoopLLMObs {
375
409
  annotations.outputData = output
376
410
  }
377
411
 
378
- this.annotate(span, annotations)
412
+ this.annotate(span, annotations, true)
379
413
  }
380
414
 
381
415
  _active () {
@@ -135,7 +135,7 @@ class LLMObsTagger {
135
135
  if (typeof value === 'number') {
136
136
  filterdMetrics[processedKey] = value
137
137
  } else {
138
- this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`)
138
+ this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`, 'invalid_metrics')
139
139
  }
140
140
  }
141
141
 
@@ -169,7 +169,7 @@ class LLMObsTagger {
169
169
  this._setTag(span, key, JSON.stringify(data))
170
170
  } catch {
171
171
  const type = key === INPUT_VALUE ? 'input' : 'output'
172
- this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`)
172
+ this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`, 'invalid_io_text')
173
173
  }
174
174
  }
175
175
  }
@@ -187,7 +187,7 @@ class LLMObsTagger {
187
187
  }
188
188
 
189
189
  if (document == null || typeof document !== 'object') {
190
- this._handleFailure('Documents must be a string, object, or list of objects.')
190
+ this._handleFailure('Documents must be a string, object, or list of objects.', 'invalid_embedding_io')
191
191
  return undefined
192
192
  }
193
193
 
@@ -195,7 +195,7 @@ class LLMObsTagger {
195
195
  let validDocument = true
196
196
 
197
197
  if (typeof text !== 'string') {
198
- this._handleFailure('Document text must be a string.')
198
+ this._handleFailure('Document text must be a string.', 'invalid_embedding_io')
199
199
  validDocument = false
200
200
  }
201
201
 
@@ -226,7 +226,7 @@ class LLMObsTagger {
226
226
  }
227
227
 
228
228
  if (message == null || typeof message !== 'object') {
229
- this._handleFailure('Messages must be a string, object, or list of objects')
229
+ this._handleFailure('Messages must be a string, object, or list of objects', 'invalid_io_messages')
230
230
  return undefined
231
231
  }
232
232
 
@@ -237,7 +237,7 @@ class LLMObsTagger {
237
237
  const messageObj = { content }
238
238
 
239
239
  if (typeof content !== 'string') {
240
- this._handleFailure('Message content must be a string.')
240
+ this._handleFailure('Message content must be a string.', 'invalid_io_messages')
241
241
  validMessage = false
242
242
  }
243
243
 
@@ -250,7 +250,7 @@ class LLMObsTagger {
250
250
 
251
251
  const filteredToolCalls = toolCalls.map(toolCall => {
252
252
  if (typeof toolCall !== 'object') {
253
- this._handleFailure('Tool call must be an object.')
253
+ this._handleFailure('Tool call must be an object.', 'invalid_io_messages')
254
254
  return undefined
255
255
  }
256
256
 
@@ -313,11 +313,15 @@ class LLMObsTagger {
313
313
 
314
314
  // any public-facing LLMObs APIs using this tagger should not soft fail
315
315
  // auto-instrumentation should soft fail
316
- _handleFailure (msg) {
316
+ _handleFailure (msg, errorTag) {
317
317
  if (this.softFail) {
318
318
  log.warn(msg)
319
319
  } else {
320
- throw new Error(msg)
320
+ const err = new Error(msg)
321
+ if (errorTag) {
322
+ Object.defineProperty(err, 'ddErrorTag', { get () { return errorTag } })
323
+ }
324
+ throw err
321
325
  }
322
326
  }
323
327
 
@@ -109,11 +109,60 @@ function recordDroppedPayload (numEvents, eventType, error) {
109
109
  llmobsMetrics.count(metricName, tags).inc(numEvents)
110
110
  }
111
111
 
112
+ function recordLLMObsAnnotate (span, err, value = 1) {
113
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
114
+ const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
115
+ const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
116
+
117
+ const tags = {
118
+ error: Number(!!err),
119
+ span_kind: spanKind,
120
+ is_root_span: Number(isRootSpan)
121
+ }
122
+ if (err) tags.error_type = err
123
+ llmobsMetrics.count('annotations', tags).inc(value)
124
+ }
125
+
126
+ function recordUserFlush (err, value = 1) {
127
+ const tags = { error: Number(!!err) }
128
+ if (err) tags.error_type = err
129
+ llmobsMetrics.count('user_flush', tags).inc(value)
130
+ }
131
+
132
+ function recordExportSpan (span, err, value = 1) {
133
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
134
+ const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
135
+ const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
136
+
137
+ const tags = {
138
+ error: Number(!!err),
139
+ span_kind: spanKind,
140
+ is_root_span: Number(isRootSpan)
141
+ }
142
+ if (err) tags.error_type = err
143
+ llmobsMetrics.count('spans_exported', tags).inc(value)
144
+ }
145
+
146
+ function recordSubmitEvaluation (options, err, value = 1) {
147
+ const tags = {
148
+ error: Number(!!err),
149
+ custom_joining_key: 0
150
+ }
151
+ const metricType = options?.metricType?.toLowerCase()
152
+ if (metricType !== 'categorical' && metricType !== 'score') tags.metric_type = 'other'
153
+ if (err) tags.error_type = err
154
+ llmobsMetrics.count('evals_submitted', tags).inc(value)
155
+ }
156
+
112
157
  module.exports = {
113
158
  recordLLMObsEnabled,
114
159
  incrementLLMObsSpanStartCount,
115
160
  incrementLLMObsSpanFinishedCount,
116
161
  recordLLMObsRawSpanSize,
117
162
  recordLLMObsSpanSize,
118
- recordDroppedPayload
163
+ recordDroppedPayload,
164
+ recordLLMObsAnnotate,
165
+ recordUserFlush,
166
+ recordExportSpan,
167
+ recordSubmitEvaluation
119
168
  }
@@ -1694,7 +1694,7 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
1694
1694
  * @param {string} parentPropName
1695
1695
  * @param {JSONPathCallback} callback
1696
1696
  * @param {boolean} hasArrExpr
1697
- * @param {boolean} literalPriority
1697
+ * @param {boolean} [literalPriority]
1698
1698
  * @returns {ReturnObject|ReturnObject[]}
1699
1699
  */
1700
1700
  JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, hasArrExpr, literalPriority) {
@@ -15,7 +15,6 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
15
15
  const { tagger } = require('./tagger')
16
16
  const { isFalse, isTrue } = require('../util')
17
17
  const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
18
- const satisfies = require('semifies')
19
18
 
20
19
  class Config {
21
20
  constructor (options = {}) {
@@ -23,7 +22,6 @@ class Config {
23
22
  DD_AGENT_HOST,
24
23
  DD_ENV,
25
24
  DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
26
- DD_PROFILING_ASYNC_ID_ENABLED,
27
25
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
28
26
  DD_PROFILING_CPU_ENABLED,
29
27
  DD_PROFILING_DEBUG_SOURCE_MAPS,
@@ -181,10 +179,6 @@ class Config {
181
179
  this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
182
180
  DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
183
181
 
184
- // Async ID gathering only works reliably on Node >= 22.10.0
185
- this.asyncIdEnabled = isTrue(coalesce(options.asyncIdEnabled,
186
- DD_PROFILING_ASYNC_ID_ENABLED, this.timelineEnabled && satisfies(process.versions.node, '>=22.10.0')))
187
-
188
182
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
189
183
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
190
184
  DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))
@@ -70,16 +70,12 @@ function ensureChannelsActivated () {
70
70
  class NativeWallProfiler {
71
71
  constructor (options = {}) {
72
72
  this.type = 'wall'
73
- // Currently there's a crash sometimes on worker threads trying to collect async IDs so for the
74
- // time being we'll constrain it to only the main thread.
75
- this._asyncIdEnabled = !!options.asyncIdEnabled && require('worker_threads').isMainThread
73
+ this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
74
+ this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
76
75
  this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
77
- this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
78
76
  this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
79
- this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
80
- this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
81
77
  this._timelineEnabled = !!options.timelineEnabled
82
- this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
78
+ this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
83
79
  // We need to capture span data into the sample context for either code hotspots
84
80
  // or endpoint collection.
85
81
  this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
@@ -88,6 +84,7 @@ class NativeWallProfiler {
88
84
  // timestamps require the sample contexts feature in the pprof wall profiler), or
89
85
  // cpu profiling is enabled.
90
86
  this._withContexts = this._captureSpanData || this._timelineEnabled || this._cpuProfilingEnabled
87
+ this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
91
88
  this._mapper = undefined
92
89
  this._pprof = undefined
93
90
 
@@ -128,14 +125,13 @@ class NativeWallProfiler {
128
125
  }
129
126
 
130
127
  this._pprof.time.start({
131
- collectAsyncId: this._asyncIdEnabled,
132
- collectCpuTime: this._cpuProfilingEnabled,
133
- durationMillis: this._flushIntervalMillis,
134
128
  intervalMicros: this._samplingIntervalMicros,
135
- lineNumbers: false,
129
+ durationMillis: this._flushIntervalMillis,
136
130
  sourceMapper: this._mapper,
137
131
  withContexts: this._withContexts,
138
- workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled
132
+ lineNumbers: false,
133
+ workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
134
+ collectCpuTime: this._cpuProfilingEnabled
139
135
  })
140
136
 
141
137
  if (this._withContexts) {
@@ -23,8 +23,8 @@ class SpanAggStats {
23
23
  this.topLevelHits = 0
24
24
  this.errors = 0
25
25
  this.duration = 0
26
- this.okDistribution = new LogCollapsingLowestDenseDDSketch(0.00775)
27
- this.errorDistribution = new LogCollapsingLowestDenseDDSketch(0.00775)
26
+ this.okDistribution = new LogCollapsingLowestDenseDDSketch()
27
+ this.errorDistribution = new LogCollapsingLowestDenseDDSketch()
28
28
  }
29
29
 
30
30
  record (span) {