dd-trace 5.100.0 → 5.102.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 (189) hide show
  1. package/index.d.ts +14 -0
  2. package/package.json +11 -9
  3. package/packages/datadog-instrumentations/src/aerospike.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +8 -8
  5. package/packages/datadog-instrumentations/src/amqplib.js +6 -7
  6. package/packages/datadog-instrumentations/src/anthropic.js +10 -10
  7. package/packages/datadog-instrumentations/src/apollo-server-core.js +3 -3
  8. package/packages/datadog-instrumentations/src/apollo-server.js +5 -5
  9. package/packages/datadog-instrumentations/src/avsc.js +6 -6
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +151 -67
  11. package/packages/datadog-instrumentations/src/azure-durable-functions.js +8 -8
  12. package/packages/datadog-instrumentations/src/bluebird.js +2 -2
  13. package/packages/datadog-instrumentations/src/body-parser.js +2 -2
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +7 -7
  15. package/packages/datadog-instrumentations/src/child_process.js +12 -12
  16. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +9 -9
  17. package/packages/datadog-instrumentations/src/connect.js +7 -7
  18. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  19. package/packages/datadog-instrumentations/src/cookie.js +2 -2
  20. package/packages/datadog-instrumentations/src/couchbase.js +16 -30
  21. package/packages/datadog-instrumentations/src/crypto.js +4 -4
  22. package/packages/datadog-instrumentations/src/cucumber.js +77 -16
  23. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  24. package/packages/datadog-instrumentations/src/dns.js +0 -3
  25. package/packages/datadog-instrumentations/src/elasticsearch.js +8 -11
  26. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +6 -6
  27. package/packages/datadog-instrumentations/src/express-session.js +4 -4
  28. package/packages/datadog-instrumentations/src/express.js +10 -11
  29. package/packages/datadog-instrumentations/src/fastify.js +2 -2
  30. package/packages/datadog-instrumentations/src/fs.js +14 -14
  31. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +5 -7
  32. package/packages/datadog-instrumentations/src/google-genai.js +4 -4
  33. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  34. package/packages/datadog-instrumentations/src/hapi.js +2 -2
  35. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +8 -8
  36. package/packages/datadog-instrumentations/src/helpers/promise.js +2 -2
  37. package/packages/datadog-instrumentations/src/hono.js +2 -2
  38. package/packages/datadog-instrumentations/src/http/client.js +26 -9
  39. package/packages/datadog-instrumentations/src/http/server.js +9 -9
  40. package/packages/datadog-instrumentations/src/jest.js +93 -63
  41. package/packages/datadog-instrumentations/src/kafkajs.js +9 -9
  42. package/packages/datadog-instrumentations/src/knex.js +17 -17
  43. package/packages/datadog-instrumentations/src/koa.js +12 -12
  44. package/packages/datadog-instrumentations/src/ldapjs.js +5 -5
  45. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  46. package/packages/datadog-instrumentations/src/limitd-client.js +4 -4
  47. package/packages/datadog-instrumentations/src/lodash.js +4 -4
  48. package/packages/datadog-instrumentations/src/mariadb.js +13 -13
  49. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  50. package/packages/datadog-instrumentations/src/microgateway-core.js +2 -2
  51. package/packages/datadog-instrumentations/src/mocha/common.js +7 -4
  52. package/packages/datadog-instrumentations/src/mocha/main.js +37 -14
  53. package/packages/datadog-instrumentations/src/mocha/utils.js +133 -16
  54. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -7
  55. package/packages/datadog-instrumentations/src/mongodb-core.js +9 -22
  56. package/packages/datadog-instrumentations/src/mongodb.js +5 -5
  57. package/packages/datadog-instrumentations/src/mongoose.js +21 -21
  58. package/packages/datadog-instrumentations/src/mquery.js +5 -5
  59. package/packages/datadog-instrumentations/src/multer.js +4 -4
  60. package/packages/datadog-instrumentations/src/mysql.js +16 -16
  61. package/packages/datadog-instrumentations/src/mysql2.js +4 -4
  62. package/packages/datadog-instrumentations/src/net.js +14 -8
  63. package/packages/datadog-instrumentations/src/nyc.js +5 -5
  64. package/packages/datadog-instrumentations/src/openai.js +19 -19
  65. package/packages/datadog-instrumentations/src/oracledb.js +6 -6
  66. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  67. package/packages/datadog-instrumentations/src/passport-utils.js +5 -5
  68. package/packages/datadog-instrumentations/src/pg.js +15 -15
  69. package/packages/datadog-instrumentations/src/pino.js +6 -10
  70. package/packages/datadog-instrumentations/src/playwright.js +20 -15
  71. package/packages/datadog-instrumentations/src/protobufjs.js +16 -16
  72. package/packages/datadog-instrumentations/src/redis.js +1 -2
  73. package/packages/datadog-instrumentations/src/restify.js +2 -2
  74. package/packages/datadog-instrumentations/src/router.js +12 -12
  75. package/packages/datadog-instrumentations/src/stripe.js +12 -12
  76. package/packages/datadog-instrumentations/src/vitest.js +107 -26
  77. package/packages/datadog-instrumentations/src/winston.js +4 -4
  78. package/packages/datadog-instrumentations/src/ws.js +7 -7
  79. package/packages/datadog-plugin-aws-sdk/src/base.js +52 -4
  80. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +19 -12
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +45 -35
  82. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +33 -22
  83. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +12 -13
  84. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +73 -54
  85. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +19 -17
  86. package/packages/datadog-plugin-aws-sdk/src/util.js +22 -0
  87. package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
  88. package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -6
  90. package/packages/datadog-plugin-cucumber/src/index.js +4 -0
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -4
  92. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  93. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -5
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +3 -1
  95. package/packages/datadog-plugin-http/src/client.js +1 -5
  96. package/packages/datadog-plugin-jest/src/util.js +1 -2
  97. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  99. package/packages/datadog-plugin-mocha/src/index.js +4 -0
  100. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -1
  101. package/packages/datadog-plugin-openai/src/tracing.js +12 -23
  102. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  103. package/packages/datadog-plugin-vitest/src/index.js +8 -1
  104. package/packages/datadog-shimmer/src/shimmer.js +7 -1
  105. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  106. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +81 -81
  107. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  110. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -2
  111. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  112. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  113. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +83 -48
  114. package/packages/dd-trace/src/appsec/index.js +21 -24
  115. package/packages/dd-trace/src/appsec/reporter.js +7 -2
  116. package/packages/dd-trace/src/appsec/rule_manager.js +4 -2
  117. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +31 -16
  118. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  119. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  120. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  121. package/packages/dd-trace/src/config/git_properties.js +2 -2
  122. package/packages/dd-trace/src/config/index.js +1 -55
  123. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  124. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  125. package/packages/dd-trace/src/datastreams/index.js +2 -1
  126. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  127. package/packages/dd-trace/src/datastreams/processor.js +18 -17
  128. package/packages/dd-trace/src/datastreams/size.js +6 -2
  129. package/packages/dd-trace/src/debugger/config.js +5 -2
  130. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  131. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  132. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +1 -0
  133. package/packages/dd-trace/src/dogstatsd.js +10 -7
  134. package/packages/dd-trace/src/encode/0.4.js +759 -234
  135. package/packages/dd-trace/src/encode/0.5.js +15 -9
  136. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  137. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  138. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  139. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  140. package/packages/dd-trace/src/git_metadata.js +66 -0
  141. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  142. package/packages/dd-trace/src/id.js +15 -26
  143. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -2
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  146. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  147. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +33 -13
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  149. package/packages/dd-trace/src/llmobs/sdk.js +29 -27
  150. package/packages/dd-trace/src/llmobs/span_processor.js +52 -6
  151. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  153. package/packages/dd-trace/src/llmobs/util.js +81 -5
  154. package/packages/dd-trace/src/msgpack/chunk.js +6 -3
  155. package/packages/dd-trace/src/openfeature/noop.js +40 -36
  156. package/packages/dd-trace/src/openfeature/writers/exposures.js +33 -52
  157. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  158. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  159. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
  160. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +1 -2
  161. package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
  162. package/packages/dd-trace/src/opentelemetry/span.js +42 -80
  163. package/packages/dd-trace/src/opentelemetry/tracer.js +0 -22
  164. package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
  165. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +2 -11
  166. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
  167. package/packages/dd-trace/src/opentracing/span.js +56 -48
  168. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  169. package/packages/dd-trace/src/plugins/util/ci.js +1 -1
  170. package/packages/dd-trace/src/plugins/util/git-cache.js +3 -5
  171. package/packages/dd-trace/src/plugins/util/test.js +19 -7
  172. package/packages/dd-trace/src/plugins/util/url.js +1 -3
  173. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  174. package/packages/dd-trace/src/plugins/util/web.js +5 -7
  175. package/packages/dd-trace/src/priority_sampler.js +6 -4
  176. package/packages/dd-trace/src/profiling/config.js +5 -4
  177. package/packages/dd-trace/src/profiling/profilers/events.js +3 -23
  178. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -5
  179. package/packages/dd-trace/src/remote_config/index.js +5 -3
  180. package/packages/dd-trace/src/runtime_metrics/index.js +2 -2
  181. package/packages/dd-trace/src/scope.js +3 -10
  182. package/packages/dd-trace/src/serverless.js +1 -4
  183. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +7 -1
  184. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +4 -0
  185. package/packages/dd-trace/src/span_format.js +52 -5
  186. package/packages/dd-trace/src/span_processor.js +0 -4
  187. package/packages/dd-trace/src/spanleak.js +0 -1
  188. package/packages/dd-trace/src/tracer.js +7 -7
  189. package/packages/dd-trace/src/util.js +17 -0
@@ -7,6 +7,7 @@ const {
7
7
  INSTRUMENTATION_METHOD_AUTO,
8
8
  UNKNOWN_MODEL_PROVIDER,
9
9
  } = require('../../constants/tags')
10
+ const { safeJsonParse } = require('../../util')
10
11
  const {
11
12
  extractChatTemplateFromInstructions,
12
13
  normalizePromptVariables,
@@ -183,13 +184,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
183
184
  _tagChatCompletion (span, inputs, response, error) {
184
185
  const { messages, model, ...parameters } = inputs
185
186
 
186
- const metadata = Object.entries(parameters).reduce((obj, [key, value]) => {
187
- if (!['tools', 'functions'].includes(key)) {
188
- obj[key] = value
187
+ const metadata = {}
188
+ for (const key of Object.keys(parameters)) {
189
+ if (key !== 'tools' && key !== 'functions') {
190
+ metadata[key] = parameters[key]
189
191
  }
190
-
191
- return obj
192
- }, {})
192
+ }
193
193
 
194
194
  this._tagger.tagMetadata(span, metadata)
195
195
 
@@ -213,14 +213,14 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
213
213
  if (message.function_call) {
214
214
  const functionCallInfo = {
215
215
  name: message.function_call.name,
216
- arguments: JSON.parse(message.function_call.arguments),
216
+ arguments: safeJsonParse(message.function_call.arguments),
217
217
  }
218
218
  outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
219
219
  } else if (message.tool_calls) {
220
220
  const toolCallsInfo = []
221
221
  for (const toolCall of message.tool_calls) {
222
222
  const toolCallInfo = {
223
- arguments: JSON.parse(toolCall.function.arguments),
223
+ arguments: safeJsonParse(toolCall.function.arguments),
224
224
  name: toolCall.function.name,
225
225
  toolId: toolCall.id,
226
226
  type: toolCall.type,
@@ -277,22 +277,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
277
277
  inputMessages.push({ role, content })
278
278
  }
279
279
  } else if (item.type === 'function_call') {
280
- // Function call: convert to message with tool_calls
281
- // Parse arguments if it's a JSON string
282
- let parsedArgs = item.arguments
283
- if (typeof parsedArgs === 'string') {
284
- try {
285
- parsedArgs = JSON.parse(parsedArgs)
286
- } catch {
287
- parsedArgs = {}
288
- }
289
- }
290
280
  inputMessages.push({
291
281
  role: 'assistant',
292
282
  toolCalls: [{
293
283
  toolId: item.call_id,
294
284
  name: item.name,
295
- arguments: parsedArgs,
285
+ arguments: safeJsonParse(item.arguments, {}),
296
286
  type: item.type,
297
287
  }],
298
288
  })
@@ -317,12 +307,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
317
307
  inputMessages.push({ role: 'user', content: input })
318
308
  }
319
309
 
320
- const inputMetadata = Object.entries(parameters).reduce((obj, [key, value]) => {
310
+ const inputMetadata = {}
311
+ for (const key of Object.keys(parameters)) {
321
312
  if (allowedParamKeys.has(key)) {
322
- obj[key] = value
313
+ inputMetadata[key] = parameters[key]
323
314
  }
324
- return obj
325
- }, {})
315
+ }
326
316
 
327
317
  this._tagger.tagMetadata(span, inputMetadata)
328
318
 
@@ -354,21 +344,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
354
344
  })
355
345
  } else if (item.type === 'function_call') {
356
346
  // Handle function_call type (responses API tool calls)
357
- let args = item.arguments
358
- // Parse arguments if it's a JSON string
359
- if (typeof args === 'string') {
360
- try {
361
- args = JSON.parse(args)
362
- } catch {
363
- args = {}
364
- }
365
- }
366
347
  outputMessages.push({
367
348
  role: 'assistant',
368
349
  toolCalls: [{
369
350
  toolId: item.call_id,
370
351
  name: item.name,
371
- arguments: args,
352
+ arguments: safeJsonParse(item.arguments, {}),
372
353
  type: item.type,
373
354
  }],
374
355
  })
@@ -390,23 +371,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
390
371
 
391
372
  // Extract tool calls if present in message.tool_calls
392
373
  if (Array.isArray(item.tool_calls)) {
393
- outputMsg.toolCalls = item.tool_calls.map(tc => {
394
- let args = tc.function?.arguments || tc.arguments
395
- // Parse arguments if it's a JSON string
396
- if (typeof args === 'string') {
397
- try {
398
- args = JSON.parse(args)
399
- } catch {
400
- args = {}
401
- }
402
- }
403
- return {
404
- toolId: tc.id,
405
- name: tc.function?.name || tc.name,
406
- arguments: args,
407
- type: tc.type || 'function_call',
408
- }
409
- })
374
+ outputMsg.toolCalls = item.tool_calls.map(tc => ({
375
+ toolId: tc.id,
376
+ name: tc.function?.name || tc.name,
377
+ arguments: safeJsonParse(tc.function?.arguments || tc.arguments, {}),
378
+ type: tc.type || 'function_call',
379
+ }))
410
380
  }
411
381
 
412
382
  outputMessages.push(outputMsg)
@@ -112,16 +112,16 @@ class LLMObs extends NoopLLMObs {
112
112
  const {
113
113
  spanOptions,
114
114
  ...llmobsOptions
115
- } = this._extractOptions(options)
115
+ } = this.#extractOptions(options)
116
116
 
117
117
  if (fn.length > 1) {
118
118
  return this._tracer.trace(name, spanOptions, (span, cb) =>
119
- this._activate(span, { kind, ...llmobsOptions }, () => fn(span, cb))
119
+ this.#activate(span, { kind, ...llmobsOptions }, () => fn(span, cb))
120
120
  )
121
121
  }
122
122
 
123
123
  return this._tracer.trace(name, spanOptions, span =>
124
- this._activate(span, { kind, ...llmobsOptions }, () => fn(span))
124
+ this.#activate(span, { kind, ...llmobsOptions }, () => fn(span))
125
125
  )
126
126
  }
127
127
 
@@ -142,53 +142,53 @@ class LLMObs extends NoopLLMObs {
142
142
  const {
143
143
  spanOptions,
144
144
  ...llmobsOptions
145
- } = this._extractOptions(options)
145
+ } = this.#extractOptions(options)
146
146
 
147
147
  const llmobs = this
148
148
 
149
- function wrapped () {
149
+ function wrapped (...args) {
150
150
  telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: false, kind })
151
151
 
152
152
  const span = llmobs._tracer.scope().active()
153
- const fnArgs = arguments
153
+ const fnArgs = args
154
154
 
155
155
  const lastArgId = fnArgs.length - 1
156
156
  const cb = fnArgs[lastArgId]
157
157
  const hasCallback = typeof cb === 'function'
158
158
 
159
159
  if (hasCallback) {
160
- const scopeBoundCb = llmobs._bind(cb)
161
- fnArgs[lastArgId] = function () {
160
+ const scopeBoundCb = llmobs.#bind(cb)
161
+ fnArgs[lastArgId] = function (...args) {
162
162
  // it is standard practice to follow the callback signature (err, result)
163
163
  // however, we try to parse the arguments to determine if the first argument is an error
164
164
  // if it is not, and is not undefined, we will use that for the output value
165
- const maybeError = arguments[0]
166
- const maybeResult = arguments[1]
165
+ const maybeError = args[0]
166
+ const maybeResult = args[1]
167
167
 
168
- llmobs._autoAnnotate(
168
+ llmobs.#autoAnnotate(
169
169
  span,
170
170
  kind,
171
171
  getFunctionArguments(fn, fnArgs),
172
172
  isError(maybeError) || maybeError == null ? maybeResult : maybeError
173
173
  )
174
174
 
175
- return scopeBoundCb.apply(this, arguments)
175
+ return scopeBoundCb.apply(this, args)
176
176
  }
177
177
  }
178
178
 
179
179
  try {
180
- const result = llmobs._activate(span, { kind, ...llmobsOptions }, () => fn.apply(this, fnArgs))
180
+ const result = llmobs.#activate(span, { kind, ...llmobsOptions }, () => fn.apply(this, fnArgs))
181
181
 
182
182
  if (result && typeof result.then === 'function') {
183
183
  return result.then(
184
184
  value => {
185
185
  if (!hasCallback) {
186
- llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value)
186
+ llmobs.#autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value)
187
187
  }
188
188
  return value
189
189
  },
190
190
  err => {
191
- llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
191
+ llmobs.#autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
192
192
  throw err
193
193
  }
194
194
  )
@@ -199,12 +199,12 @@ class LLMObs extends NoopLLMObs {
199
199
  // the callback is called before the function returns (although unlikely)
200
200
  // we do not want to throw for "annotating a finished span" in this case
201
201
  if (!hasCallback) {
202
- llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result)
202
+ llmobs.#autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result)
203
203
  }
204
204
 
205
205
  return result
206
206
  } catch (e) {
207
- llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
207
+ llmobs.#autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
208
208
  throw e
209
209
  }
210
210
  }
@@ -251,7 +251,7 @@ class LLMObs extends NoopLLMObs {
251
251
  throw new Error('LLMObs span must have a span kind specified')
252
252
  }
253
253
 
254
- const { inputData, outputData, metadata, metrics, tags, prompt } = options
254
+ const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
255
255
 
256
256
  if (inputData || outputData) {
257
257
  if (spanKind === 'llm') {
@@ -271,9 +271,13 @@ class LLMObs extends NoopLLMObs {
271
271
  if (metrics) {
272
272
  this._tagger.tagMetrics(span, metrics)
273
273
  }
274
+ // Apply tags before costTags so costTags can reference tags from the same annotation.
274
275
  if (tags) {
275
276
  this._tagger.tagSpanTags(span, tags)
276
277
  }
278
+ if (costTags != null) {
279
+ this._tagger.tagCostTags(span, costTags, 'annotate')
280
+ }
277
281
  if (prompt) {
278
282
  this._tagger.tagPrompt(span, prompt)
279
283
  }
@@ -512,7 +516,7 @@ class LLMObs extends NoopLLMObs {
512
516
  flushCh.publish()
513
517
  }
514
518
 
515
- _autoAnnotate (span, kind, input, output) {
519
+ #autoAnnotate (span, kind, input, output) {
516
520
  const annotations = {}
517
521
  if (input && !['llm', 'embedding'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[INPUT_VALUE]) {
518
522
  annotations.inputData = input
@@ -530,7 +534,7 @@ class LLMObs extends NoopLLMObs {
530
534
  return store?.span
531
535
  }
532
536
 
533
- _activate (span, options, fn) {
537
+ #activate (span, options, fn) {
534
538
  const parentStore = storage.getStore()
535
539
  if (this.enabled) storage.enterWith({ ...parentStore, span })
536
540
 
@@ -563,22 +567,20 @@ class LLMObs extends NoopLLMObs {
563
567
  }
564
568
 
565
569
  // bind function to active LLMObs span
566
- _bind (fn) {
570
+ #bind (fn) {
567
571
  if (typeof fn !== 'function') return fn
568
572
 
569
573
  const llmobs = this
570
574
  const activeSpan = llmobs._active()
571
575
 
572
- const bound = function () {
573
- return llmobs._activate(activeSpan, null, () => {
574
- return fn.apply(this, arguments)
576
+ return function (...args) {
577
+ return llmobs.#activate(activeSpan, null, () => {
578
+ return fn.apply(this, args)
575
579
  })
576
580
  }
577
-
578
- return bound
579
581
  }
580
582
 
581
- _extractOptions (options) {
583
+ #extractOptions (options) {
582
584
  const {
583
585
  modelName,
584
586
  modelProvider,
@@ -14,6 +14,8 @@ const {
14
14
  MODEL_NAME,
15
15
  MODEL_PROVIDER,
16
16
  METADATA,
17
+ COST_TAGS,
18
+ TOOL_DEFINITIONS,
17
19
  INPUT_MESSAGES,
18
20
  INPUT_VALUE,
19
21
  INTEGRATION,
@@ -37,10 +39,16 @@ const telemetry = require('./telemetry')
37
39
  const LLMObsTagger = require('./tagger')
38
40
 
39
41
  class LLMObservabilitySpan {
40
- constructor () {
42
+ /**
43
+ * @param {string} kind span kind
44
+ */
45
+ constructor (kind) {
41
46
  this.input = []
42
47
  this.output = []
43
48
 
49
+ /** @type {string} */
50
+ this.kind = kind
51
+
44
52
  this._tags = {}
45
53
  }
46
54
 
@@ -113,7 +121,6 @@ class LLMObsSpanProcessor {
113
121
  }
114
122
 
115
123
  format (span) {
116
- const llmObsSpan = new LLMObservabilitySpan()
117
124
  let inputType, outputType
118
125
 
119
126
  const spanTags = span.context()._tags
@@ -130,15 +137,31 @@ class LLMObsSpanProcessor {
130
137
  meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
131
138
  }
132
139
 
133
- if (mlObsTags[METADATA]) {
134
- this.#addObject(mlObsTags[METADATA], meta.metadata = {})
140
+ if (mlObsTags[METADATA] || mlObsTags[COST_TAGS]) {
141
+ const metadata = {}
142
+ if (mlObsTags[METADATA]) this.#addObject(mlObsTags[METADATA], metadata)
143
+ // Only seed `metadata._dd` when there's something to put in it (currently cost_tags). Mirrors
144
+ // dd-trace-py and the cross-language wire format enforced by system-tests — metadata-only
145
+ // spans must not carry an empty `_dd: {}` block.
146
+ if (mlObsTags[COST_TAGS]) {
147
+ this.#getDdMetadata(metadata).cost_tags = mlObsTags[COST_TAGS]
148
+ }
149
+ meta.metadata = metadata
150
+ }
151
+
152
+ if (mlObsTags[TOOL_DEFINITIONS]) {
153
+ meta.tool_definitions = []
154
+ this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions)
135
155
  }
136
156
 
157
+ const llmObsSpan = new LLMObservabilitySpan(spanKind)
158
+
137
159
  if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
138
160
  llmObsSpan.input = mlObsTags[INPUT_MESSAGES]
139
161
  inputType = 'messages'
140
162
  } else if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
141
- input.documents = mlObsTags[INPUT_DOCUMENTS]
163
+ llmObsSpan.input = mlObsTags[INPUT_DOCUMENTS].map(doc => ({ content: doc.text, role: '' }))
164
+ inputType = 'documents'
142
165
  } else if (mlObsTags[INPUT_VALUE]) {
143
166
  llmObsSpan.input = [{ role: '', content: mlObsTags[INPUT_VALUE] }]
144
167
  inputType = 'value'
@@ -148,7 +171,8 @@ class LLMObsSpanProcessor {
148
171
  llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
149
172
  outputType = 'messages'
150
173
  } else if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
151
- output.documents = mlObsTags[OUTPUT_DOCUMENTS]
174
+ llmObsSpan.output = mlObsTags[OUTPUT_DOCUMENTS].map(doc => ({ content: doc.text, role: '' }))
175
+ outputType = 'documents'
152
176
  } else if (mlObsTags[OUTPUT_VALUE]) {
153
177
  llmObsSpan.output = [{ role: '', content: mlObsTags[OUTPUT_VALUE] }]
154
178
  outputType = 'value'
@@ -180,6 +204,11 @@ class LLMObsSpanProcessor {
180
204
  input.messages = processedSpan.input
181
205
  } else if (inputType === 'value') {
182
206
  input.value = processedSpan.input[0].content
207
+ } else if (inputType === 'documents') {
208
+ input.documents = processedSpan.input.map((processedDocument, processedDocumentIdx) => ({
209
+ ...mlObsTags[INPUT_DOCUMENTS][processedDocumentIdx],
210
+ text: processedDocument.content,
211
+ }))
183
212
  }
184
213
  }
185
214
 
@@ -188,6 +217,11 @@ class LLMObsSpanProcessor {
188
217
  output.messages = processedSpan.output
189
218
  } else if (outputType === 'value') {
190
219
  output.value = processedSpan.output[0].content
220
+ } else if (outputType === 'documents') {
221
+ output.documents = processedSpan.output.map((processedDocument, processedDocumentIdx) => ({
222
+ ...mlObsTags[OUTPUT_DOCUMENTS][processedDocumentIdx],
223
+ text: processedDocument.content,
224
+ }))
191
225
  }
192
226
  }
193
227
 
@@ -259,6 +293,18 @@ class LLMObsSpanProcessor {
259
293
  add(obj, carrier)
260
294
  }
261
295
 
296
+ /**
297
+ * Returns `metadata._dd`, normalizing it to a fresh object if missing or invalid.
298
+ * @param {Record<string, unknown>} metadata
299
+ * @returns {Record<string, unknown>}
300
+ */
301
+ #getDdMetadata (metadata) {
302
+ if (!metadata._dd || typeof metadata._dd !== 'object' || Array.isArray(metadata._dd)) {
303
+ metadata._dd = {}
304
+ }
305
+ return metadata._dd
306
+ }
307
+
262
308
  #getTags (span, mlApp, sessionId, error) {
263
309
  let tags = {
264
310
  ...this.#config.parsedDdTags,
@@ -12,7 +12,9 @@ const {
12
12
  INPUT_DOCUMENTS,
13
13
  OUTPUT_VALUE,
14
14
  METADATA,
15
+ COST_TAGS,
15
16
  METRICS,
17
+ TOOL_DEFINITIONS,
16
18
  PARENT_ID_KEY,
17
19
  INPUT_MESSAGES,
18
20
  OUTPUT_MESSAGES,
@@ -41,6 +43,7 @@ const {
41
43
  INSTRUMENTATION_METHOD_ANNOTATED,
42
44
  } = require('./constants/tags')
43
45
  const { storage } = require('./storage')
46
+ const { validateCostTags } = require('./util')
44
47
 
45
48
  // global registry of LLMObs spans
46
49
  // maps LLMObs spans to their annotations
@@ -120,6 +123,11 @@ class LLMObsTagger {
120
123
  const tags = annotationContext?.tags
121
124
  if (tags) this.tagSpanTags(span, tags)
122
125
 
126
+ // apply after tags so only keys present at span start are accepted.
127
+ if (annotationContext?.costTags != null) {
128
+ this.tagCostTags(span, annotationContext.costTags, 'annotation_context')
129
+ }
130
+
123
131
  // apply annotation context name
124
132
  const annotationContextName = annotationContext?.name
125
133
  if (annotationContextName) this._setTag(span, NAME, annotationContextName)
@@ -159,6 +167,14 @@ class LLMObsTagger {
159
167
  this.#tagText(span, outputData, OUTPUT_VALUE)
160
168
  }
161
169
 
170
+ tagToolDefinitions (span, toolDefinitions) {
171
+ if (Array.isArray(toolDefinitions) && toolDefinitions.length > 0) {
172
+ this._setTag(span, TOOL_DEFINITIONS, toolDefinitions)
173
+ } else {
174
+ this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
175
+ }
176
+ }
177
+
162
178
  tagMetadata (span, metadata) {
163
179
  const existingMetadata = registry.get(span)?.[METADATA]
164
180
  if (existingMetadata) {
@@ -225,6 +241,32 @@ class LLMObsTagger {
225
241
  }
226
242
  }
227
243
 
244
+ /**
245
+ * Validates and tags cost tag keys on an LLMObs span. Cost tag references are validated against
246
+ * the span's already-applied tags, which are read from the registry.
247
+ * @param {import('../opentracing/span')} span
248
+ * @param {unknown} costTags Raw user-provided cost tags; validated here.
249
+ * @param {'annotate' | 'annotation_context'} source
250
+ */
251
+ tagCostTags (span, costTags, source) {
252
+ const spanTags = registry.get(span)?.[TAGS] || {}
253
+ const validatedCostTags = validateCostTags(span, costTags, source, spanTags)
254
+ if (!validatedCostTags.length) return
255
+
256
+ // Might consider switching to a `Set` if per-span cost tag cardinality grows large enough that
257
+ // this `.includes`/`.push` merge becomes a hot spot
258
+ const currentCostTags = registry.get(span)?.[COST_TAGS]
259
+ if (currentCostTags) {
260
+ for (const costTag of validatedCostTags) {
261
+ if (!currentCostTags.includes(costTag)) {
262
+ currentCostTags.push(costTag)
263
+ }
264
+ }
265
+ } else {
266
+ this._setTag(span, COST_TAGS, validatedCostTags)
267
+ }
268
+ }
269
+
228
270
  /**
229
271
  * Tags a prompt on an LLMObs span.
230
272
  * @param {import('../opentracing/span')} span
@@ -6,6 +6,7 @@ const telemetryMetrics = require('../telemetry/metrics')
6
6
  const {
7
7
  SPAN_KIND,
8
8
  MODEL_PROVIDER,
9
+ ML_APP,
9
10
  PARENT_ID_KEY,
10
11
  SESSION_ID,
11
12
  ROOT_PARENT_ID,
@@ -123,6 +124,32 @@ function recordLLMObsAnnotate (span, err, value = 1) {
123
124
  llmobsMetrics.count('annotations', tags).inc(value)
124
125
  }
125
126
 
127
+ function recordCostTagsAnnotated (span, source, value = 1) {
128
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
129
+ const tags = {
130
+ span_kind: mlObsTags[SPAN_KIND] || 'N/A',
131
+ source,
132
+ ml_app: mlObsTags[ML_APP] || 'N/A',
133
+ model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
134
+ }
135
+
136
+ llmobsMetrics.count('cost_tags.annotated', tags).inc(value)
137
+ }
138
+
139
+ function recordCostTagsSubmitted (span, count, source, state, reason = 'none') {
140
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
141
+ const tags = {
142
+ span_kind: mlObsTags[SPAN_KIND] || 'N/A',
143
+ source,
144
+ ml_app: mlObsTags[ML_APP] || 'N/A',
145
+ model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
146
+ state,
147
+ reason,
148
+ }
149
+
150
+ llmobsMetrics.count('cost_tags.submitted', tags).inc(count)
151
+ }
152
+
126
153
  function recordUserFlush (err, value = 1) {
127
154
  const tags = { error: Number(!!err) }
128
155
  if (err) tags.error_type = err
@@ -167,6 +194,8 @@ module.exports = {
167
194
  recordLLMObsSpanSize,
168
195
  recordDroppedPayload,
169
196
  recordLLMObsAnnotate,
197
+ recordCostTagsAnnotated,
198
+ recordCostTagsSubmitted,
170
199
  recordUserFlush,
171
200
  recordExportSpan,
172
201
  recordSubmitEvaluation,
@@ -1,14 +1,26 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../log')
3
4
  const { SPAN_KINDS } = require('./constants/tags')
4
5
 
6
+ // LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once
7
+ // looking for the first non-ASCII char; if there is none, hand the input
8
+ // straight back. Otherwise pick up the slow path from the byte that needed
9
+ // escaping. ~5x faster on typical prompt strings than the per-char `+=`
10
+ // loop the function used to do unconditionally.
5
11
  function encodeUnicode (str = '') {
6
- let result = ''
7
- for (let i = 0; i < str.length; i++) {
8
- const code = str.charCodeAt(i)
9
- result += code > 127 ? String.raw`\u${code.toString(16).padStart(4, '0')}` : str[i]
12
+ for (let index = 0; index < str.length; index++) {
13
+ if (str.charCodeAt(index) > 127) {
14
+ let result = str.slice(0, index)
15
+ // eslint-disable-next-line sonarjs/updated-loop-counter -- inner loop continues from outer position
16
+ for (; index < str.length; index++) {
17
+ const code = str.charCodeAt(index)
18
+ result += code > 127 ? String.raw`\u${code.toString(16).padStart(4, '0')}` : str[index]
19
+ }
20
+ return result
21
+ }
10
22
  }
11
- return result
23
+ return str
12
24
  }
13
25
 
14
26
  function validateKind (kind) {
@@ -22,6 +34,57 @@ function validateKind (kind) {
22
34
  return kind
23
35
  }
24
36
 
37
+ /**
38
+ * Validates cost tag keys and records telemetry for the annotation source.
39
+ * @param {import('../opentracing/span')} span
40
+ * @param {unknown} costTags
41
+ * @param {string} source
42
+ * @param {Record<string, unknown>} spanTags
43
+ * @returns {string[]}
44
+ */
45
+ function validateCostTags (span, costTags, source, spanTags) {
46
+ // Lazy-required to avoid the `index.js -> telemetry -> tagger -> util` module cycle.
47
+ const telemetry = require('./telemetry')
48
+
49
+ telemetry.recordCostTagsAnnotated(span, source)
50
+
51
+ if (!Array.isArray(costTags)) {
52
+ log.warn('costTags must be an array of strings. Ignoring value.')
53
+ telemetry.recordCostTagsSubmitted(span, 1, source, 'error', 'non_list')
54
+ return []
55
+ }
56
+
57
+ const validatedCostTags = new Set()
58
+ let nonStringEntries = 0
59
+ let missingSpanTags = 0
60
+
61
+ for (const costTag of costTags) {
62
+ if (typeof costTag !== 'string') {
63
+ log.warn('costTags entries must be strings. Skipping entry %s.', costTag)
64
+ nonStringEntries++
65
+ continue
66
+ }
67
+ if (!Object.hasOwn(spanTags, costTag)) {
68
+ log.warn('costTags entry "%s" must reference a key present in span tags. Skipping entry.', costTag)
69
+ missingSpanTags++
70
+ continue
71
+ }
72
+ validatedCostTags.add(costTag)
73
+ }
74
+
75
+ if (nonStringEntries) {
76
+ telemetry.recordCostTagsSubmitted(span, nonStringEntries, source, 'error', 'non_string_entry')
77
+ }
78
+ if (missingSpanTags) {
79
+ telemetry.recordCostTagsSubmitted(span, missingSpanTags, source, 'error', 'missing_span_tag')
80
+ }
81
+ if (validatedCostTags.size) {
82
+ telemetry.recordCostTagsSubmitted(span, validatedCostTags.size, source, 'success')
83
+ }
84
+
85
+ return [...validatedCostTags]
86
+ }
87
+
25
88
  // extracts the argument names from a function string
26
89
  function parseArgumentNames (str) {
27
90
  const result = []
@@ -174,9 +237,22 @@ function spanHasError (span) {
174
237
  return !!(tags.error || tags['error.type'])
175
238
  }
176
239
 
240
+ // LLM SDKs stream tool-call argument JSON across SSE chunks; a malformed
241
+ // accumulation would otherwise throw straight into the chunk subscriber.
242
+ function safeJsonParse (value, fallback) {
243
+ if (typeof value !== 'string') return value
244
+ try {
245
+ return JSON.parse(value)
246
+ } catch {
247
+ return fallback === undefined ? value : fallback
248
+ }
249
+ }
250
+
177
251
  module.exports = {
178
252
  encodeUnicode,
253
+ validateCostTags,
179
254
  validateKind,
180
255
  getFunctionArguments,
256
+ safeJsonParse,
181
257
  spanHasError,
182
258
  }