dd-trace 5.132.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 (231) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
  16. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  17. package/index.d.ts +41 -19
  18. package/initialize.mjs +1 -3
  19. package/openfeature/rules-based.d.ts +11 -0
  20. package/openfeature/rules-based.js +7 -0
  21. package/package.json +25 -22
  22. package/packages/datadog-core/src/storage.js +16 -0
  23. package/packages/datadog-esbuild/index.js +18 -12
  24. package/packages/datadog-instrumentations/index.js +2 -1
  25. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  26. package/packages/datadog-instrumentations/src/ai.js +3 -3
  27. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  28. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  29. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  30. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  31. package/packages/datadog-instrumentations/src/cucumber.js +65 -41
  32. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  33. package/packages/datadog-instrumentations/src/electron.js +6 -6
  34. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  35. package/packages/datadog-instrumentations/src/fs.js +2 -2
  36. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  37. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  38. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +28 -9
  39. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  40. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  41. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  42. package/packages/datadog-instrumentations/src/helpers/register.js +45 -1
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +89 -6
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
  50. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
  51. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  52. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  53. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  54. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  55. package/packages/datadog-instrumentations/src/jest.js +72 -67
  56. package/packages/datadog-instrumentations/src/knex.js +9 -5
  57. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  58. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  59. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  60. package/packages/datadog-instrumentations/src/mocha/utils.js +38 -4
  61. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
  62. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  63. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  64. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  65. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  66. package/packages/datadog-instrumentations/src/nova-sonic/session.js +492 -0
  67. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  68. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  69. package/packages/datadog-instrumentations/src/openai.js +7 -0
  70. package/packages/datadog-instrumentations/src/pino.js +5 -6
  71. package/packages/datadog-instrumentations/src/playwright.js +2 -0
  72. package/packages/datadog-instrumentations/src/redis.js +27 -13
  73. package/packages/datadog-instrumentations/src/router.js +30 -0
  74. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  75. package/packages/datadog-instrumentations/src/stripe.js +23 -12
  76. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +8 -5
  77. package/packages/datadog-instrumentations/src/vitest-main.js +16 -18
  78. package/packages/datadog-instrumentations/src/vitest-util.js +7 -4
  79. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  80. package/packages/datadog-instrumentations/src/vitest.js +9 -1
  81. package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
  82. package/packages/datadog-instrumentations/src/ws.js +1 -4
  83. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  84. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  85. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  86. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  87. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  89. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  90. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  91. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  92. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  93. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  94. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +4 -5
  95. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  96. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  97. package/packages/datadog-plugin-cypress/src/support.js +3 -4
  98. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  99. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  100. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  101. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  102. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  103. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  104. package/packages/datadog-plugin-mocha/src/index.js +7 -10
  105. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
  106. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  107. package/packages/datadog-plugin-next/src/index.js +1 -2
  108. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  109. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  110. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  111. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  112. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  113. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  114. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  115. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  116. package/packages/datadog-webpack/index.js +55 -43
  117. package/packages/datadog-webpack/src/loader.js +56 -8
  118. package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
  119. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  120. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  121. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  123. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  124. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  127. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  128. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  129. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
  130. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  131. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  132. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  133. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  134. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  135. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  136. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  137. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  138. package/packages/dd-trace/src/appsec/stack_trace.js +4 -4
  139. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  140. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
  141. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  142. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  143. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  144. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  145. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  146. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  147. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  148. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  149. package/packages/dd-trace/src/config/defaults.js +5 -5
  150. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  151. package/packages/dd-trace/src/config/helper.js +34 -3
  152. package/packages/dd-trace/src/config/index.js +55 -9
  153. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  154. package/packages/dd-trace/src/config/parsers.js +2 -2
  155. package/packages/dd-trace/src/config/supported-configurations.json +44 -0
  156. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  157. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  158. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
  159. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
  160. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
  161. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
  162. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  163. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  164. package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
  165. package/packages/dd-trace/src/dogstatsd.js +1 -1
  166. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  167. package/packages/dd-trace/src/exporters/common/request.js +1 -5
  168. package/packages/dd-trace/src/id.js +14 -1
  169. package/packages/dd-trace/src/lambda/handler.js +179 -19
  170. package/packages/dd-trace/src/lambda/index.js +15 -1
  171. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  172. package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
  173. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  174. package/packages/dd-trace/src/llmobs/constants/tags.js +15 -0
  175. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  176. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  177. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  178. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  180. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  181. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
  182. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +64 -2
  183. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  184. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  185. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  186. package/packages/dd-trace/src/llmobs/sdk.js +15 -4
  187. package/packages/dd-trace/src/llmobs/span_processor.js +13 -2
  188. package/packages/dd-trace/src/llmobs/tagger.js +20 -10
  189. package/packages/dd-trace/src/llmobs/util.js +11 -6
  190. package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
  191. package/packages/dd-trace/src/log/log.js +1 -5
  192. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  193. package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
  194. package/packages/dd-trace/src/openfeature/index.js +6 -0
  195. package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
  196. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  199. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  200. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +24 -24
  201. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  202. package/packages/dd-trace/src/otel-sampling.js +4 -5
  203. package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
  204. package/packages/dd-trace/src/plugin_manager.js +1 -2
  205. package/packages/dd-trace/src/plugins/database.js +7 -16
  206. package/packages/dd-trace/src/plugins/tracing.js +0 -1
  207. package/packages/dd-trace/src/plugins/util/git.js +99 -59
  208. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  209. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
  210. package/packages/dd-trace/src/plugins/util/test.js +50 -4
  211. package/packages/dd-trace/src/plugins/util/web.js +90 -20
  212. package/packages/dd-trace/src/priority_sampler.js +1 -2
  213. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  214. package/packages/dd-trace/src/proxy.js +1 -2
  215. package/packages/dd-trace/src/ritm.js +4 -2
  216. package/packages/dd-trace/src/serverless.js +5 -0
  217. package/packages/dd-trace/src/span_processor.js +13 -17
  218. package/packages/dd-trace/src/span_stats.js +4 -1
  219. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  220. package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
  221. package/packages/dd-trace/src/util.js +37 -4
  222. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  223. package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
  224. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  225. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  226. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  227. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  228. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  229. package/vendor/dist/protobufjs/index.js +1 -1
  230. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  231. package/vendor/dist/shell-quote/index.js +1 -1
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const { pcm16ToWav } = require('../audio-codec')
4
+ const { fitsInlineAudioBudget, formatAudioPartWithGuard } = require('../audio-utils')
5
+ const { AUDIO_FALLBACK, LLMOBS_AUDIO_INLINE_MAX_BYTES } = require('../constants/audio')
6
+ const { storage } = require('../storage')
7
+ const LLMObsPlugin = require('./base')
8
+
9
+ class SonicLLMObsPlugin extends LLMObsPlugin {
10
+ static id = 'bedrockruntime_sonic_llmobs'
11
+ static integration = 'bedrock'
12
+ static system = 'aws.bedrock'
13
+ static prefix = 'tracing:apm:aws:bedrockruntime:sonic:span'
14
+ static emitsGenAiApmTags = false
15
+
16
+ constructor (...args) {
17
+ super(...args)
18
+ this.addSub('dd-trace:aws:bedrockruntime:sonic:capture-context', context => {
19
+ if (!this._llmobsEnabled) return
20
+ context.enabled = true
21
+ // Capture once at invocation, even when absent or already finished. Scoped replay restores
22
+ // the consumer's context on every exit, including exceptions, without repairing IDs later.
23
+ const store = storage.getStore()
24
+ const previous = context.runInContext ?? (fn => fn())
25
+ context.runInContext = fn => previous(() => storage.run(store, fn))
26
+ })
27
+ }
28
+
29
+ /** @param {object} ctx */
30
+ getLLMObsSpanRegisterOptions (ctx) {
31
+ const options = {
32
+ kind: ctx.kind,
33
+ name: ctx.name,
34
+ sessionId: ctx.descriptor.sessionId,
35
+ }
36
+ if (ctx.kind === 'llm') {
37
+ options.modelName = ctx.model
38
+ options.modelProvider = 'amazon'
39
+ }
40
+ return options
41
+ }
42
+
43
+ /** @param {object} ctx */
44
+ start (ctx) {
45
+ super.start(ctx)
46
+ // Replay already has the complete descriptor. Annotate before any end subscriber can finish
47
+ // the span, including when disabling/re-enabling LLMObs changes subscriber registration order.
48
+ if (this._llmobsEnabledFor(ctx)) this.setLLMObsTags(ctx)
49
+ }
50
+
51
+ /** @param {object} ctx */
52
+ setLLMObsTags (ctx) {
53
+ const span = ctx.currentStore?.span
54
+ if (!span) return
55
+ const { turn, history, metadata } = ctx.descriptor
56
+ const text = turn.finalText || turn.speculativeText
57
+ if (ctx.kind !== 'llm') {
58
+ if (ctx.name === 'nova sonic audio turn') this._tagger.tagTextIO(span, turn.userText, text)
59
+ return
60
+ }
61
+
62
+ const input = { role: 'user', content: turn.userText || (turn.inputAudioObserved ? AUDIO_FALLBACK : '') }
63
+ const output = { role: 'assistant', content: text || (turn.outputAudioObserved ? AUDIO_FALLBACK : '') }
64
+ if (turn.tools.length) output.toolCalls = turn.tools
65
+ const inputs = [...history, input, ...turn.toolResults]
66
+ const outputs = [output]
67
+ // Reserve the serialized text/tools and metadata before spending one budget across both WAVs.
68
+ let budget = Math.max(0, LLMOBS_AUDIO_INLINE_MAX_BYTES -
69
+ Buffer.byteLength(JSON.stringify([inputs, outputs, metadata])) - 256)
70
+ for (const [message, pcm, rate] of [
71
+ [input, turn.inputPcm, turn.inputRate],
72
+ [output, turn.output.pcm(), turn.output.rate],
73
+ ]) {
74
+ if (!pcm?.length || !rate) continue
75
+ const part = fitsInlineAudioBudget(pcm.length + 44, budget)
76
+ ? formatAudioPartWithGuard(pcm16ToWav(pcm, rate), 'audio/wav', budget)
77
+ : undefined
78
+ if (part) {
79
+ message.audioParts = [part]
80
+ budget -= part.content.length
81
+ } else if (message === output) {
82
+ metadata.output_audio_omitted_reason = 'payload_limit'
83
+ }
84
+ }
85
+ if (turn.output.omittedReason) metadata.output_audio_omitted_reason = turn.output.omittedReason
86
+ this._tagger.tagLLMIO(span, inputs, outputs)
87
+ this._tagger.tagMetadata(span, metadata)
88
+ this._tagger.tagMetrics(span, turn.metrics)
89
+ }
90
+ }
91
+
92
+ module.exports = SonicLLMObsPlugin
@@ -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 } = 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,
@@ -16,6 +22,7 @@ const {
16
22
  extractContentParts,
17
23
  hasMultimodalInputs,
18
24
  getModelProviderAndClient,
25
+ getServerToolUsageMetrics,
19
26
  } = require('./utils')
20
27
 
21
28
  const allowedParamKeys = new Set([
@@ -107,6 +114,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
107
114
  this._tagEmbedding(span, inputs, response, error)
108
115
  } else if (operation === 'response') {
109
116
  this.#tagResponse(span, inputs, response, error)
117
+ } else if (operation === 'decision') {
118
+ this.#tagDecision(span, inputs, response, error)
110
119
  }
111
120
 
112
121
  if (!error) {
@@ -148,12 +157,20 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
148
157
  // Responses API - always include
149
158
  const cacheReadTokens = tokenUsage.input_tokens_details.cached_tokens
150
159
  if (cacheReadTokens !== undefined) metrics.cacheReadTokens = cacheReadTokens
160
+ const cacheWriteTokens = tokenUsage.input_tokens_details.cache_write_tokens
161
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
162
+ // Cache writes are billed at a different rate per TTL
163
+ const cacheWriteDetails = tokenUsage.input_tokens_details.cache_write_tokens_details
164
+ if (cacheWriteDetails?.ttl_30m_tokens != null) metrics.cacheWrite30mTokens = cacheWriteDetails.ttl_30m_tokens
165
+ if (cacheWriteDetails?.ttl_12h_tokens != null) metrics.cacheWrite12hTokens = cacheWriteDetails.ttl_12h_tokens
151
166
  } else if (tokenUsage.prompt_tokens_details) {
152
167
  // Chat/Completions API - only include if > 0
153
168
  const cacheReadTokens = tokenUsage.prompt_tokens_details.cached_tokens
154
169
  if (cacheReadTokens != null) {
155
170
  metrics.cacheReadTokens = cacheReadTokens
156
171
  }
172
+ const cacheWriteTokens = tokenUsage.prompt_tokens_details.cache_write_tokens
173
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
157
174
  }
158
175
  // Reasoning tokens - Responses API returns `output_tokens_details`, `completion_tokens_details`
159
176
  const reasoningOutputObject = tokenUsage.output_tokens_details ?? tokenUsage.completion_tokens_details
@@ -161,6 +178,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
161
178
  if (reasoningOutputTokens !== undefined) metrics.reasoningOutputTokens = reasoningOutputTokens
162
179
  }
163
180
 
181
+ Object.assign(metrics, getServerToolUsageMetrics(response))
182
+
164
183
  return metrics
165
184
  }
166
185
 
@@ -464,6 +483,32 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
464
483
 
465
484
  this._tagger.tagMetadata(span, outputMetadata) // update the metadata with the output metadata
466
485
  }
486
+
487
+ #tagDecision (span, inputs, response, error) {
488
+ this._tagger.tagSpanTags(span, { [DECISION_PROVIDER_TAG]: DECISION_PROVIDER })
489
+
490
+ const { input, questions } = inputs
491
+
492
+ const inputMessages = typeof input === 'string'
493
+ ? [{ role: 'user', content: input }]
494
+ : (Array.isArray(input) ? input : []).map(message => ({
495
+ role: message.role ?? 'user',
496
+ content: decisionMessageContent(message.content),
497
+ }))
498
+
499
+ if (Array.isArray(questions)) {
500
+ this._tagger.tagMetadata(span, { questions })
501
+ }
502
+
503
+ if (error) {
504
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
505
+ return
506
+ }
507
+
508
+ this._tagger.tagLLMIO(span, inputMessages, [
509
+ { role: 'assistant', content: JSON.stringify(response.answers ?? []) },
510
+ ])
511
+ }
467
512
  }
468
513
 
469
514
  // TODO: this will be moved to the APM integration
@@ -485,13 +530,28 @@ function normalizeOpenAIResourceName (resource) {
485
530
  case 'responses.create':
486
531
  return 'createResponse'
487
532
 
533
+ // decisions
534
+ case 'decisions.create':
535
+ return 'createDecision'
536
+
488
537
  default:
489
538
  return resource
490
539
  }
491
540
  }
492
541
 
542
+ // Decision images must be inline data URLs, so they are replaced with a marker instead of copied onto the span
543
+ function decisionMessageContent (content) {
544
+ if (!Array.isArray(content)) return content ?? ''
545
+
546
+ return content
547
+ .map(part => (part?.type === INPUT_TYPE_IMAGE ? IMAGE_FALLBACK : part?.text ?? ''))
548
+ .filter(Boolean)
549
+ .join('\n')
550
+ }
551
+
493
552
  function gateResource (resource) {
494
- return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse'].includes(resource)
553
+ return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse', 'createDecision']
554
+ .includes(resource)
495
555
  ? resource
496
556
  : undefined
497
557
  }
@@ -506,6 +566,8 @@ function getOperation (resource) {
506
566
  return 'embedding'
507
567
  case 'createResponse':
508
568
  return 'response'
569
+ case 'createDecision':
570
+ return 'decision'
509
571
  default:
510
572
  // should never happen
511
573
  return 'unknown'
@@ -198,6 +198,43 @@ function getModelProviderAndClient (baseUrl = '') {
198
198
  return { modelProvider, client: 'OpenAI' }
199
199
  }
200
200
 
201
+ /**
202
+ * Count the billable server-side tool calls in an OpenAI Responses response.
203
+ *
204
+ * Web searches are read from the undocumented `tool_usage.web_search.num_requests` when present, otherwise
205
+ * counted from completed `web_search_call` items whose action is `search` (`open_page` / `find_in_page` steps are
206
+ * not billed). File searches have no usage counter, so each completed `file_search_call` item counts once.
207
+ * Zero counts are omitted.
208
+ *
209
+ * @param {Record<string, unknown>} [response] - Any OpenAI response; only Responses API fields are read
210
+ * @returns {{ webSearchCount?: number, storageSearchCount?: number }}
211
+ */
212
+ function getServerToolUsageMetrics (response) {
213
+ const metrics = {}
214
+ const reportedWebSearches = response?.tool_usage?.web_search?.num_requests
215
+ const hasReportedWebSearches = typeof reportedWebSearches === 'number'
216
+ if (hasReportedWebSearches && reportedWebSearches > 0) metrics.webSearchCount = reportedWebSearches
217
+
218
+ const output = response?.output
219
+ if (!Array.isArray(output)) return metrics
220
+
221
+ for (const item of output) {
222
+ if (item?.status !== 'completed') continue
223
+
224
+ if (item.type === 'web_search_call') {
225
+ if (hasReportedWebSearches) continue
226
+ // Items without an action (older payloads) are assumed to be searches.
227
+ const actionType = item.action?.type
228
+ if (actionType !== undefined && actionType !== 'search') continue
229
+ metrics.webSearchCount = (metrics.webSearchCount ?? 0) + 1
230
+ } else if (item.type === 'file_search_call') {
231
+ metrics.storageSearchCount = (metrics.storageSearchCount ?? 0) + 1
232
+ }
233
+ }
234
+
235
+ return metrics
236
+ }
237
+
201
238
  module.exports = {
202
239
  extractChatTemplateFromInstructions,
203
240
  normalizePromptVariables,
@@ -206,4 +243,5 @@ module.exports = {
206
243
  hasMultimodalInputs,
207
244
  getModelProviderAndClient,
208
245
  getOpenAIModelProvider,
246
+ getServerToolUsageMetrics,
209
247
  }
@@ -84,13 +84,11 @@ function promptFromData (data, source) {
84
84
  }
85
85
 
86
86
  /**
87
- * Copy a prompt with a different source.
88
87
  * @param {ManagedPrompt} prompt
89
- * @param {'registry'|'cache'|'fallback'|'ff'|'resolve'} source
90
88
  */
91
- function withSource (prompt, source) {
92
- if (source === prompt.source) return prompt
93
- return new ManagedPrompt({ ...prompt, source })
89
+ function withCacheSource (prompt) {
90
+ if (SOURCE_CACHE === prompt.source) return prompt
91
+ return new ManagedPrompt({ ...prompt, source: /** @type {ManagedPrompt['source']} */ (SOURCE_CACHE) })
94
92
  }
95
93
 
96
94
  function detailFromBody (body) {
@@ -310,7 +308,7 @@ class PromptManager {
310
308
  const cacheable = generationAtStart === this.cacheGeneration && latest
311
309
  if (result.prompt) {
312
310
  if (cacheable) {
313
- const cached = withSource(result.prompt, SOURCE_CACHE)
311
+ const cached = withCacheSource(result.prompt)
314
312
  if (hot) this.hotCache.set(request.key, cached)
315
313
  if (!request.resolve) this.warmCache.set(request.key, cached)
316
314
  }
@@ -335,7 +333,7 @@ class PromptManager {
335
333
  async #backgroundFetch (request, signal) {
336
334
  const result = await this.#fetchAndCache(request, { hot: false, signal })
337
335
  if (!result.cacheable) return
338
- if (result.prompt) return withSource(result.prompt, SOURCE_CACHE)
336
+ if (result.prompt) return withCacheSource(result.prompt)
339
337
  if (result.notFound) return
340
338
  throw new Error(result.reason)
341
339
  }
@@ -93,7 +93,7 @@ class ManagedPrompt {
93
93
  format (variables = {}) {
94
94
  if (typeof this.template === 'string') return render(this.template, variables)
95
95
  return this.template.flatMap(item => {
96
- if (!isPlaceholder(item)) return [{ ...item, content: render(item.content, variables) }]
96
+ if (!isPlaceholder(item)) return { ...item, content: render(item.content, variables) }
97
97
  if (!Object.hasOwn(variables, item.name)) {
98
98
  throw new TypeError(`Missing message placeholder variable '${item.name}'`)
99
99
  }
@@ -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
  }
@@ -16,6 +16,8 @@ const {
16
16
  METADATA,
17
17
  COST_TAGS,
18
18
  AGENT_MANIFEST,
19
+ AGENT_VERSION,
20
+ AGENT_VERSION_TAG_KEY,
19
21
  TOOL_DEFINITIONS,
20
22
  EXPERIMENT_INPUT,
21
23
  EXPERIMENT_OUTPUT,
@@ -32,6 +34,7 @@ const {
32
34
  PARENT_ID_KEY,
33
35
  PARENT_AGENT_NAME,
34
36
  PARENT_AGENT_SPAN_ID,
37
+ PARENT_AGENT_VERSION,
35
38
  SESSION_ID,
36
39
  NAME,
37
40
  INPUT_PROMPT,
@@ -92,9 +95,8 @@ class LLMObsSpanProcessor {
92
95
 
93
96
  // TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
94
97
  process (span) {
95
- if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
96
98
  // if the span is not in our private tagger map, it is not an llmobs span
97
- if (!LLMObsTagger.tagMap.has(span)) return
99
+ if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !LLMObsTagger.tagMap.has(span)) return
98
100
 
99
101
  try {
100
102
  this.#setGenAiApmTags(span)
@@ -407,6 +409,15 @@ class LLMObsSpanProcessor {
407
409
  const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
408
410
  if (existingTags) tags = { ...tags, ...existingTags }
409
411
 
412
+ const mlObsTags = LLMObsTagger.tagMap.get(span)
413
+ // Resolved here because a span can become an agent after registration. An agent without its own version, and
414
+ // any other span, reports the version inherited at registration. Either wins over a user
415
+ // tag of the same name.
416
+ const agentVersion = mlObsTags?.[SPAN_KIND] === 'agent'
417
+ ? mlObsTags[AGENT_VERSION] || mlObsTags[PARENT_AGENT_VERSION]
418
+ : mlObsTags?.[PARENT_AGENT_VERSION]
419
+ if (agentVersion) tags[AGENT_VERSION_TAG_KEY] = agentVersion
420
+
410
421
  return tags
411
422
  }
412
423
 
@@ -19,6 +19,7 @@ const {
19
19
  METADATA,
20
20
  COST_TAGS,
21
21
  AGENT_MANIFEST,
22
+ AGENT_VERSION,
22
23
  METRICS,
23
24
  TOOL_DEFINITIONS,
24
25
  PARENT_ID_KEY,
@@ -28,6 +29,7 @@ const {
28
29
  NAME,
29
30
  PARENT_AGENT_NAME,
30
31
  PARENT_AGENT_SPAN_ID,
32
+ PARENT_AGENT_VERSION,
31
33
  PROPAGATED_PARENT_ID_KEY,
32
34
  PROPAGATED_PARENT_AGENT_ID_KEY,
33
35
  PROPAGATED_PARENT_AGENT_NAME_KEY,
@@ -116,10 +118,11 @@ class LLMObsTagger {
116
118
  kind,
117
119
  name,
118
120
  integration,
121
+ agentVersion,
119
122
  _decorator,
120
123
  } = {}) {
121
- if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
122
- if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
124
+ // Do not register it in the map if it does not have an LLMObs span kind.
125
+ if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !kind) return
123
126
 
124
127
  const spanMlApp =
125
128
  mlApp ||
@@ -193,6 +196,8 @@ class LLMObsTagger {
193
196
  storage.getStore()?.agentDeclarations
194
197
  )
195
198
  if (agentDeclarations) this.#applyAgentDeclarations(span, agentDeclarations)
199
+ // The span's own version wins over the enclosing contexts.
200
+ if (agentVersion) this._setTag(span, AGENT_VERSION, agentVersion)
196
201
 
197
202
  // apply annotation context name
198
203
  const annotationContextName = annotationContext?.name
@@ -213,15 +218,16 @@ class LLMObsTagger {
213
218
 
214
219
  /**
215
220
  * Applies the agents declared by the enclosing annotation contexts, outermost first, to every span in the block.
216
- * The manifest is emitted only if the span is an agent when it finishes, since some integrations promote a span
217
- * to an agent after registration.
221
+ * Both are emitted only if the span is an agent when it finishes, since some integrations promote a span to an
222
+ * agent after registration.
218
223
  *
219
224
  * @param {import('../opentracing/span')} span
220
225
  * @param {import('./agent-manifest').AgentDeclaration[]} declarations
221
226
  */
222
227
  #applyAgentDeclarations (span, declarations) {
223
228
  for (const declaration of declarations) {
224
- this.#tagAgentManifestFields(span, declaration.manifest)
229
+ if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
230
+ if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
225
231
  }
226
232
  }
227
233
 
@@ -233,7 +239,9 @@ class LLMObsTagger {
233
239
  */
234
240
  tagAgent (span, agent) {
235
241
  const declaration = buildAgentDeclaration(agent)
236
- if (declaration) this.#tagAgentManifestFields(span, declaration.manifest)
242
+ if (!declaration) return
243
+ if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
244
+ if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
237
245
  }
238
246
 
239
247
  /**
@@ -276,8 +284,9 @@ class LLMObsTagger {
276
284
 
277
285
  /**
278
286
  * Store the nearest agent ancestor on the span so it can be surfaced as
279
- * `meta.agent_attribution` at finish. Resolved once here, at registration, so downstream
280
- * children inherit it with a single lookup rather than walking the ancestor chain.
287
+ * `meta.agent_attribution` at finish, along with that agent's version, which the agent panel reads
288
+ * off each span to attribute its spend. Resolved once here, at registration, so downstream children
289
+ * inherit it with a single lookup rather than walking the ancestor chain.
281
290
  *
282
291
  * @param {import('../opentracing/span')} span
283
292
  * @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
@@ -285,10 +294,10 @@ class LLMObsTagger {
285
294
  // TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
286
295
  // sub-agents) will not retroactively update already-finished children's attribution. Follow up.
287
296
  #tagAgentAttribution (span, parent) {
288
- let name, spanId
297
+ let name, spanId, version
289
298
  if (registry.has(parent)) {
290
299
  // Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
291
- ({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
300
+ ({ name, spanId, version } = resolveAgentAttribution(registry.get(parent), parent))
292
301
  } else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
293
302
  // Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
294
303
  // name may be absent when the upstream hop ran an older SDK or its name was not
@@ -300,6 +309,7 @@ class LLMObsTagger {
300
309
 
301
310
  if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
302
311
  if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
312
+ if (version) this._setTag(span, PARENT_AGENT_VERSION, version)
303
313
  }
304
314
 
305
315
  // TODO: similarly for the following `tag` methods,
@@ -8,8 +8,10 @@ const {
8
8
  SPAN_KINDS,
9
9
  SPAN_KIND,
10
10
  NAME,
11
+ AGENT_VERSION,
11
12
  PARENT_AGENT_NAME,
12
13
  PARENT_AGENT_SPAN_ID,
14
+ PARENT_AGENT_VERSION,
13
15
  } = require('./constants/tags')
14
16
 
15
17
  const DECIMAL_TRACE_ID_REGEX = /^\d+$/
@@ -260,8 +262,7 @@ function findArgumentsBounds (str) {
260
262
 
261
263
  const memo = new WeakMap()
262
264
  function getFunctionArguments (fn, args = []) {
263
- if (!fn) return
264
- if (!args.length) return
265
+ if (!fn || !args.length) return
265
266
  if (args.length === 1) return args[0]
266
267
 
267
268
  try {
@@ -326,14 +327,18 @@ function agentNameWireSafe (name) {
326
327
  *
327
328
  * @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
328
329
  * @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
329
- * @returns {{ name: string | undefined, spanId: string | undefined }}
330
+ * @returns {{ name: string | undefined, spanId: string | undefined, version: string | undefined }}
330
331
  */
331
332
  function resolveAgentAttribution (tags, span) {
332
- if (!tags) return { name: undefined, spanId: undefined }
333
+ if (!tags) return { name: undefined, spanId: undefined, version: undefined }
333
334
  if (tags[SPAN_KIND] === 'agent') {
334
- return { name: tags[NAME] || span._name, spanId: span.context().toSpanId() }
335
+ return {
336
+ name: tags[NAME] || span._name,
337
+ spanId: span.context().toSpanId(),
338
+ version: tags[AGENT_VERSION] || tags[PARENT_AGENT_VERSION],
339
+ }
335
340
  }
336
- return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
341
+ return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID], version: tags[PARENT_AGENT_VERSION] }
337
342
  }
338
343
 
339
344
  function spanHasError (span) {
@@ -23,13 +23,12 @@ class LLMObsBuffer {
23
23
  /**
24
24
  * @param {{
25
25
  * events: Record<string, unknown>[],
26
- * size: number,
27
26
  * routing?: { apiKey?: string, site?: string }
28
27
  * }} options
29
28
  */
30
- constructor ({ events, size, routing = {} }) {
29
+ constructor ({ events, routing = {} }) {
31
30
  this.events = events
32
- this.size = size
31
+ this.size = 0
33
32
  this.routing = routing
34
33
  }
35
34
 
@@ -53,7 +52,7 @@ class BaseLLMObsWriter {
53
52
  this._eventType = eventType
54
53
 
55
54
  /** @type {LLMObsBuffer} */
56
- this._buffer = new LLMObsBuffer({ events: [], size: 0 })
55
+ this._buffer = new LLMObsBuffer({ events: [] })
57
56
 
58
57
  /** @type {import('../../config/config-base')} */
59
58
  this._config = config
@@ -92,7 +91,7 @@ class BaseLLMObsWriter {
92
91
  const apiKey = routing.apiKey
93
92
  let buffer = this.#multiTenantBuffers.get(apiKey)
94
93
  if (!buffer) {
95
- buffer = new LLMObsBuffer({ events: [], size: 0, routing })
94
+ buffer = new LLMObsBuffer({ events: [], routing })
96
95
  this.#multiTenantBuffers.set(apiKey, buffer)
97
96
  }
98
97
  return buffer
@@ -17,11 +17,7 @@ class Log {
17
17
  get formatted () {
18
18
  const { message, args } = this
19
19
 
20
- let formatted = message
21
- if (message && args && args.length) {
22
- formatted = format(message, ...args)
23
- }
24
- return formatted
20
+ return message && args && args.length ? format(message, ...args) : message
25
21
  }
26
22
 
27
23
  static parse (...args) {