dd-trace 5.98.0 → 5.99.1

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 (139) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/ext/tags.js +1 -0
  3. package/index.d.ts +9 -1
  4. package/package.json +68 -47
  5. package/packages/datadog-instrumentations/src/crypto.js +45 -0
  6. package/packages/datadog-instrumentations/src/cypress-config.js +122 -16
  7. package/packages/datadog-instrumentations/src/dns.js +24 -56
  8. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  9. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +74 -0
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +4 -1
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +10 -3
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +59 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +11 -2
  16. package/packages/datadog-instrumentations/src/jest.js +5 -5
  17. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +7 -0
  18. package/packages/datadog-instrumentations/src/pino.js +4 -28
  19. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +27 -0
  20. package/packages/datadog-instrumentations/src/playwright.js +5 -17
  21. package/packages/datadog-instrumentations/src/stripe.js +38 -24
  22. package/packages/datadog-instrumentations/src/vitest.js +32 -4
  23. package/packages/datadog-instrumentations/src/zlib.js +29 -0
  24. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -2
  25. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +8 -15
  26. package/packages/datadog-plugin-azure-service-bus/src/producer.js +4 -9
  27. package/packages/datadog-plugin-cucumber/src/index.js +2 -2
  28. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +5 -5
  29. package/packages/datadog-plugin-cypress/src/source-map-utils.js +48 -1
  30. package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -1
  31. package/packages/datadog-plugin-graphql/src/utils.js +2 -2
  32. package/packages/datadog-plugin-http/src/server.js +11 -11
  33. package/packages/datadog-plugin-jest/src/index.js +2 -2
  34. package/packages/datadog-plugin-memcached/src/index.js +1 -1
  35. package/packages/datadog-plugin-mocha/src/index.js +1 -2
  36. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/index.js +24 -0
  37. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +55 -0
  38. package/packages/datadog-plugin-mongodb-core/src/index.js +1 -6
  39. package/packages/datadog-plugin-playwright/src/index.js +2 -3
  40. package/packages/datadog-plugin-vitest/src/index.js +14 -6
  41. package/packages/datadog-plugin-ws/src/close.js +2 -0
  42. package/packages/datadog-plugin-ws/src/producer.js +2 -0
  43. package/packages/datadog-plugin-ws/src/receiver.js +1 -0
  44. package/packages/dd-trace/src/aiguard/channels.js +8 -0
  45. package/packages/dd-trace/src/aiguard/index.js +7 -3
  46. package/packages/dd-trace/src/aiguard/sdk.js +44 -0
  47. package/packages/dd-trace/src/aiguard/tags.js +1 -0
  48. package/packages/dd-trace/src/appsec/blocking.js +18 -6
  49. package/packages/dd-trace/src/appsec/graphql.js +7 -7
  50. package/packages/dd-trace/src/appsec/index.js +9 -11
  51. package/packages/dd-trace/src/appsec/rasp/command_injection.js +4 -5
  52. package/packages/dd-trace/src/appsec/rasp/lfi.js +8 -4
  53. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +5 -10
  54. package/packages/dd-trace/src/appsec/rasp/ssrf.js +5 -6
  55. package/packages/dd-trace/src/appsec/recommended.json +2438 -13
  56. package/packages/dd-trace/src/appsec/reporter.js +6 -5
  57. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +4 -8
  58. package/packages/dd-trace/src/appsec/store.js +50 -0
  59. package/packages/dd-trace/src/appsec/waf/index.js +3 -5
  60. package/packages/dd-trace/src/baggage.js +16 -13
  61. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -2
  62. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -2
  63. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +2 -2
  64. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -2
  65. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +1 -1
  66. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +3 -4
  67. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -2
  68. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +4 -5
  69. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +3 -4
  70. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +6 -6
  71. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +2 -2
  72. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +2 -2
  73. package/packages/dd-trace/src/config/config-types.d.ts +0 -4
  74. package/packages/dd-trace/src/config/defaults.js +10 -10
  75. package/packages/dd-trace/src/config/generated-config-types.d.ts +39 -38
  76. package/packages/dd-trace/src/config/index.js +29 -39
  77. package/packages/dd-trace/src/config/parsers.js +26 -9
  78. package/packages/dd-trace/src/config/supported-configurations.json +46 -78
  79. package/packages/dd-trace/src/debugger/config.js +2 -0
  80. package/packages/dd-trace/src/debugger/devtools_client/send.js +25 -5
  81. package/packages/dd-trace/src/dogstatsd.js +5 -8
  82. package/packages/dd-trace/src/encode/0.4.js +4 -5
  83. package/packages/dd-trace/src/exporter.js +1 -1
  84. package/packages/dd-trace/src/exporters/agent/index.js +0 -1
  85. package/packages/dd-trace/src/exporters/agent/writer.js +1 -2
  86. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -3
  87. package/packages/dd-trace/src/exporters/common/util.js +2 -2
  88. package/packages/dd-trace/src/git_metadata_tagger.js +1 -1
  89. package/packages/dd-trace/src/id.js +2 -0
  90. package/packages/dd-trace/src/index.js +2 -5
  91. package/packages/dd-trace/src/lambda/handler.js +1 -3
  92. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  93. package/packages/dd-trace/src/llmobs/plugins/{anthropic.js → anthropic/index.js} +5 -63
  94. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +106 -0
  95. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +3 -2
  96. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +3 -2
  97. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +2 -1
  98. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +0 -49
  99. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js +2 -1
  100. package/packages/dd-trace/src/llmobs/plugins/langchain/messages.js +76 -0
  101. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -26
  102. package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/index.js +68 -0
  103. package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/utils.js +57 -0
  104. package/packages/dd-trace/src/llmobs/sdk.js +23 -3
  105. package/packages/dd-trace/src/llmobs/span_processor.js +14 -1
  106. package/packages/dd-trace/src/llmobs/writers/base.js +7 -1
  107. package/packages/dd-trace/src/llmobs/writers/spans.js +1 -1
  108. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +103 -0
  109. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -0
  110. package/packages/dd-trace/src/opentelemetry/logs/index.js +6 -6
  111. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +3 -2
  112. package/packages/dd-trace/src/opentelemetry/metrics/index.js +7 -7
  113. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +3 -2
  114. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +19 -66
  115. package/packages/dd-trace/src/opentelemetry/trace/index.js +11 -16
  116. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +11 -3
  117. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +51 -41
  118. package/packages/dd-trace/src/opentelemetry/tracer.js +9 -11
  119. package/packages/dd-trace/src/opentracing/propagation/text_map.js +30 -23
  120. package/packages/dd-trace/src/opentracing/span.js +2 -2
  121. package/packages/dd-trace/src/opentracing/tracer.js +12 -5
  122. package/packages/dd-trace/src/plugin_manager.js +6 -6
  123. package/packages/dd-trace/src/plugins/index.js +1 -0
  124. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  125. package/packages/dd-trace/src/plugins/util/test.js +128 -7
  126. package/packages/dd-trace/src/plugins/util/url.js +2 -1
  127. package/packages/dd-trace/src/profiling/profilers/event_plugins/crypto.js +32 -0
  128. package/packages/dd-trace/src/profiling/profilers/event_plugins/zlib.js +19 -0
  129. package/packages/dd-trace/src/profiling/profilers/events.js +35 -0
  130. package/packages/dd-trace/src/proxy.js +8 -14
  131. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -2
  132. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  133. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  134. package/packages/dd-trace/src/span_processor.js +1 -2
  135. package/packages/dd-trace/src/tagger.js +2 -2
  136. package/packages/dd-trace/src/telemetry/send-data.js +5 -7
  137. package/packages/dd-trace/src/tracer.js +2 -2
  138. package/vendor/dist/ignore/LICENSE +0 -21
  139. package/vendor/dist/ignore/index.js +0 -1
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const { UNKNOWN_MODEL_PROVIDER } = require('../constants/tags')
4
- const LLMObsPlugin = require('./base')
3
+ const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
4
+ const LLMObsPlugin = require('../base')
5
+ const { appendMessage } = require('./util')
5
6
 
6
7
  const ALLOWED_METADATA_KEYS = new Set([
7
8
  'max_tokens',
@@ -144,51 +145,11 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
144
145
  const inputMessages = []
145
146
 
146
147
  if (system) {
147
- messages.unshift({ content: system, role: 'system' })
148
+ appendMessage(inputMessages, { role: 'system', content: system })
148
149
  }
149
150
 
150
151
  for (const message of messages) {
151
- const { content, role } = message
152
-
153
- if (typeof content === 'string') {
154
- inputMessages.push({ content, role })
155
- continue
156
- }
157
-
158
- for (const block of content) {
159
- if (block.type === 'text') {
160
- inputMessages.push({ content: block.text, role })
161
- } else if (block.type === 'image') {
162
- inputMessages.push({ content: '([IMAGE DETECTED])', role })
163
- } else if (block.type === 'tool_use') {
164
- const { text, name, id, type } = block
165
- let input = block.input
166
- if (typeof input === 'string') {
167
- input = JSON.parse(input)
168
- }
169
-
170
- const toolCall = {
171
- name,
172
- arguments: input,
173
- toolId: id,
174
- type,
175
- }
176
-
177
- inputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
178
- } else if (block.type === 'tool_result') {
179
- const { content } = block
180
- const formattedContent = this.#formatAnthropicToolResultContent(content)
181
- const toolResult = {
182
- result: formattedContent,
183
- toolId: block.tool_use_id,
184
- type: 'tool_result',
185
- }
186
-
187
- inputMessages.push({ content: '', role, toolResults: [toolResult] })
188
- } else {
189
- inputMessages.push({ content: JSON.stringify(block), role })
190
- }
191
- }
152
+ appendMessage(inputMessages, message)
192
153
  }
193
154
 
194
155
  this._tagger.tagLLMIO(span, inputMessages)
@@ -273,25 +234,6 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
273
234
 
274
235
  this._tagger.tagMetrics(span, metrics)
275
236
  }
276
-
277
- // maybe can make into a util file
278
- #formatAnthropicToolResultContent (content) {
279
- if (typeof content === 'string') {
280
- return content
281
- } else if (Array.isArray(content)) {
282
- const formattedContent = []
283
- for (const toolResultBlock of content) {
284
- if (toolResultBlock.text) {
285
- formattedContent.push(toolResultBlock.text)
286
- } else if (toolResultBlock.type === 'image') {
287
- formattedContent.push('([IMAGE DETECTED])')
288
- }
289
- }
290
-
291
- return formattedContent.join(',')
292
- }
293
- return JSON.stringify(content)
294
- }
295
237
  }
296
238
 
297
239
  module.exports = AnthropicLLMObsPlugin
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @typedef {{type: 'text', text: string}} TextBlock
5
+ * @typedef {{type: 'image'}} ImageBlock
6
+ * @typedef {{
7
+ * type: 'tool_use', text: string, name: string, id: string, input: string | Record<string, unknown>
8
+ * }} ToolUseBlock
9
+ * @typedef {{
10
+ * type: 'tool_result',
11
+ * tool_use_id: string,
12
+ * content: string | Array<{type: string, text?: string}>
13
+ * }} ToolResultBlock
14
+ *
15
+ * @typedef {{
16
+ * content: string,
17
+ * role: string,
18
+ * toolCalls?: Array<{
19
+ * name: string,
20
+ * arguments: string | Record<string, unknown>,
21
+ * toolId: string,
22
+ * type: string
23
+ * }>,
24
+ * toolResults?: Array<{
25
+ * result: string,
26
+ * toolId: string,
27
+ * type: 'tool_result'
28
+ * }>
29
+ * }} AnthropicLlmObsMessage
30
+ */
31
+
32
+ /**
33
+ * Formats tool result into LLM Observability compatible contents
34
+ * @param {ToolResultBlock['content']} content
35
+ */
36
+ function formatAnthropicToolResultContent (content) {
37
+ if (typeof content === 'string') {
38
+ return content
39
+ } else if (Array.isArray(content)) {
40
+ const formattedContent = []
41
+ for (const toolResultBlock of content) {
42
+ if (toolResultBlock.text) {
43
+ formattedContent.push(toolResultBlock.text)
44
+ } else if (toolResultBlock.type === 'image') {
45
+ formattedContent.push('([IMAGE DETECTED])')
46
+ }
47
+ }
48
+
49
+ return formattedContent.join(',')
50
+ }
51
+ return JSON.stringify(content)
52
+ }
53
+
54
+ /**
55
+ * Normalizes and formats a message into LLM Observability compatible contents.
56
+ * Can be spread into a list of other messages.
57
+ *
58
+ * @param {AnthropicLlmObsMessage[]} messages
59
+ * @param {{ role: string, content: string | Array<TextBlock | ImageBlock | ToolUseBlock | ToolResultBlock> }} message
60
+ * @returns {void}
61
+ */
62
+ function appendMessage (messages, { role, content }) {
63
+ if (typeof content === 'string') {
64
+ messages.push({ content, role })
65
+ return
66
+ }
67
+
68
+ for (const block of content) {
69
+ if (block.type === 'text') {
70
+ messages.push({ content: block.text, role })
71
+ } else if (block.type === 'image') {
72
+ messages.push({ content: '([IMAGE DETECTED])', role })
73
+ } else if (block.type === 'tool_use') {
74
+ const { text, name, id, type } = block
75
+ let input = block.input
76
+ if (typeof input === 'string') {
77
+ input = JSON.parse(input)
78
+ }
79
+
80
+ const toolCall = {
81
+ name,
82
+ arguments: input,
83
+ toolId: id,
84
+ type,
85
+ }
86
+
87
+ messages.push({ content: text ?? '', role, toolCalls: [toolCall] })
88
+ } else if (block.type === 'tool_result') {
89
+ const { content } = block
90
+ const formattedContent = formatAnthropicToolResultContent(content)
91
+ const toolResult = {
92
+ result: formattedContent,
93
+ toolId: block.tool_use_id,
94
+ type: 'tool_result',
95
+ }
96
+
97
+ messages.push({ content: '', role, toolResults: [toolResult] })
98
+ } else {
99
+ messages.push({ content: JSON.stringify(block), role })
100
+ }
101
+ }
102
+ }
103
+
104
+ module.exports = {
105
+ appendMessage,
106
+ }
@@ -1,16 +1,17 @@
1
1
  'use strict'
2
2
 
3
3
  const { spanHasError } = require('../../../util')
4
+ const { formatIO } = require('../messages')
4
5
  const LangChainLLMObsHandler = require('.')
5
6
 
6
7
  class LangChainLLMObsChainHandler extends LangChainLLMObsHandler {
7
8
  setMetaTags ({ span, inputs, results }) {
8
9
  let input
9
10
  if (inputs) {
10
- input = this.formatIO(inputs)
11
+ input = formatIO(inputs)
11
12
  }
12
13
 
13
- const output = !results || spanHasError(span) ? '' : this.formatIO(results)
14
+ const output = !results || spanHasError(span) ? '' : formatIO(results)
14
15
 
15
16
  // chain spans will always be workflows
16
17
  this._tagger.tagTextIO(span, input, output)
@@ -2,6 +2,7 @@
2
2
 
3
3
  const LLMObsTagger = require('../../../tagger')
4
4
  const { spanHasError } = require('../../../util')
5
+ const { getRole } = require('../messages')
5
6
  const LangChainLLMObsHandler = require('.')
6
7
 
7
8
  const LLM = 'llm'
@@ -22,7 +23,7 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
22
23
  for (const messageSet of inputs) {
23
24
  for (const message of messageSet) {
24
25
  const content = message.content || ''
25
- const role = this.getRole(message)
26
+ const role = getRole(message)
26
27
  inputMessages.push({ content, role })
27
28
  }
28
29
  }
@@ -54,7 +55,7 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
54
55
  for (const messageSet of results.generations) {
55
56
  for (const chatCompletion of messageSet) {
56
57
  const chatCompletionMessage = chatCompletion.message
57
- const role = this.getRole(chatCompletionMessage)
58
+ const role = getRole(chatCompletionMessage)
58
59
  const content = chatCompletionMessage.text || ''
59
60
  const toolCalls = this.extractToolCalls(chatCompletionMessage)
60
61
  outputMessages.push({ content, role, toolCalls })
@@ -2,6 +2,7 @@
2
2
 
3
3
  const LLMObsTagger = require('../../../tagger')
4
4
  const { spanHasError } = require('../../../util')
5
+ const { formatIO } = require('../messages')
5
6
  const LangChainLLMObsHandler = require('.')
6
7
 
7
8
  class LangChainLLMObsEmbeddingHandler extends LangChainLLMObsHandler {
@@ -10,7 +11,7 @@ class LangChainLLMObsEmbeddingHandler extends LangChainLLMObsHandler {
10
11
  let embeddingInput, embeddingOutput
11
12
 
12
13
  if (isWorkflow) {
13
- embeddingInput = this.formatIO(inputs)
14
+ embeddingInput = formatIO(inputs)
14
15
  } else {
15
16
  const input = Array.isArray(inputs) ? inputs : [inputs]
16
17
  embeddingInput = input.map(doc => ({ text: doc }))
@@ -1,11 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const ROLE_MAPPINGS = {
4
- human: 'user',
5
- ai: 'assistant',
6
- system: 'system',
7
- }
8
-
9
3
  class LangChainLLMObsHandler {
10
4
  constructor (tagger) {
11
5
  /** @type {import('../../../tagger')} */
@@ -18,38 +12,6 @@ class LangChainLLMObsHandler {
18
12
 
19
13
  setMetaTags () {}
20
14
 
21
- formatIO (messages) {
22
- if (messages.constructor.name === 'Object') { // plain JSON
23
- const formatted = {}
24
- for (const [key, value] of Object.entries(messages)) {
25
- formatted[key] = this.formatIO(value)
26
- }
27
-
28
- return formatted
29
- } else if (Array.isArray(messages)) {
30
- return messages.map(message => this.formatIO(message))
31
- } // either a BaseMesage type or a string
32
- return this.getContentFromMessage(messages)
33
- }
34
-
35
- getContentFromMessage (message) {
36
- if (typeof message === 'string') {
37
- return message
38
- }
39
- try {
40
- const messageContent = {
41
- content: message.content || '',
42
- }
43
-
44
- const role = this.getRole(message)
45
- if (role) messageContent.role = role
46
-
47
- return messageContent
48
- } catch {
49
- return JSON.stringify(message)
50
- }
51
- }
52
-
53
15
  checkTokenUsageChatOrLLMResult (results) {
54
16
  const llmOutput = results.llmOutput
55
17
  const tokens = {
@@ -90,17 +52,6 @@ class LangChainLLMObsHandler {
90
52
  runId: runIdBase,
91
53
  }
92
54
  }
93
-
94
- getRole (message) {
95
- if (message.role) return ROLE_MAPPINGS[message.role] || message.role
96
-
97
- const type = (
98
- (typeof message.getType === 'function' && message.getType()) ||
99
- (typeof message._getType === 'function' && message._getType())
100
- )
101
-
102
- return ROLE_MAPPINGS[type] || type
103
- }
104
55
  }
105
56
 
106
57
  module.exports = LangChainLLMObsHandler
@@ -1,11 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const { spanHasError } = require('../../../util')
4
+ const { formatIO } = require('../messages')
4
5
  const LangChainLLMObsHandler = require('.')
5
6
 
6
7
  class LangChainLLMObsVectorStoreHandler extends LangChainLLMObsHandler {
7
8
  setMetaTags ({ span, inputs, results }) {
8
- const input = this.formatIO(inputs)
9
+ const input = formatIO(inputs)
9
10
  if (spanHasError(span)) {
10
11
  this._tagger.tagRetrievalIO(span, input)
11
12
  return
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const ROLE_MAPPINGS = {
4
+ human: 'user',
5
+ ai: 'assistant',
6
+ system: 'system',
7
+ }
8
+
9
+ function getRole (message) {
10
+ if (message.role) return ROLE_MAPPINGS[message.role] || message.role
11
+
12
+ const type = (
13
+ (typeof message.getType === 'function' && message.getType()) ||
14
+ (typeof message._getType === 'function' && message._getType())
15
+ )
16
+
17
+ return ROLE_MAPPINGS[type] || type
18
+ }
19
+
20
+ function getContentFromMessage (message) {
21
+ if (typeof message === 'string') {
22
+ return message
23
+ }
24
+ try {
25
+ const messageContent = {
26
+ content: message.content || '',
27
+ }
28
+
29
+ const role = getRole(message)
30
+ if (role) messageContent.role = role
31
+
32
+ return messageContent
33
+ } catch {
34
+ return JSON.stringify(message)
35
+ }
36
+ }
37
+
38
+ function isBaseMessage (data) {
39
+ return typeof data._getType === 'function' || typeof data.getType === 'function'
40
+ }
41
+
42
+ function formatIO (data) {
43
+ if (data == null) return ''
44
+
45
+ if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
46
+ return data
47
+ }
48
+
49
+ if (data.constructor?.name === 'Object') {
50
+ const formatted = {}
51
+ for (const [key, value] of Object.entries(data)) {
52
+ formatted[key] = formatIO(value)
53
+ }
54
+ return formatted
55
+ }
56
+
57
+ if (Array.isArray(data)) {
58
+ return data.map(item => formatIO(item))
59
+ }
60
+
61
+ // Only duck-typed BaseMessage instances collapse to { content, role }.
62
+ // Other class instances (e.g. LangChain Document) preserve their shape via JSON.stringify,
63
+ // otherwise they'd reduce to { content: '' } and lose data.
64
+ if (isBaseMessage(data)) return getContentFromMessage(data)
65
+
66
+ try {
67
+ return JSON.stringify(data)
68
+ } catch {
69
+ return String(data)
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ getRole,
75
+ formatIO,
76
+ }
@@ -1,36 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const LLMObsPlugin = require('../base')
4
+ const { formatIO } = require('../langchain/messages')
4
5
  const { spanHasError } = require('../../util')
5
6
 
6
7
  const streamDataMap = new WeakMap()
7
8
 
8
- function formatIO (data) {
9
- if (data == null) return ''
10
-
11
- if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
12
- return data
13
- }
14
-
15
- if (data.constructor?.name === 'Object') {
16
- const formatted = {}
17
- for (const [key, value] of Object.entries(data)) {
18
- formatted[key] = formatIO(value)
19
- }
20
- return formatted
21
- }
22
-
23
- if (Array.isArray(data)) {
24
- return data.map(item => formatIO(item))
25
- }
26
-
27
- try {
28
- return JSON.stringify(data)
29
- } catch {
30
- return String(data)
31
- }
32
- }
33
-
34
9
  class PregelStreamLLMObsPlugin extends LLMObsPlugin {
35
10
  static id = 'llmobs_langgraph_pregel_stream'
36
11
  static integration = 'langgraph'
@@ -0,0 +1,68 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('../base')
4
+ const { formatInput, formatOutput } = require('./utils')
5
+
6
+ class McpToolCallLLMObsPlugin extends LLMObsPlugin {
7
+ static id = 'llmobs_mcp_tool_call'
8
+ static integration = 'modelcontextprotocol-sdk'
9
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_callTool'
10
+
11
+ getLLMObsSpanRegisterOptions (ctx) {
12
+ const params = ctx.arguments?.[0]
13
+ const toolName = params?.name || 'unknown_tool'
14
+
15
+ return {
16
+ kind: 'tool',
17
+ name: `MCP Client Tool Call: ${toolName}`,
18
+ }
19
+ }
20
+
21
+ setLLMObsTags (ctx) {
22
+ const span = ctx.currentStore?.span
23
+ if (!span) return
24
+
25
+ const params = ctx.arguments?.[0]
26
+ const toolName = params?.name
27
+ const toolArguments = params?.arguments
28
+
29
+ const spanTags = { mcp_tool_kind: 'client' }
30
+
31
+ const serverVersion = ctx.self?.getServerVersion?.()
32
+ if (serverVersion) {
33
+ if (serverVersion.name) spanTags.mcp_server_name = serverVersion.name
34
+ if (serverVersion.version) spanTags.mcp_server_version = serverVersion.version
35
+ if (serverVersion.title) spanTags.mcp_server_title = serverVersion.title
36
+ }
37
+
38
+ this._tagger.tagSpanTags(span, spanTags)
39
+
40
+ const hasError = ctx.error || ctx.result?.isError
41
+ const input = formatInput(toolName, toolArguments)
42
+ const output = hasError ? undefined : formatOutput(ctx.result)
43
+
44
+ this._tagger.tagTextIO(span, input, output)
45
+ }
46
+ }
47
+
48
+ class McpListToolsLLMObsPlugin extends LLMObsPlugin {
49
+ static id = 'llmobs_mcp_list_tools'
50
+ static integration = 'modelcontextprotocol-sdk'
51
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_listTools'
52
+
53
+ getLLMObsSpanRegisterOptions () {
54
+ return {
55
+ kind: 'task',
56
+ name: 'MCP Client List Tools',
57
+ }
58
+ }
59
+
60
+ setLLMObsTags (ctx) {
61
+ const span = ctx.currentStore?.span
62
+ if (!span || ctx.error) return
63
+
64
+ this._tagger.tagTextIO(span, null, JSON.stringify(ctx.result))
65
+ }
66
+ }
67
+
68
+ module.exports = [McpToolCallLLMObsPlugin, McpListToolsLLMObsPlugin]
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Formats tool call input as a JSON string.
5
+ * @param {string} toolName - The name of the tool being called
6
+ * @param {object} toolArguments - The arguments passed to the tool
7
+ * @returns {string} Formatted input string
8
+ */
9
+ function formatInput (toolName, toolArguments) {
10
+ if (!toolName && !toolArguments) return ''
11
+
12
+ if (toolArguments === undefined || toolArguments === null) {
13
+ return toolName || ''
14
+ }
15
+
16
+ try {
17
+ return JSON.stringify({ name: toolName, arguments: toolArguments })
18
+ } catch {
19
+ return toolName || ''
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Formats MCP tool call result as a structured object matching Python's output format.
25
+ * MCP tool results contain a `content` array with items like:
26
+ * `[{ type: 'text', text: '...' }, { type: 'image', data: '...', mimeType: '...' }]`
27
+ * @param {object} result - The MCP CallToolResult
28
+ * @returns {string} JSON string of `{ content: Array<{type, text, annotations, meta}>, isError: boolean }`
29
+ */
30
+ function formatOutput (result) {
31
+ if (!result) return ''
32
+
33
+ const content = result.content
34
+ const isError = result.isError || false
35
+
36
+ const processed = []
37
+ if (Array.isArray(content)) {
38
+ for (const item of content) {
39
+ if (item.type !== 'text') continue
40
+ const contentBlock = {
41
+ type: item.type,
42
+ text: item.text || '',
43
+ annotations: item.annotations || {},
44
+ meta: item._meta || {},
45
+ }
46
+ processed.push(contentBlock)
47
+ }
48
+ }
49
+
50
+ try {
51
+ return JSON.stringify({ content: processed, isError })
52
+ } catch {
53
+ return ''
54
+ }
55
+ }
56
+
57
+ module.exports = { formatInput, formatOutput }
@@ -2,12 +2,18 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const { isTrue, isError } = require('../util')
5
+ const { isError, isTrue } = require('../util')
6
6
  const tracerVersion = require('../../../../package.json').version
7
7
  const logger = require('../log')
8
8
  const { getValueFromEnvSources } = require('../config/helper')
9
9
  const Span = require('../opentracing/span')
10
- const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags')
10
+ const {
11
+ SPAN_KIND,
12
+ OUTPUT_VALUE,
13
+ INPUT_VALUE,
14
+ LLMOBS_TRACE_ID_BRIDGE_KEY,
15
+ LLMOBS_PARENT_ID_BRIDGE_KEY,
16
+ } = require('./constants/tags')
11
17
  const {
12
18
  getFunctionArguments,
13
19
  validateKind,
@@ -427,7 +433,7 @@ class LLMObs extends NoopLLMObs {
427
433
  }
428
434
 
429
435
  // When OTel tracing is enabled, add source:otel tag to allow backend to wait for OTel span conversion
430
- if (isTrue(getValueFromEnvSources('DD_TRACE_OTEL_ENABLED'))) {
436
+ if (this._config.DD_TRACE_OTEL_ENABLED) {
431
437
  evaluationTags.source = 'otel'
432
438
  }
433
439
 
@@ -533,6 +539,20 @@ class LLMObs extends NoopLLMObs {
533
539
  ...options,
534
540
  parent: parentStore?.span,
535
541
  })
542
+
543
+ // Bridge tags read by the dd-go LLMObs trace-indexer to correlate OTel
544
+ // gen_ai.* spans with SDK LLMObs spans. Written once per local trace,
545
+ // on the first successful SDK LLMObs span registration. The shared
546
+ // _trace.tags bag is serialized to the first span in every flushed
547
+ // chunk's meta, so partial flush is covered automatically without a
548
+ // separate flush-time processor. Writing only after registerLLMObsSpan
549
+ // succeeds avoids poisoning _trace.tags with bridge tags pointing at a
550
+ // span that will never produce an LLMObs event.
551
+ const traceTags = span?.context?.()._trace?.tags
552
+ if (this.enabled && traceTags && !traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY]) {
553
+ traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = span.context().toTraceId(true)
554
+ traceTags[LLMOBS_PARENT_ID_BRIDGE_KEY] = span.context().toSpanId()
555
+ }
536
556
  }
537
557
 
538
558
  try {
@@ -30,6 +30,7 @@ const {
30
30
  INPUT_PROMPT,
31
31
  ROUTING_API_KEY,
32
32
  ROUTING_SITE,
33
+ LLMOBS_SUBMITTED_TAG_KEY,
33
34
  } = require('./constants/tags')
34
35
  const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
35
36
  const telemetry = require('./telemetry')
@@ -87,7 +88,19 @@ class LLMObsSpanProcessor {
87
88
  site: mlObsTags[ROUTING_SITE],
88
89
  }
89
90
 
90
- this.#writer.append(formattedEvent, routing)
91
+ const enqueued = this.#writer.append(formattedEvent, routing)
92
+
93
+ // Marker read by the dd-go LLMObs trace-indexer: when reparenting OTel
94
+ // gen_ai.* spans, the parent-chain walk stops at any span carrying this
95
+ // tag, preserving this span as the immediate LLMObs parent. Set only
96
+ // when the writer actually buffered the event — format may have dropped
97
+ // it (user processor returned null), thrown, or the writer may have
98
+ // dropped it silently when its buffer is full. Leaving this tag off in
99
+ // those cases avoids dd-go reparenting OTel children under a span that
100
+ // has no corresponding LLMObs event.
101
+ if (enqueued) {
102
+ span.context()._tags[LLMOBS_SUBMITTED_TAG_KEY] = '1'
103
+ }
91
104
  } catch (e) {
92
105
  // this should be a rare case
93
106
  // we protect against unserializable properties in the format function, and in
@@ -87,19 +87,25 @@ class BaseLLMObsWriter {
87
87
  return buffer
88
88
  }
89
89
 
90
+ /**
91
+ * @returns {boolean} `true` if the event was buffered, `false` if it was dropped
92
+ * (e.g. the per-routing buffer was full). Callers that depend on the event
93
+ * actually being submitted should check this value.
94
+ */
90
95
  append (event, routing, byteLength) {
91
96
  const buffer = this._getBuffer(routing)
92
97
 
93
98
  if (buffer.events.length >= buffer.limit) {
94
99
  logger.warn(`${this.constructor.name} event buffer full (limit is ${buffer.limit}), dropping event`)
95
100
  telemetry.recordDroppedPayload(1, this._eventType, 'buffer_full')
96
- return
101
+ return false
97
102
  }
98
103
 
99
104
  const eventSize = byteLength || Buffer.byteLength(JSON.stringify(event))
100
105
 
101
106
  buffer.size += eventSize
102
107
  buffer.events.push(event)
108
+ return true
103
109
  }
104
110
 
105
111
  flush () {