dd-trace 5.96.0 → 5.97.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 (114) hide show
  1. package/index.d.ts +34 -0
  2. package/package.json +9 -7
  3. package/packages/datadog-esbuild/index.js +20 -9
  4. package/packages/datadog-instrumentations/src/child_process.js +7 -17
  5. package/packages/datadog-instrumentations/src/crypto.js +1 -2
  6. package/packages/datadog-instrumentations/src/cucumber.js +4 -1
  7. package/packages/datadog-instrumentations/src/cypress-config.js +324 -0
  8. package/packages/datadog-instrumentations/src/cypress.js +86 -4
  9. package/packages/datadog-instrumentations/src/dns.js +1 -2
  10. package/packages/datadog-instrumentations/src/express.js +4 -4
  11. package/packages/datadog-instrumentations/src/fs.js +27 -29
  12. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  13. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +41 -13
  14. package/packages/datadog-instrumentations/src/helpers/hook.js +31 -6
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +12 -19
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -13
  17. package/packages/datadog-instrumentations/src/helpers/register.js +103 -142
  18. package/packages/datadog-instrumentations/src/http/client.js +2 -3
  19. package/packages/datadog-instrumentations/src/http/server.js +2 -5
  20. package/packages/datadog-instrumentations/src/http2/client.js +1 -3
  21. package/packages/datadog-instrumentations/src/http2/server.js +1 -3
  22. package/packages/datadog-instrumentations/src/jest.js +13 -4
  23. package/packages/datadog-instrumentations/src/limitd-client.js +1 -1
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +4 -1
  25. package/packages/datadog-instrumentations/src/net.js +2 -8
  26. package/packages/datadog-instrumentations/src/pino.js +1 -1
  27. package/packages/datadog-instrumentations/src/playwright.js +4 -1
  28. package/packages/datadog-instrumentations/src/prisma.js +1 -2
  29. package/packages/datadog-instrumentations/src/selenium.js +4 -1
  30. package/packages/datadog-instrumentations/src/sequelize.js +1 -1
  31. package/packages/datadog-instrumentations/src/url.js +1 -3
  32. package/packages/datadog-instrumentations/src/vitest.js +5 -1
  33. package/packages/datadog-instrumentations/src/vm.js +1 -3
  34. package/packages/datadog-plugin-aws-sdk/src/base.js +4 -3
  35. package/packages/datadog-plugin-cucumber/src/index.js +7 -3
  36. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +57 -5
  37. package/packages/datadog-plugin-graphql/src/resolve.js +1 -1
  38. package/packages/datadog-plugin-jest/src/index.js +4 -2
  39. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +31 -4
  40. package/packages/datadog-plugin-mocha/src/index.js +5 -2
  41. package/packages/datadog-plugin-next/src/index.js +2 -14
  42. package/packages/datadog-plugin-openai/src/services.js +1 -0
  43. package/packages/datadog-webpack/index.js +3 -3
  44. package/packages/dd-trace/index.js +12 -10
  45. package/packages/dd-trace/src/agent/url.js +2 -2
  46. package/packages/dd-trace/src/aiguard/sdk.js +4 -0
  47. package/packages/dd-trace/src/appsec/blocking.js +3 -0
  48. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +1 -1
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  51. package/packages/dd-trace/src/appsec/remote_config.js +1 -0
  52. package/packages/dd-trace/src/appsec/sdk/index.js +4 -0
  53. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +6 -1
  54. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  55. package/packages/dd-trace/src/config/defaults.js +316 -146
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -1
  57. package/packages/dd-trace/src/config/helper.js +59 -10
  58. package/packages/dd-trace/src/config/index.js +569 -1505
  59. package/packages/dd-trace/src/config/parsers.js +256 -0
  60. package/packages/dd-trace/src/config/remote_config.js +59 -2
  61. package/packages/dd-trace/src/config/supported-configurations.json +350 -433
  62. package/packages/dd-trace/src/crashtracking/crashtracker.js +7 -1
  63. package/packages/dd-trace/src/crashtracking/index.js +1 -7
  64. package/packages/dd-trace/src/debugger/index.js +1 -1
  65. package/packages/dd-trace/src/dogstatsd.js +12 -9
  66. package/packages/dd-trace/src/encode/0.4.js +1 -1
  67. package/packages/dd-trace/src/exporters/agent/writer.js +7 -1
  68. package/packages/dd-trace/src/exporters/common/request.js +9 -0
  69. package/packages/dd-trace/src/exporters/common/writer.js +12 -2
  70. package/packages/dd-trace/src/heap_snapshots.js +3 -0
  71. package/packages/dd-trace/src/index.js +5 -2
  72. package/packages/dd-trace/src/lambda/runtime/ritm.js +6 -6
  73. package/packages/dd-trace/src/llmobs/index.js +4 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -1
  75. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +60 -12
  76. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +4 -2
  77. package/packages/dd-trace/src/llmobs/sdk.js +12 -8
  78. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  79. package/packages/dd-trace/src/llmobs/tagger.js +9 -6
  80. package/packages/dd-trace/src/llmobs/writers/base.js +2 -0
  81. package/packages/dd-trace/src/llmobs/writers/util.js +3 -0
  82. package/packages/dd-trace/src/log/index.js +26 -55
  83. package/packages/dd-trace/src/log/writer.js +7 -19
  84. package/packages/dd-trace/src/noop/proxy.js +8 -0
  85. package/packages/dd-trace/src/opentelemetry/logs/index.js +1 -1
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +9 -4
  88. package/packages/dd-trace/src/payload-tagging/config/index.js +6 -5
  89. package/packages/dd-trace/src/plugin_manager.js +8 -6
  90. package/packages/dd-trace/src/plugins/ci_plugin.js +4 -0
  91. package/packages/dd-trace/src/plugins/plugin.js +7 -4
  92. package/packages/dd-trace/src/process-tags/index.js +3 -0
  93. package/packages/dd-trace/src/profiler.js +27 -2
  94. package/packages/dd-trace/src/profiling/config.js +73 -241
  95. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -4
  96. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +6 -2
  97. package/packages/dd-trace/src/profiling/profiler.js +56 -44
  98. package/packages/dd-trace/src/profiling/profilers/events.js +2 -3
  99. package/packages/dd-trace/src/profiling/profilers/wall.js +89 -6
  100. package/packages/dd-trace/src/profiling/ssi-heuristics.js +4 -1
  101. package/packages/dd-trace/src/propagation-hash/index.js +2 -1
  102. package/packages/dd-trace/src/proxy.js +32 -3
  103. package/packages/dd-trace/src/remote_config/index.js +3 -0
  104. package/packages/dd-trace/src/require-package-json.js +8 -4
  105. package/packages/dd-trace/src/ritm.js +58 -26
  106. package/packages/dd-trace/src/runtime_metrics/index.js +3 -0
  107. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +3 -0
  108. package/packages/dd-trace/src/sampler.js +1 -1
  109. package/packages/dd-trace/src/standalone/index.js +3 -0
  110. package/packages/dd-trace/src/telemetry/index.js +2 -3
  111. package/packages/dd-trace/src/telemetry/send-data.js +5 -19
  112. package/packages/dd-trace/src/telemetry/session-propagation.js +19 -44
  113. package/packages/dd-trace/src/telemetry/telemetry.js +28 -171
  114. package/packages/dd-trace/src/util.js +0 -9
@@ -23,6 +23,9 @@ class Crashtracker {
23
23
  }
24
24
  }
25
25
 
26
+ /**
27
+ * @param {import('../config/config-base')} config - Tracer configuration
28
+ */
26
29
  start (config) {
27
30
  if (this.#started) return this.configure(config)
28
31
 
@@ -35,7 +38,7 @@ class Crashtracker {
35
38
  this.#getMetadata(config)
36
39
  )
37
40
  } catch (e) {
38
- log.error('Error initialising crashtracker', e)
41
+ log.error('Error initializing crashtracker', e)
39
42
  }
40
43
  }
41
44
 
@@ -49,6 +52,9 @@ class Crashtracker {
49
52
  }
50
53
 
51
54
  // TODO: Send only configured values when defaults are fixed.
55
+ /**
56
+ * @param {import('../config/config-base')} config - Tracer configuration
57
+ */
52
58
  #getConfig (config) {
53
59
  const url = getAgentUrl(config)
54
60
 
@@ -1,15 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { existsSync } = require('node:fs')
4
3
  const { isMainThread } = require('worker_threads')
5
4
  const log = require('../log')
6
5
 
7
- // libdatadog v29 crashtracker segfaults during init on ARM64 musl (Alpine).
8
- // The segfault bypasses JS try/catch so we must avoid loading it entirely.
9
- // See: https://github.com/DataDog/libdatadog-nodejs/issues/114
10
- const isArm64Musl = process.arch === 'arm64' && existsSync('/etc/alpine-release')
11
-
12
- if (isMainThread && !isArm64Musl) {
6
+ if (isMainThread) {
13
7
  try {
14
8
  module.exports = require('./crashtracker')
15
9
  } catch (e) {
@@ -147,7 +147,7 @@ function start (config, rcInstance) {
147
147
  * Sends the new configuration to the worker thread via the config channel.
148
148
  * Does nothing if the worker is not started.
149
149
  *
150
- * @param {Config} config - The updated tracer configuration object
150
+ * @param {import('../config/config-base')} config - The updated tracer configuration object
151
151
  */
152
152
  function configure (config) {
153
153
  if (configChannel === null) return
@@ -1,13 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const lookup = require('dns').lookup // cache to avoid instrumentation
4
3
  const dgram = require('dgram')
5
4
  const isIP = require('net').isIP
6
5
 
7
6
  const request = require('./exporters/common/request')
8
7
  const log = require('./log')
9
8
  const Histogram = require('./histogram')
10
- const defaults = require('./config/defaults')
11
9
  const { getAgentUrl } = require('./agent/url')
12
10
  const { entityId } = require('./exporters/common/docker')
13
11
 
@@ -23,7 +21,9 @@ const TYPE_HISTOGRAM = 'h'
23
21
  * @implements {DogStatsD}
24
22
  */
25
23
  class DogStatsDClient {
26
- constructor (options = {}) {
24
+ #lookup
25
+ constructor (options) {
26
+ this.#lookup = options.lookup
27
27
  if (options.metricsProxyUrl) {
28
28
  this._httpOptions = {
29
29
  method: 'POST',
@@ -32,11 +32,10 @@ class DogStatsDClient {
32
32
  }
33
33
  }
34
34
 
35
- this._host = options.host || defaults['dogstatsd.hostname']
35
+ this._host = options.host
36
36
  this._family = isIP(this._host)
37
- this._port = options.port || defaults['dogstatsd.port']
38
- this._prefix = options.prefix || ''
39
- this._tags = options.tags || []
37
+ this._port = options.port
38
+ this._tags = options.tags
40
39
  this._queue = []
41
40
  this._buffer = ''
42
41
  this._offset = 0
@@ -99,7 +98,7 @@ class DogStatsDClient {
99
98
 
100
99
  _sendUdp (queue) {
101
100
  if (this._family === 0) {
102
- lookup(this._host, (err, address, family) => {
101
+ this.#lookup(this._host, (err, address, family) => {
103
102
  if (err) return log.error('DogStatsDClient: Host not found', err)
104
103
  this._sendUdpFromQueue(queue, address, family)
105
104
  })
@@ -118,7 +117,7 @@ class DogStatsDClient {
118
117
  }
119
118
 
120
119
  _add (stat, value, type, tags) {
121
- let message = `${this._prefix + stat}:${value}|${type}`
120
+ let message = `${stat}:${value}|${type}`
122
121
 
123
122
  // Don't manipulate this._tags as it is still used
124
123
  tags = tags ? [...this._tags, ...tags] : this._tags
@@ -164,6 +163,9 @@ class DogStatsDClient {
164
163
  return socket
165
164
  }
166
165
 
166
+ /**
167
+ * @param {import('./config/config-base')} config - Tracer configuration
168
+ */
167
169
  static generateClientConfig (config) {
168
170
  const tags = []
169
171
 
@@ -183,6 +185,7 @@ class DogStatsDClient {
183
185
  host: config.dogstatsd.hostname,
184
186
  port: config.dogstatsd.port,
185
187
  tags,
188
+ lookup: config.lookup,
186
189
  }
187
190
 
188
191
  if (config.url || config.port) {
@@ -140,7 +140,7 @@ class AgentEncoder {
140
140
  this._traceBytes.length = 0
141
141
  this._stringCount = 0
142
142
  this._stringBytes.length = 0
143
- this._stringMap = {}
143
+ this._stringMap = Object.create(null)
144
144
 
145
145
  this._cacheString('')
146
146
  }
@@ -1,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { inspect } = require('node:util')
4
+ const { channel } = require('dc-polyfill')
5
+
4
6
  const request = require('../common/request')
5
7
  const { logIntegrations, logAgentError } = require('../../startup-log')
6
8
  const runtimeMetrics = require('../../runtime_metrics')
@@ -10,10 +12,14 @@ const BaseWriter = require('../common/writer')
10
12
  const propagationHash = require('../../propagation-hash')
11
13
 
12
14
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
15
+ const firstFlushChannel = channel('dd-trace:exporter:first-flush')
13
16
 
14
17
  class AgentWriter extends BaseWriter {
15
18
  constructor (...args) {
16
- super(...args)
19
+ super({
20
+ ...args[0],
21
+ beforeFirstFlush: () => firstFlushChannel.publish(),
22
+ })
17
23
  const { prioritySampler, lookup, protocolVersion, headers, config = {} } = args[0]
18
24
  const AgentEncoder = getEncoder(protocolVersion)
19
25
 
@@ -18,6 +18,10 @@ const maxActiveBufferSize = 1024 * 1024 * 64
18
18
 
19
19
  let activeBufferSize = 0
20
20
 
21
+ /**
22
+ * @param {string|URL|object} urlObjOrString
23
+ * @returns {object}
24
+ */
21
25
  function parseUrl (urlObjOrString) {
22
26
  if (urlObjOrString !== null && typeof urlObjOrString === 'object') return urlToHttpOptions(urlObjOrString)
23
27
 
@@ -33,6 +37,11 @@ function parseUrl (urlObjOrString) {
33
37
  return url
34
38
  }
35
39
 
40
+ /**
41
+ * @param {Buffer|string|Readable|Array<Buffer|string>} data
42
+ * @param {object} options
43
+ * @param {(error: Error|null, result: string, statusCode: number) => void} callback
44
+ */
36
45
  function request (data, options, callback) {
37
46
  if (!options.headers) {
38
47
  options.headers = {}
@@ -1,14 +1,21 @@
1
1
  'use strict'
2
2
 
3
+ const { channel } = require('dc-polyfill')
4
+
3
5
  const log = require('../../log')
4
6
  const request = require('./request')
5
7
  const { safeJSONStringify } = require('./util')
6
8
 
9
+ const firstFlushChannel = channel('dd-trace:exporter:first-flush')
10
+
7
11
  class Writer {
8
- constructor ({ url }) {
12
+ constructor ({ url, beforeFirstFlush }) {
9
13
  this._url = url
14
+ this._beforeFirstFlush = beforeFirstFlush
10
15
  }
11
16
 
17
+ #isFirstFlush = true
18
+
12
19
  flush (done = () => {}) {
13
20
  const count = this._encoder.count()
14
21
 
@@ -16,8 +23,11 @@ class Writer {
16
23
  this._encoder.reset()
17
24
  done()
18
25
  } else if (count > 0) {
26
+ if (this.#isFirstFlush && firstFlushChannel.hasSubscribers && this._beforeFirstFlush) {
27
+ this.#isFirstFlush = false
28
+ this._beforeFirstFlush()
29
+ }
19
30
  const payload = this._encoder.makePayload()
20
-
21
31
  this._sendPayload(payload, count, done)
22
32
  } else {
23
33
  done()
@@ -45,6 +45,9 @@ function getName (destination) {
45
45
  }
46
46
 
47
47
  module.exports = {
48
+ /**
49
+ * @param {import('./config/config-base')} config - Tracer configuration
50
+ */
48
51
  async start (config) {
49
52
  const destination = config.heapSnapshot.destination
50
53
 
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { getValueFromEnvSources } = require('./config/helper')
4
- const { isFalse } = require('./util')
4
+ const { isFalse, isTrue } = require('./util')
5
5
 
6
6
  // Global `jest` is only present in Jest workers.
7
7
  const inJestWorker = typeof jest !== 'undefined'
@@ -9,7 +9,10 @@ const inJestWorker = typeof jest !== 'undefined'
9
9
  const ddTraceDisabled = getValueFromEnvSources('DD_TRACE_ENABLED')
10
10
  ? isFalse(getValueFromEnvSources('DD_TRACE_ENABLED'))
11
11
  : String(getValueFromEnvSources('OTEL_TRACES_EXPORTER')).toLowerCase() === 'none'
12
+ const shouldUseProxyWhenTracingDisabled =
13
+ isTrue(getValueFromEnvSources('DD_DYNAMIC_INSTRUMENTATION_ENABLED')) ||
14
+ isTrue(getValueFromEnvSources('DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED'))
12
15
 
13
- module.exports = ddTraceDisabled || inJestWorker
16
+ module.exports = (ddTraceDisabled && !shouldUseProxyWhenTracingDisabled) || inJestWorker
14
17
  ? require('./noop/proxy')
15
18
  : require('./proxy')
@@ -89,12 +89,12 @@ const registerLambdaHook = () => {
89
89
  const lambdaFilePaths = _getLambdaFilePaths(lambdaStylePath)
90
90
 
91
91
  // TODO: Redo this like any other instrumentation.
92
- Hook(lambdaFilePaths, (moduleExports, name) => {
92
+ Hook(lambdaFilePaths, (moduleExports, name, _, moduleVersion) => {
93
93
  require('./patch')
94
94
 
95
95
  for (const { hook } of instrumentations[name]) {
96
96
  try {
97
- moduleExports = hook(moduleExports)
97
+ moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports
98
98
  } catch (e) {
99
99
  log.error('Error executing lambda hook', e)
100
100
  }
@@ -104,16 +104,16 @@ const registerLambdaHook = () => {
104
104
  })
105
105
  } else {
106
106
  const moduleToPatch = 'datadog-lambda-js'
107
- Hook([moduleToPatch], (moduleExports, moduleName, _) => {
107
+ Hook([moduleToPatch], (moduleExports, moduleName, _, moduleVersion) => {
108
108
  moduleName = moduleName.replace(pathSepExpr, '/')
109
109
 
110
110
  require('./patch')
111
111
 
112
- for (const { name, file, hook } of instrumentations[moduleToPatch]) {
113
- const fullFilename = filename(name, file)
112
+ for (const { file, hook } of instrumentations[moduleToPatch]) {
113
+ const fullFilename = filename(moduleToPatch, file)
114
114
  if (moduleName === fullFilename) {
115
115
  try {
116
- moduleExports = hook(moduleExports)
116
+ moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports
117
117
  } catch (e) {
118
118
  log.error('Error executing lambda hook for datadog-lambda-js', e)
119
119
  }
@@ -43,9 +43,12 @@ let spanWriter
43
43
  /** @type {LLMObsEvalMetricsWriter | null} */
44
44
  let evalWriter
45
45
 
46
- /** @type {import('../config')} */
46
+ /** @type {import('../config/config-base')} */
47
47
  let globalTracerConfig
48
48
 
49
+ /**
50
+ * @param {@type import('../config/config-base')} config
51
+ */
49
52
  function enable (config) {
50
53
  globalTracerConfig = config
51
54
 
@@ -18,6 +18,7 @@ const {
18
18
  getToolNameFromTags,
19
19
  getToolCallResultContent,
20
20
  getLlmObsSpanName,
21
+ getTelemetryMetadata,
21
22
  } = require('./util')
22
23
 
23
24
  /**
@@ -216,6 +217,9 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin {
216
217
 
217
218
  this._tagger.tagEmbeddingIO(span, parsedInputs, output)
218
219
 
220
+ const metadata = getTelemetryMetadata(tags)
221
+ this._tagger.tagMetadata(span, metadata)
222
+
219
223
  const usage = tags['ai.usage.tokens']
220
224
  this._tagger.tagMetrics(span, {
221
225
  inputTokens: usage,
@@ -234,7 +238,7 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin {
234
238
 
235
239
  this._tagger.tagTextIO(span, prompt, output)
236
240
 
237
- const metadata = getGenerationMetadata(tags) ?? {}
241
+ const metadata = getGenerationMetadata(tags)
238
242
  metadata.schema = getJsonStringValue(tags['ai.schema'], {})
239
243
  this._tagger.tagMetadata(span, metadata)
240
244
  }
@@ -10,6 +10,10 @@ const MODEL_METADATA_KEYS = new Set([
10
10
  'stop_sequences',
11
11
  ])
12
12
 
13
+ const VERCEL_AI_TELEMETRY_METADATA_PREFIX = 'ai.telemetry.metadata.'
14
+ const VERCEL_AI_MODEL_METADATA_PREFIX = 'gen_ai.request.'
15
+ const VERCEL_AI_GENERATION_METADATA_PREFIX = 'ai.settings.'
16
+
13
17
  /**
14
18
  * @typedef {import('../../../opentracing/span')} Span
15
19
  *
@@ -107,17 +111,29 @@ function getJsonStringValue (str, defaultValue) {
107
111
 
108
112
  /**
109
113
  * Get the model metadata from the span tags (top_p, top_k, temperature, etc.)
114
+ * Additionally, set telemetry metadata from manual telemetry tags.
110
115
  * @param {SpanTags} tags
111
116
  * @returns {Record<string, unknown> | null}
112
117
  */
113
118
  function getModelMetadata (tags) {
114
119
  /** @type {Record<string, unknown>} */
115
120
  const modelMetadata = {}
116
- for (const metadata of MODEL_METADATA_KEYS) {
117
- const metadataTagKey = `gen_ai.request.${metadata}`
118
- const metadataValue = tags[metadataTagKey]
119
- if (metadataValue) {
120
- modelMetadata[metadata] = metadataValue
121
+ for (const tag of Object.keys(tags)) {
122
+ const isModelMetadata = tag.startsWith(VERCEL_AI_MODEL_METADATA_PREFIX)
123
+ if (isModelMetadata) {
124
+ const lastCommaPosition = tag.lastIndexOf('.')
125
+ const metadataKey = lastCommaPosition === -1 ? tag : tag.slice(lastCommaPosition + 1)
126
+ if (metadataKey && MODEL_METADATA_KEYS.has(metadataKey)) {
127
+ modelMetadata[metadataKey] = tags[tag]
128
+ }
129
+ } else {
130
+ const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
131
+ if (isTelemetryMetadata) {
132
+ const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
133
+ if (metadataKey) {
134
+ modelMetadata[metadataKey] = tags[tag]
135
+ }
136
+ }
121
137
  }
122
138
  }
123
139
 
@@ -126,6 +142,7 @@ function getModelMetadata (tags) {
126
142
 
127
143
  /**
128
144
  * Get the generation metadata from the span tags (maxSteps, maxRetries, etc.)
145
+ * Additionally, set telemetry metadata from manual telemetry tags.
129
146
  * @param {SpanTags} tags
130
147
  * @returns {Record<string, unknown> | null}
131
148
  */
@@ -134,14 +151,24 @@ function getGenerationMetadata (tags) {
134
151
  const metadata = {}
135
152
 
136
153
  for (const tag of Object.keys(tags)) {
137
- if (!tag.startsWith('ai.settings')) continue
138
-
139
- const settingKey = tag.split('.').pop()
140
- const transformedKey = settingKey.replaceAll(/[A-Z]/g, letter => '_' + letter.toLowerCase())
141
- if (MODEL_METADATA_KEYS.has(transformedKey)) continue
154
+ const isGenerationMetadata = tag.startsWith(VERCEL_AI_GENERATION_METADATA_PREFIX)
155
+ if (isGenerationMetadata) {
156
+ const lastCommaPosition = tag.lastIndexOf('.')
157
+ const settingKey = lastCommaPosition === -1 ? tag : tag.slice(lastCommaPosition + 1)
158
+ const transformedKey = settingKey.replaceAll(/[A-Z]/g, letter => '_' + letter.toLowerCase())
159
+ if (MODEL_METADATA_KEYS.has(transformedKey)) continue
142
160
 
143
- const settingValue = tags[tag]
144
- metadata[settingKey] = settingValue
161
+ const settingValue = tags[tag]
162
+ metadata[settingKey] = settingValue
163
+ } else {
164
+ const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
165
+ if (isTelemetryMetadata) {
166
+ const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
167
+ if (metadataKey) {
168
+ metadata[metadataKey] = tags[tag]
169
+ }
170
+ }
171
+ }
145
172
  }
146
173
 
147
174
  return Object.keys(metadata).length ? metadata : null
@@ -205,6 +232,26 @@ function getLlmObsSpanName (operation, functionId) {
205
232
  return functionId ? `${functionId}.${operation}` : operation
206
233
  }
207
234
 
235
+ /**
236
+ * Get custom telemetry metadata from ai.telemetry.metadata.* attributes
237
+ * @param {Record<string, unknown>} tags
238
+ * @returns {Record<string, unknown> | null}
239
+ */
240
+ function getTelemetryMetadata (tags) {
241
+ const metadata = {}
242
+
243
+ for (const tag of Object.keys(tags)) {
244
+ if (!tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)) continue
245
+
246
+ const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
247
+ if (metadataKey) {
248
+ metadata[metadataKey] = tags[tag]
249
+ }
250
+ }
251
+
252
+ return Object.keys(metadata).length ? metadata : null
253
+ }
254
+
208
255
  module.exports = {
209
256
  getSpanTags,
210
257
  getOperation,
@@ -215,4 +262,5 @@ module.exports = {
215
262
  getToolNameFromTags,
216
263
  getToolCallResultContent,
217
264
  getLlmObsSpanName,
265
+ getTelemetryMetadata,
218
266
  }
@@ -65,10 +65,12 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
65
65
  telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: true, integration: 'bedrock' })
66
66
 
67
67
  const parent = llmobsStore.getStore()?.span
68
+ // Use full modelId and unified provider for LLMObs (required for backend cost estimation).
69
+ // Split modelProvider/modelName from parseModelId() are still used below for response parsing.
68
70
  this._tagger.registerLLMObsSpan(span, {
69
71
  parent,
70
- modelName: modelName.toLowerCase(),
71
- modelProvider: modelProvider.toLowerCase(),
72
+ modelName: request.params.modelId.toLowerCase(),
73
+ modelProvider: 'amazon_bedrock',
72
74
  kind: 'llm',
73
75
  name: 'bedrock-runtime.command',
74
76
  integration: 'bedrock',
@@ -29,16 +29,23 @@ class LLMObs extends NoopLLMObs {
29
29
  */
30
30
  #hasUserSpanProcessor = false
31
31
 
32
+ /**
33
+ * @param {import('../tracer')} tracer - Tracer instance
34
+ * @param {import('./index')} llmobsModule - LLMObs module instance
35
+ * @param {import('../config/config-base')} config - Tracer configuration
36
+ */
32
37
  constructor (tracer, llmobsModule, config) {
33
38
  super(tracer)
34
39
 
40
+ /** @type {import('../config/config-base')} */
35
41
  this._config = config
42
+
36
43
  this._llmobsModule = llmobsModule
37
44
  this._tagger = new LLMObsTagger(config)
38
45
  }
39
46
 
40
47
  get enabled () {
41
- return this._config.llmobs.enabled
48
+ return this._config.llmobs.enabled ?? false
42
49
  }
43
50
 
44
51
  enable (options = {}) {
@@ -56,13 +63,10 @@ class LLMObs extends NoopLLMObs {
56
63
  return
57
64
  }
58
65
 
59
- const llmobs = {
60
- mlApp: options.mlApp,
61
- agentlessEnabled: options.agentlessEnabled,
62
- }
63
- // TODO: This will update config telemetry with the origin 'code', which is not ideal when `enable()` is called
64
- // based on `APM_TRACING` RC product updates.
65
- this._config.updateOptions({ llmobs })
66
+ // TODO: These configs should be passed through directly at construction time instead.
67
+ this._config.llmobs.enabled = true
68
+ this._config.llmobs.mlApp = options.mlApp
69
+ this._config.llmobs.agentlessEnabled = options.agentlessEnabled
66
70
 
67
71
  // configure writers and channel subscribers
68
72
  this._llmobsModule.enable(this._config)
@@ -49,7 +49,7 @@ class LLMObservabilitySpan {
49
49
  }
50
50
 
51
51
  class LLMObsSpanProcessor {
52
- /** @type {import('../config')} */
52
+ /** @type {import('../config/config-base')} */
53
53
  #config
54
54
 
55
55
  /** @type {((span: LLMObservabilitySpan) => LLMObservabilitySpan | null) | null} */
@@ -47,8 +47,11 @@ const { storage } = require('./storage')
47
47
  const registry = new WeakMap()
48
48
 
49
49
  class LLMObsTagger {
50
+ /** @type {import('../config/config-base')} */
51
+ #config
52
+
50
53
  constructor (config, softFail = false) {
51
- this._config = config
54
+ this.#config = config
52
55
 
53
56
  this.softFail = softFail
54
57
  }
@@ -72,15 +75,15 @@ class LLMObsTagger {
72
75
  integration,
73
76
  _decorator,
74
77
  } = {}) {
75
- if (!this._config.llmobs.enabled) return
78
+ if (!this.#config.llmobs.enabled) return
76
79
  if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
77
80
 
78
81
  const spanMlApp =
79
82
  mlApp ||
80
83
  registry.get(parent)?.[ML_APP] ||
81
84
  span.context()._trace.tags[PROPAGATED_ML_APP_KEY] ||
82
- this._config.llmobs.mlApp ||
83
- this._config.service // this should always have a default
85
+ this.#config.llmobs.mlApp ||
86
+ this.#config.service // this should always have a default
84
87
 
85
88
  if (!spanMlApp) {
86
89
  throw new Error(
@@ -624,7 +627,7 @@ class LLMObsTagger {
624
627
  }
625
628
 
626
629
  _register (span) {
627
- if (!this._config.llmobs.enabled) return
630
+ if (!this.#config.llmobs.enabled) return
628
631
  if (registry.has(span)) {
629
632
  this.#handleFailure(`LLMObs Span "${span._name}" already registered.`)
630
633
  return
@@ -634,7 +637,7 @@ class LLMObsTagger {
634
637
  }
635
638
 
636
639
  _setTag (span, key, value) {
637
- if (!this._config.llmobs.enabled) return
640
+ if (!this.#config.llmobs.enabled) return
638
641
  if (!registry.has(span)) {
639
642
  this.#handleFailure(`Span "${span._name}" must be an LLMObs generated span.`)
640
643
  return
@@ -45,7 +45,9 @@ class BaseLLMObsWriter {
45
45
  /** @type {LLMObsBuffer} */
46
46
  this._buffer = new LLMObsBuffer({ events: [], size: 0, isDefault: true })
47
47
 
48
+ /** @type {import('../../config/config-base')} */
48
49
  this._config = config
50
+
49
51
  this._endpoint = endpoint
50
52
  this._baseEndpoint = endpoint // should not be unset
51
53
  this._intake = intake
@@ -6,6 +6,9 @@ const telemetry = require('../telemetry')
6
6
  const { fetchAgentInfo } = require('../../agent/info')
7
7
  const { getAgentUrl } = require('../../agent/url')
8
8
 
9
+ /**
10
+ * @param {import('../../config/config-base')} config
11
+ */
9
12
  function setAgentStrategy (config, setWritersAgentlessValue) {
10
13
  const agentlessEnabled = config.llmobs.agentlessEnabled
11
14