dd-trace 5.133.0 → 5.134.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 (55) hide show
  1. package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
  2. package/index.d.ts +16 -5
  3. package/package.json +17 -16
  4. package/packages/datadog-instrumentations/index.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +17 -8
  6. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +27 -8
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
  13. package/packages/datadog-instrumentations/src/jest.js +69 -57
  14. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -2
  15. package/packages/datadog-instrumentations/src/nova-sonic/session.js +25 -16
  16. package/packages/datadog-instrumentations/src/playwright.js +2 -0
  17. package/packages/datadog-instrumentations/src/stripe.js +23 -12
  18. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +7 -3
  20. package/packages/datadog-instrumentations/src/vitest-util.js +5 -2
  21. package/packages/datadog-instrumentations/src/vitest.js +8 -0
  22. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -0
  23. package/packages/datadog-plugin-cypress/src/support.js +2 -0
  24. package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
  25. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
  26. package/packages/dd-trace/src/appsec/stack_trace.js +3 -2
  27. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
  28. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  29. package/packages/dd-trace/src/config/index.js +45 -0
  30. package/packages/dd-trace/src/config/supported-configurations.json +9 -0
  31. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
  32. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
  33. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
  34. package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
  35. package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
  36. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  37. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
  38. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +9 -1
  39. package/packages/dd-trace/src/llmobs/sdk.js +15 -4
  40. package/packages/dd-trace/src/llmobs/span_processor.js +12 -0
  41. package/packages/dd-trace/src/llmobs/tagger.js +18 -8
  42. package/packages/dd-trace/src/llmobs/util.js +10 -4
  43. package/packages/dd-trace/src/openfeature/index.js +6 -0
  44. package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
  45. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +18 -1
  46. package/packages/dd-trace/src/plugin_manager.js +1 -2
  47. package/packages/dd-trace/src/plugins/tracing.js +0 -1
  48. package/packages/dd-trace/src/plugins/util/git.js +91 -35
  49. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
  50. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +90 -20
  52. package/packages/dd-trace/src/span_stats.js +4 -1
  53. package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
  54. package/packages/dd-trace/src/util.js +8 -4
  55. package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
@@ -1,10 +1,24 @@
1
1
  'use strict'
2
2
 
3
- const { HTTP_CLIENT_IP, HTTP_USERAGENT, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
3
+ const {
4
+ ERROR,
5
+ HTTP_CLIENT_IP,
6
+ HTTP_USERAGENT,
7
+ MANUAL_DROP,
8
+ MANUAL_KEEP,
9
+ NETWORK_CLIENT_IP,
10
+ RESOURCE_NAME,
11
+ SAMPLING_PRIORITY,
12
+ SERVICE_NAME,
13
+ SPAN_KIND,
14
+ SPAN_TYPE,
15
+ } = require('../../../../ext/tags')
4
16
  const createRfdc = require('../../../../vendor/dist/rfdc')
5
17
  const clone = createRfdc({ proto: false, circles: false })
6
18
  const { USER_ID, USER_SESSION_ID } = require('../appsec/addresses')
7
19
  const { getActiveRequest } = require('../appsec/store')
20
+ const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE, PROCESS_ID, SAMPLING_PRIORITY_KEY } = require('../constants')
21
+ const log = require('../log')
8
22
  const { keepTrace } = require('../priority_sampler')
9
23
  const { extractIp } = require('../plugins/util/ip_extractor')
10
24
  const { AI_GUARD } = require('../standalone/product')
@@ -20,6 +34,12 @@ const ALLOW = 'ALLOW'
20
34
  /** @typedef {import('../../../../index').aiguard.RedactionReplacement} RedactionReplacement */
21
35
  /** @typedef {import('../opentracing/span')} Span */
22
36
 
37
+ /**
38
+ * @typedef {object} ExtraEntry
39
+ * @property {string} key
40
+ * @property {unknown} value
41
+ */
42
+
23
43
  /**
24
44
  * @typedef {object} EvaluationResponse
25
45
  * @property {string} action
@@ -30,6 +50,8 @@ const ALLOW = 'ALLOW'
30
50
  * @property {boolean} hasTagProbabilities
31
51
  * @property {boolean} blockingEnabled
32
52
  * @property {unknown} redactionReplacements
53
+ * @property {ExtraEntry[]} extraTags
54
+ * @property {ExtraEntry[]} extraMetastruct
33
55
  */
34
56
 
35
57
  /**
@@ -40,6 +62,8 @@ const ALLOW = 'ALLOW'
40
62
  * @property {boolean} shouldBlock
41
63
  * @property {boolean} hasTagProbabilities
42
64
  * @property {{ enabled: boolean, applied: boolean, failures: number }} redaction
65
+ * @property {ExtraEntry[]} extraTags
66
+ * @property {ExtraEntry[]} extraMetastruct
43
67
  */
44
68
 
45
69
  /**
@@ -68,6 +92,38 @@ function isRecord (value) {
68
92
  return value !== null && typeof value === 'object' && !Array.isArray(value)
69
93
  }
70
94
 
95
+ // Shared by every evaluation without extra entries, so the common case allocates nothing.
96
+ const NO_EXTRA_ENTRIES = /** @type {ExtraEntry[]} */ (/** @type {unknown} */ (Object.freeze([])))
97
+
98
+ /**
99
+ * Normalizes a backend extra field into well-formed key/value entries. The backend sends lists of
100
+ * entries rather than objects because the dotted keys are not valid JSON:API member names. Values
101
+ * are kept as-is: which of them reach the span is decided when they are applied.
102
+ *
103
+ * @param {string} field
104
+ * @param {unknown} entries
105
+ * @returns {ExtraEntry[]}
106
+ */
107
+ function normalizeExtraEntries (field, entries) {
108
+ if (entries === undefined) return NO_EXTRA_ENTRIES
109
+ if (!Array.isArray(entries)) {
110
+ log.debug('AI Guard: ignoring %s of type %s', field, entries === null ? 'null' : typeof entries)
111
+ return NO_EXTRA_ENTRIES
112
+ }
113
+
114
+ const normalized = []
115
+ for (const entry of entries) {
116
+ const key = isRecord(entry) ? entry.key : undefined
117
+ // A __proto__ key is never valid: it cannot become an own property on the span tags or the meta-struct.
118
+ if (typeof key === 'string' && key.length > 0 && key !== '__proto__') {
119
+ normalized.push({ key, value: entry.value })
120
+ } else {
121
+ log.debug('AI Guard: ignoring malformed %s entry', field)
122
+ }
123
+ }
124
+ return normalized.length === 0 ? NO_EXTRA_ENTRIES : normalized
125
+ }
126
+
71
127
  /**
72
128
  * Parses the backend evaluation response into the tracer's internal contract.
73
129
  *
@@ -89,6 +145,9 @@ function parseEvaluationResponse (body) {
89
145
  hasTagProbabilities: isRecord(attributes.tag_probs),
90
146
  blockingEnabled: attributes.is_blocking_enabled === true,
91
147
  redactionReplacements: attributes.redaction_replacements,
148
+ // Malformed extra entries are dropped here instead of turning the response into a bad_response.
149
+ extraTags: normalizeExtraEntries('extra_tags', attributes.extra_tags),
150
+ extraMetastruct: normalizeExtraEntries('extra_metastruct', attributes.extra_metastruct),
92
151
  }
93
152
  }
94
153
 
@@ -132,6 +191,8 @@ function createEvaluationOutcome (messages, evaluation, options) {
132
191
  applied: redaction.redacted,
133
192
  failures: redaction.failures,
134
193
  },
194
+ extraTags: evaluation.extraTags,
195
+ extraMetastruct: evaluation.extraMetastruct,
135
196
  }
136
197
  }
137
198
 
@@ -147,6 +208,86 @@ const SERVICE_ENTRY_TAG_MAPPINGS = [
147
208
  [USER_SESSION_ID, TAGS.USR_SESSION_ID_TAG_KEY],
148
209
  ]
149
210
 
211
+ // Backend extra tags never override what the tracer computes itself, nor keys that control the
212
+ // span or its trace (sampling, service, resource, error state). Internal _dd keys are covered by
213
+ // isReservedExtraTag instead of being listed here.
214
+ const RESERVED_EXTRA_TAGS = new Set([
215
+ TAGS.ACTION_TAG_KEY,
216
+ TAGS.REASON_TAG_KEY,
217
+ TAGS.TARGET_TAG_KEY,
218
+ TAGS.TOOL_NAME_TAG_KEY,
219
+ TAGS.BLOCKED_TAG_KEY,
220
+ TAGS.REDACTED_TAG_KEY,
221
+ TAGS.EVENT_TAG_KEY,
222
+ ...SERVICE_ENTRY_TAG_MAPPINGS.map(([, destinationTag]) => destinationTag),
223
+ MANUAL_KEEP,
224
+ MANUAL_DROP,
225
+ SAMPLING_PRIORITY,
226
+ SERVICE_NAME,
227
+ RESOURCE_NAME,
228
+ SPAN_TYPE,
229
+ SPAN_KIND,
230
+ ERROR,
231
+ ERROR_TYPE,
232
+ ERROR_MESSAGE,
233
+ ERROR_STACK,
234
+ SAMPLING_PRIORITY_KEY,
235
+ PROCESS_ID,
236
+ 'analytics.event',
237
+ 'language',
238
+ 'runtime-id',
239
+ 'service.version',
240
+ 'version',
241
+ 'env',
242
+ ])
243
+
244
+ /**
245
+ * Returns whether a backend extra tag key must not be set on the span.
246
+ *
247
+ * @param {string} key
248
+ */
249
+ function isReservedExtraTag (key) {
250
+ // _dd is the tracer's internal namespace (_dd.*, _dd1.*): the span formatter writes sampling,
251
+ // top-level and propagation keys there and a span tag with the same key would overwrite them.
252
+ // This is not an AI Guard namespace filter; the backend owns ai_guard.* keys.
253
+ return key.startsWith('_dd') || RESERVED_EXTRA_TAGS.has(key)
254
+ }
255
+
256
+ const RESERVED_EXTRA_META_STRUCT_KEYS = new Set(['messages', 'attack_categories', 'sds', 'tag_probs'])
257
+
258
+ /**
259
+ * Copies backend-provided extra_tags to the span and extra_metastruct to the AI Guard meta-struct,
260
+ * skipping values and keys the span must not receive. Keys are used verbatim and entries are
261
+ * applied in order, so a repeated key keeps its last value.
262
+ *
263
+ * @param {Span} span
264
+ * @param {EvaluationMetaStruct} metaStruct
265
+ * @param {ExtraEntry[]} extraTags
266
+ * @param {ExtraEntry[]} extraMetastruct
267
+ */
268
+ function applyExtraSpanData (span, metaStruct, extraTags, extraMetastruct) {
269
+ // The client decodes values with JSON.parse, so a string check and a null check cover every JSON type.
270
+ for (const { key, value } of extraTags) {
271
+ if (typeof value !== 'string') {
272
+ log.debug('AI Guard: ignoring extra_tags entry %s with non-string value', key)
273
+ } else if (isReservedExtraTag(key)) {
274
+ log.debug('AI Guard: ignoring reserved extra tag %s', key)
275
+ } else {
276
+ span.setTag(key, value)
277
+ }
278
+ }
279
+
280
+ for (const { key, value } of extraMetastruct) {
281
+ if (value === undefined || value === null) {
282
+ log.debug('AI Guard: ignoring extra_metastruct entry %s with null value', key)
283
+ } else if (RESERVED_EXTRA_META_STRUCT_KEYS.has(key)) {
284
+ log.debug('AI Guard: ignoring reserved extra_metastruct key %s', key)
285
+ } else {
286
+ metaStruct[key] = value
287
+ }
288
+ }
289
+ }
290
+
150
291
  class EvaluationReporter {
151
292
  #config
152
293
  #maxMessagesLength
@@ -230,6 +371,7 @@ class EvaluationReporter {
230
371
  if (result.tags.length > 0) report.metaStruct.attack_categories = result.tags
231
372
  if (result.sds.length > 0) report.metaStruct.sds = result.sds
232
373
  if (outcome.hasTagProbabilities) report.metaStruct.tag_probs = result.tagProbabilities
374
+ applyExtraSpanData(report.span, report.metaStruct, outcome.extraTags, outcome.extraMetastruct)
233
375
 
234
376
  if (redaction.enabled) {
235
377
  report.span.setTag(TAGS.REDACTED_TAG_KEY, redaction.applied ? 'true' : 'false')
@@ -66,7 +66,8 @@ function getCallSiteFramesForLocation (callSiteFrames, externallyExcludedPaths)
66
66
 
67
67
  if (
68
68
  !isExcluded(callsite, externallyExcludedPaths) &&
69
- (!filepath.includes(pathLine.ddBasePath) || globalThis.__DD_ESBUILD_IAST_WITH_NO_SM)
69
+ (pathLine.ddBasePath === undefined || !filepath.includes(pathLine.ddBasePath) ||
70
+ globalThis.__DD_ESBUILD_IAST_WITH_NO_SM)
70
71
  ) {
71
72
  clientFrames.push(callsite)
72
73
  }
@@ -34,8 +34,9 @@ function getCallSiteList (maxDepth = 100, constructorOpt) {
34
34
 
35
35
  function filterOutFramesFromLibrary (callSiteList) {
36
36
  return callSiteList.filter(callSite => {
37
- if (globalThis.__DD_ESBUILD_IAST_WITH_NO_SM) {
38
- // bundled and no SourceMap, not possible to discriminate if the frame comes from dd-trace code or not
37
+ if (globalThis.__DD_ESBUILD_IAST_WITH_NO_SM || ddBasePath === undefined) {
38
+ // bundled without a SourceMap, or dd-trace's location is unknown: not possible to discriminate if the frame
39
+ // comes from dd-trace code or not
39
40
  return true
40
41
  }
41
42
 
@@ -228,9 +228,10 @@ async function addBreakpoint (probe) {
228
228
  }
229
229
  }
230
230
 
231
- function start () {
231
+ async function start () {
232
+ await session.post('Debugger.enable')
233
+ // Only flag the session as started once enabled, so the next probe retries a failed start and reports its cause
232
234
  sessionStarted = true
233
- return session.post('Debugger.enable') // return instead of await to reduce number of promises created
234
235
  }
235
236
 
236
237
  function drainBreakpointHitRequests () {
@@ -439,6 +439,7 @@ export interface GeneratedConfig {
439
439
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
440
440
  DD_FEATURE_FLAGS_ENABLED: boolean;
441
441
  DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED: boolean;
442
+ DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED: boolean;
442
443
  DD_FLAGGING_EVALUATION_COUNTS_ENABLED: boolean;
443
444
  };
444
445
  flushInterval: number;
@@ -726,6 +727,7 @@ export interface GeneratedEnvVarConfig {
726
727
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
727
728
  DD_FEATURE_FLAGS_ENABLED: boolean;
728
729
  DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED: boolean;
730
+ DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED: boolean;
729
731
  DD_FLAGGING_EVALUATION_COUNTS_ENABLED: boolean;
730
732
  DD_GIT_BRANCH: string | undefined;
731
733
  DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
@@ -12,6 +12,7 @@ const uuid = require('../../../../vendor/dist/crypto-randomuuid') // we need to
12
12
  const set = require('../../../datadog-core/src/utils/src/set')
13
13
  const { DD_MAJOR, NODE_MAJOR } = require('../../../../version')
14
14
  const log = require('../log')
15
+ const { warn } = require('../log/writer')
15
16
  const pkg = require('../pkg')
16
17
  const { isTrue } = require('../util')
17
18
  const telemetry = require('../telemetry')
@@ -25,6 +26,7 @@ const {
25
26
  const { ORIGIN_KEY, DATADOG_MINI_AGENT_PATH } = require('../constants')
26
27
  const { appendRules } = require('../payload-tagging/config')
27
28
  const { createSiteUrl } = require('../exporters/common/url')
29
+ const { getUnsupportedRuntime } = require('../debugger/unsupported-runtime')
28
30
  const ConfigBase = require('./config-base')
29
31
  const {
30
32
  getEnvironmentVariable,
@@ -200,6 +202,12 @@ class Config extends ConfigBase {
200
202
  */
201
203
  #parsedDdTags
202
204
 
205
+ /**
206
+ * Options already explained as deactivated, so recalculating the config doesn't repeat the explanation
207
+ * @type {Set<ConfigPath>}
208
+ */
209
+ #explainedDeactivations = new Set()
210
+
203
211
  /**
204
212
  * @type {Record<string, string>}
205
213
  */
@@ -639,6 +647,12 @@ class Config extends ConfigBase {
639
647
  setAndTrack(this, 'remoteConfig.DD_REMOTE_CONFIGURATION_ENABLED', false)
640
648
  }
641
649
 
650
+ if (this.isCiVisibility) {
651
+ // Test Optimization reports its metrics through instrumentation telemetry, not DogStatsD.
652
+ set(this, 'dogstatsd', undefined)
653
+ setAndTrack(this, 'runtimeMetrics.enabled', false)
654
+ }
655
+
642
656
  const isTestOptimizationWorker = this.isCiVisibility &&
643
657
  TEST_OPTIMIZATION_WORKER_EXPORTERS.has(this.tracing.DD_TRACE_EXPERIMENTAL_EXPORTER)
644
658
  if (isTestOptimizationWorker) {
@@ -750,11 +764,42 @@ class Config extends ConfigBase {
750
764
  deactivateIfEnabledAndWarnOnWindows(this, 'DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED')
751
765
  }
752
766
 
767
+ const unsupportedRuntime = getUnsupportedRuntime()
768
+ if (unsupportedRuntime !== undefined) {
769
+ this.#deactivateOnUnsupportedRuntime(
770
+ 'dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_ENABLED',
771
+ `Live Debugger/Dynamic Instrumentation is disabled, because it is not supported on ${unsupportedRuntime}.`
772
+ )
773
+ this.#deactivateOnUnsupportedRuntime(
774
+ 'testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED',
775
+ 'Failed Test Replay is disabled, ' +
776
+ `because Live Debugger/Dynamic Instrumentation is not supported on ${unsupportedRuntime}.`
777
+ )
778
+ }
779
+
753
780
  // Single tags update is tracked as a calculated value.
754
781
  setAndTrack(this, 'tags', this.tags)
755
782
 
756
783
  telemetry.updateConfig([...configWithOrigin.values()], this)
757
784
  }
785
+
786
+ /**
787
+ * Only explains the deactivation of an option that was explicitly set, since Failed Test Replay is enabled by
788
+ * default.
789
+ *
790
+ * @param {ConfigPath} name
791
+ * @param {string} reason
792
+ */
793
+ #deactivateOnUnsupportedRuntime (name, reason) {
794
+ if (!get(this, name)) return
795
+
796
+ if (this.startupLogs && trackedConfigOrigins.has(name) && !this.#explainedDeactivations.has(name)) {
797
+ this.#explainedDeactivations.add(name)
798
+ // `log.warn` only prints with debug logging enabled, so write the diagnostic like the startup logs do
799
+ warn(`DATADOG TRACER DIAGNOSTIC - ${reason}`)
800
+ }
801
+ setAndTrack(this, name, false)
802
+ }
758
803
  }
759
804
 
760
805
  /**
@@ -1004,6 +1004,15 @@
1004
1004
  "namespace": "featureFlags"
1005
1005
  }
1006
1006
  ],
1007
+ "DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED": [
1008
+ {
1009
+ "implementation": "A",
1010
+ "type": "boolean",
1011
+ "default": "true",
1012
+ "description": "Enable Feature Flagging exposure delivery to Datadog independently of flag evaluation and evaluation telemetry.",
1013
+ "namespace": "featureFlags"
1014
+ }
1015
+ ],
1007
1016
  "DD_FLAGGING_EVALUATION_COUNTS_ENABLED": [
1008
1017
  {
1009
1018
  "implementation": "A",
@@ -103,14 +103,14 @@ async function addBreakpoint (probe) {
103
103
  file, lineNumber, columnNumber, probe.id, probe.version
104
104
  )
105
105
  const position = await getGeneratedPosition(url, source, lineNumber, sourceMapURL)
106
- if (position.line !== null && position.column !== null) {
107
- lineNumber = position.line
108
- columnNumber = position.column
109
- } else {
106
+ if (position.line === null || position.column === null) {
110
107
  throw new Error(
111
108
  // eslint-disable-next-line @stylistic/max-len
112
109
  `Could not find generated position for ${url}:${lineNumber}:${columnNumber} (probe: ${probe.id}, version: ${probe.version})`
113
110
  )
111
+ } else {
112
+ lineNumber = position.line
113
+ columnNumber = position.column
114
114
  }
115
115
  }
116
116
 
@@ -357,9 +357,10 @@ async function reEvaluateProbe (probe) {
357
357
  }
358
358
 
359
359
  async function start () {
360
- sessionStarted = true
361
360
  log.debug('[debugger:devtools_client] Starting debugger')
362
361
  await session.post('Debugger.enable')
362
+ // Only flag the session as started once enabled, so the next probe retries a failed start and reports its cause
363
+ sessionStarted = true
363
364
 
364
365
  // Wait until there's a pause in script-loading to avoid accidentally adding probes to incorrect scripts. This is not
365
366
  // a guarantee, but best effort.
@@ -312,9 +312,9 @@ function isInstanceOfCoreType (type, variable, fallback = `${variable} instanceo
312
312
  function getSize (variable) {
313
313
  return `((val) => {
314
314
  if (${isString('val')} || ${isArrayOrTypedArray('val')}) {
315
- return ${guardAgainstPropertyAccessSideEffects('val', '"length"')}
315
+ return ${guardAgainstPropertyAccessSideEffects('"length"')}
316
316
  } else if (${isInstanceOfCoreType('Set', 'val')} || ${isInstanceOfCoreType('Map', 'val')}) {
317
- return ${guardAgainstPropertyAccessSideEffects('val', '"size"')}
317
+ return ${guardAgainstPropertyAccessSideEffects('"size"')}
318
318
  } else if (${isInstanceOfCoreType('WeakSet', 'val')} || ${isInstanceOfCoreType('WeakMap', 'val')}) {
319
319
  throw new TypeError('Cannot get size of WeakSet or WeakMap')
320
320
  } else if (typeof val === 'object' && val !== null) {
@@ -338,12 +338,12 @@ function accessProperty (variable, keyOrIndex, allowMapAccess) {
338
338
  } else if (${isInstanceOfCoreType('Set', 'val')} || ${isInstanceOfCoreType('WeakSet', 'val')}) {
339
339
  throw new Error('Accessing a Set or WeakSet is not allowed')
340
340
  } else {
341
- return ${guardAgainstPropertyAccessSideEffects('val', 'key')}
341
+ return ${guardAgainstPropertyAccessSideEffects('key')}
342
342
  }
343
343
  })(${variable}, ${keyOrIndex})`
344
344
  }
345
345
 
346
- function guardAgainstPropertyAccessSideEffects (variable, propertyName) {
346
+ function guardAgainstPropertyAccessSideEffects (propertyName) {
347
347
  return `((val, key) => {
348
348
  if (
349
349
  ${isInstanceOfCoreType('Proxy', 'val', 'true')} ||
@@ -353,7 +353,7 @@ function guardAgainstPropertyAccessSideEffects (variable, propertyName) {
353
353
  } else {
354
354
  return val[key]
355
355
  }
356
- })(${variable}, ${propertyName})`
356
+ })(val, ${propertyName})`
357
357
  }
358
358
 
359
359
  function guardAgainstCoercionSideEffects (variable) {
@@ -126,8 +126,8 @@ async function traverseGetPropertiesResult (props, opts, depth) {
126
126
  if (prop.value === undefined) continue
127
127
  const { value: { type, objectId, subtype, description } } = prop
128
128
  if (type === 'object') {
129
- if (objectId === undefined) continue // if `subtype` is "null"
130
- if (LEAF_SUBTYPES.has(subtype)) continue // don't waste time with these subtypes
129
+ // if `subtype` is "null"
130
+ if (objectId === undefined || LEAF_SUBTYPES.has(subtype)) continue // don't waste time with these subtypes
131
131
  const size = parseLengthFromDescription(description, subtype)
132
132
  if (size !== null && size >= LARGE_OBJECT_SKIP_THRESHOLD) {
133
133
  const empty = []
@@ -310,8 +310,7 @@ function removeNonEnumerableProperties (props) {
310
310
  }
311
311
 
312
312
  function parseLengthFromDescription (description, subtype) {
313
- if (typeof description !== 'string') return null
314
- if (!SIZE_IN_DESCRIPTION_SUBTYPES.has(subtype)) return null
313
+ if (typeof description !== 'string' || !SIZE_IN_DESCRIPTION_SUBTYPES.has(subtype)) return null
315
314
 
316
315
  const open = description.lastIndexOf('(')
317
316
  if (open === -1) return null
@@ -323,8 +322,7 @@ function parseLengthFromDescription (description, subtype) {
323
322
  if (s === '') return null
324
323
 
325
324
  const n = Number(s)
326
- if (!Number.isSafeInteger(n) || n < 0) return null
327
- if (String(n) !== s) return null
325
+ if (!Number.isSafeInteger(n) || n < 0 || String(n) !== s) return null
328
326
 
329
327
  return n
330
328
  }
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Get the name of the runtime if it can't run Live Debugger/Dynamic Instrumentation, or `undefined` if it can.
5
+ *
6
+ * Live Debugger/Dynamic Instrumentation needs the Debugger domain of the `node:inspector` module. Bun only implements a
7
+ * few of its methods, never returns their results, and never emits its events. Deno doesn't let worker threads
8
+ * connect to the main thread inspector. Deno reports a Node.js `process.version`, so both runtimes are detected by
9
+ * their own `process.versions` entry instead.
10
+ */
11
+ function getUnsupportedRuntime () {
12
+ if (process.versions.bun !== undefined) return 'Bun'
13
+ if (process.versions.deno !== undefined) return 'Deno'
14
+ }
15
+
16
+ module.exports = { getUnsupportedRuntime }
@@ -35,7 +35,7 @@ const LABEL_FIELDS = ['name', 'instructions', 'model']
35
35
  * model_settings?: Record<string, unknown>,
36
36
  * tools?: AgentTool[],
37
37
  * }} AgentManifestFields
38
- * @typedef {{ manifest: AgentManifestFields }} AgentDeclaration
38
+ * @typedef {{ version?: string, manifest?: AgentManifestFields }} AgentDeclaration
39
39
  */
40
40
 
41
41
  /**
@@ -50,8 +50,9 @@ function buildAgentDeclaration (agent) {
50
50
  let declaration
51
51
  try {
52
52
  if (isPlainObject(agent)) {
53
+ const version = toAgentVersion(agent.version)
53
54
  const manifest = buildAgentManifest(agent)
54
- if (manifest !== undefined) declaration = { manifest }
55
+ if (version !== undefined || manifest !== undefined) declaration = { version, manifest }
55
56
  } else {
56
57
  log.warn('Dropping agent annotation, the agent must be a plain object.')
57
58
  }
@@ -105,6 +106,21 @@ function buildAgentManifest (agent) {
105
106
  return manifest
106
107
  }
107
108
 
109
+ /**
110
+ * Unset values (`undefined`, `null`, `''`) are ignored without a warning, so they leave an earlier version in place.
111
+ * A comma is rejected because intake splits tags on it.
112
+ *
113
+ * @param {unknown} version
114
+ */
115
+ function toAgentVersion (version) {
116
+ if (typeof version === 'string' && version !== '') {
117
+ if (!version.includes(',')) return version
118
+ log.warn('Dropping the agent version, it must not contain a comma.')
119
+ return
120
+ }
121
+ if (!isUnset(version)) log.warn('Dropping the agent version, it must be a string.')
122
+ }
123
+
108
124
  /**
109
125
  * @param {unknown} settings
110
126
  * @param {string[]} dropped
@@ -293,4 +309,5 @@ function isScalar (value) {
293
309
  module.exports = {
294
310
  MANUAL_FRAMEWORK_NAME,
295
311
  buildAgentDeclaration,
312
+ toAgentVersion,
296
313
  }
@@ -23,6 +23,8 @@ module.exports = {
23
23
  METADATA: '_ml_obs.meta.metadata',
24
24
  COST_TAGS: '_ml_obs.meta.metadata._dd.cost_tags',
25
25
  AGENT_MANIFEST: '_ml_obs.meta.metadata._dd.agent_manifest',
26
+ AGENT_VERSION: '_ml_obs.agent_version',
27
+ AGENT_VERSION_TAG_KEY: 'agent_version',
26
28
  METRICS: '_ml_obs.metrics',
27
29
  TOOL_DEFINITIONS: '_ml_obs.meta.tool_definitions',
28
30
  ML_APP: '_ml_obs.meta.ml_app',
@@ -33,6 +35,7 @@ module.exports = {
33
35
 
34
36
  PARENT_AGENT_NAME: '_ml_obs.parent_agent_name',
35
37
  PARENT_AGENT_SPAN_ID: '_ml_obs.parent_agent_span_id',
38
+ PARENT_AGENT_VERSION: '_ml_obs.parent_agent_version',
36
39
  PROPAGATED_PARENT_AGENT_ID_KEY: '_dd.p.llmobs_pagent_span_id',
37
40
  PROPAGATED_PARENT_AGENT_NAME_KEY: '_dd.p.llmobs_pagent_name',
38
41
  PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
@@ -12,6 +12,9 @@ const AUDIO_MIME_TYPES = {
12
12
  mp3: 'audio/mpeg',
13
13
  }
14
14
 
15
+ const DECISION_PROVIDER_TAG = 'decision_provider'
16
+ const DECISION_PROVIDER = 'openai'
17
+
15
18
  module.exports = {
16
19
  INPUT_TYPE_IMAGE,
17
20
  INPUT_TYPE_FILE,
@@ -19,4 +22,6 @@ module.exports = {
19
22
  IMAGE_FALLBACK,
20
23
  FILE_FALLBACK,
21
24
  AUDIO_MIME_TYPES,
25
+ DECISION_PROVIDER_TAG,
26
+ DECISION_PROVIDER,
22
27
  }
@@ -8,7 +8,13 @@ const {
8
8
  } = require('../../constants/tags')
9
9
  const { audioMimeTypeFromFormat, formatAudioPart } = require('../../audio-utils')
10
10
  const { safeJsonParse } = require('../../util')
11
- const { AUDIO_MIME_TYPES, IMAGE_FALLBACK, INPUT_TYPE_IMAGE } = require('./constants')
11
+ const {
12
+ AUDIO_MIME_TYPES,
13
+ DECISION_PROVIDER,
14
+ DECISION_PROVIDER_TAG,
15
+ IMAGE_FALLBACK,
16
+ INPUT_TYPE_IMAGE,
17
+ } = require('./constants')
12
18
  const {
13
19
  extractChatTemplateFromInstructions,
14
20
  normalizePromptVariables,
@@ -479,6 +485,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
479
485
  }
480
486
 
481
487
  #tagDecision (span, inputs, response, error) {
488
+ this._tagger.tagSpanTags(span, { [DECISION_PROVIDER_TAG]: DECISION_PROVIDER })
489
+
482
490
  const { input, questions } = inputs
483
491
 
484
492
  const inputMessages = typeof input === 'string'
@@ -29,7 +29,7 @@ const {
29
29
  getFunctionArguments,
30
30
  validateKind,
31
31
  } = require('./util')
32
- const { buildAgentDeclaration } = require('./agent-manifest')
32
+ const { buildAgentDeclaration, toAgentVersion } = require('./agent-manifest')
33
33
  /** @typedef {import('./agent-manifest').AgentDeclaration} AgentDeclaration */
34
34
  const { storage } = require('./storage')
35
35
  const telemetry = require('./telemetry')
@@ -163,7 +163,7 @@ class LLMObs extends NoopLLMObs {
163
163
  const {
164
164
  spanOptions,
165
165
  ...llmobsOptions
166
- } = this.#extractOptions(options)
166
+ } = this.#extractOptions(options, kind)
167
167
 
168
168
  if (fn.length > 1) {
169
169
  return this._tracer.trace(name, spanOptions, (span, cb) =>
@@ -193,7 +193,7 @@ class LLMObs extends NoopLLMObs {
193
193
  const {
194
194
  spanOptions,
195
195
  ...llmobsOptions
196
- } = this.#extractOptions(options)
196
+ } = this.#extractOptions(options, kind)
197
197
 
198
198
  const llmobs = this
199
199
 
@@ -719,21 +719,32 @@ class LLMObs extends NoopLLMObs {
719
719
  }
720
720
  }
721
721
 
722
- #extractOptions (options) {
722
+ #extractOptions (options, kind) {
723
723
  const {
724
724
  modelName,
725
725
  modelProvider,
726
726
  sessionId,
727
727
  mlApp,
728
+ version,
728
729
  _decorator,
729
730
  ...spanOptions
730
731
  } = options
731
732
 
733
+ let agentVersion
734
+ if (version !== undefined) {
735
+ if (kind === 'agent') {
736
+ agentVersion = toAgentVersion(version)
737
+ } else {
738
+ logger.warn('Dropping the version option on a %s span, it is only supported for agent spans.', kind)
739
+ }
740
+ }
741
+
732
742
  return {
733
743
  mlApp,
734
744
  modelName,
735
745
  modelProvider,
736
746
  sessionId,
747
+ agentVersion,
737
748
  _decorator,
738
749
  spanOptions,
739
750
  }