dd-trace 5.98.0 → 5.99.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 (123) 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 +48 -46
  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/modelcontextprotocol-sdk.js +7 -0
  17. package/packages/datadog-instrumentations/src/pino.js +4 -28
  18. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +27 -0
  19. package/packages/datadog-instrumentations/src/playwright.js +5 -17
  20. package/packages/datadog-instrumentations/src/stripe.js +38 -24
  21. package/packages/datadog-instrumentations/src/vitest.js +32 -4
  22. package/packages/datadog-instrumentations/src/zlib.js +29 -0
  23. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -2
  24. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +8 -15
  25. package/packages/datadog-plugin-azure-service-bus/src/producer.js +4 -9
  26. package/packages/datadog-plugin-cucumber/src/index.js +2 -2
  27. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +5 -5
  28. package/packages/datadog-plugin-cypress/src/source-map-utils.js +48 -1
  29. package/packages/datadog-plugin-http/src/server.js +11 -11
  30. package/packages/datadog-plugin-jest/src/index.js +2 -2
  31. package/packages/datadog-plugin-mocha/src/index.js +1 -2
  32. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/index.js +24 -0
  33. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +55 -0
  34. package/packages/datadog-plugin-mongodb-core/src/index.js +1 -6
  35. package/packages/datadog-plugin-playwright/src/index.js +2 -3
  36. package/packages/datadog-plugin-vitest/src/index.js +14 -6
  37. package/packages/datadog-plugin-ws/src/close.js +2 -0
  38. package/packages/datadog-plugin-ws/src/producer.js +2 -0
  39. package/packages/datadog-plugin-ws/src/receiver.js +1 -0
  40. package/packages/dd-trace/src/aiguard/channels.js +8 -0
  41. package/packages/dd-trace/src/aiguard/index.js +7 -3
  42. package/packages/dd-trace/src/aiguard/sdk.js +44 -0
  43. package/packages/dd-trace/src/aiguard/tags.js +1 -0
  44. package/packages/dd-trace/src/appsec/graphql.js +6 -6
  45. package/packages/dd-trace/src/appsec/index.js +9 -11
  46. package/packages/dd-trace/src/appsec/rasp/command_injection.js +4 -5
  47. package/packages/dd-trace/src/appsec/rasp/lfi.js +8 -4
  48. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +5 -10
  49. package/packages/dd-trace/src/appsec/rasp/ssrf.js +5 -6
  50. package/packages/dd-trace/src/appsec/recommended.json +2438 -13
  51. package/packages/dd-trace/src/appsec/reporter.js +6 -5
  52. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +4 -8
  53. package/packages/dd-trace/src/appsec/store.js +50 -0
  54. package/packages/dd-trace/src/appsec/waf/index.js +3 -5
  55. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -2
  56. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -2
  57. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +2 -2
  58. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -2
  59. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +3 -4
  60. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -2
  61. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +4 -5
  62. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +3 -4
  63. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +6 -6
  64. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +2 -2
  65. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +2 -2
  66. package/packages/dd-trace/src/config/config-types.d.ts +0 -4
  67. package/packages/dd-trace/src/config/defaults.js +10 -10
  68. package/packages/dd-trace/src/config/generated-config-types.d.ts +13 -12
  69. package/packages/dd-trace/src/config/index.js +25 -35
  70. package/packages/dd-trace/src/config/parsers.js +26 -9
  71. package/packages/dd-trace/src/config/supported-configurations.json +32 -36
  72. package/packages/dd-trace/src/debugger/config.js +2 -0
  73. package/packages/dd-trace/src/debugger/devtools_client/send.js +25 -5
  74. package/packages/dd-trace/src/encode/0.4.js +4 -5
  75. package/packages/dd-trace/src/exporters/agent/index.js +0 -1
  76. package/packages/dd-trace/src/exporters/agent/writer.js +1 -2
  77. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -3
  78. package/packages/dd-trace/src/exporters/common/util.js +2 -2
  79. package/packages/dd-trace/src/id.js +2 -0
  80. package/packages/dd-trace/src/index.js +2 -5
  81. package/packages/dd-trace/src/lambda/handler.js +1 -3
  82. package/packages/dd-trace/src/llmobs/plugins/{anthropic.js → anthropic/index.js} +5 -63
  83. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +106 -0
  84. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +3 -2
  85. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +3 -2
  86. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +2 -1
  87. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +0 -49
  88. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js +2 -1
  89. package/packages/dd-trace/src/llmobs/plugins/langchain/messages.js +76 -0
  90. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -26
  91. package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/index.js +68 -0
  92. package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/utils.js +57 -0
  93. package/packages/dd-trace/src/llmobs/sdk.js +2 -2
  94. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +103 -0
  95. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -0
  96. package/packages/dd-trace/src/opentelemetry/logs/index.js +1 -1
  97. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +3 -2
  98. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  99. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +3 -2
  100. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +19 -66
  101. package/packages/dd-trace/src/opentelemetry/trace/index.js +11 -16
  102. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +11 -3
  103. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +51 -41
  104. package/packages/dd-trace/src/opentelemetry/tracer.js +9 -11
  105. package/packages/dd-trace/src/opentracing/propagation/text_map.js +17 -10
  106. package/packages/dd-trace/src/opentracing/span.js +1 -1
  107. package/packages/dd-trace/src/opentracing/tracer.js +12 -5
  108. package/packages/dd-trace/src/plugins/index.js +1 -0
  109. package/packages/dd-trace/src/plugins/util/test.js +126 -5
  110. package/packages/dd-trace/src/plugins/util/url.js +2 -1
  111. package/packages/dd-trace/src/profiling/profilers/event_plugins/crypto.js +32 -0
  112. package/packages/dd-trace/src/profiling/profilers/event_plugins/zlib.js +19 -0
  113. package/packages/dd-trace/src/profiling/profilers/events.js +35 -0
  114. package/packages/dd-trace/src/proxy.js +2 -8
  115. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -2
  116. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  117. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  118. package/packages/dd-trace/src/span_processor.js +1 -2
  119. package/packages/dd-trace/src/tagger.js +2 -2
  120. package/packages/dd-trace/src/telemetry/send-data.js +5 -7
  121. package/packages/dd-trace/src/tracer.js +2 -2
  122. package/vendor/dist/ignore/LICENSE +0 -21
  123. package/vendor/dist/ignore/index.js +0 -1
@@ -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,7 +2,7 @@
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')
@@ -427,7 +427,7 @@ class LLMObs extends NoopLLMObs {
427
427
  }
428
428
 
429
429
  // 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'))) {
430
+ if (this._config.DD_TRACE_OTEL_ENABLED) {
431
431
  evaluationTags.source = 'otel'
432
432
  }
433
433
 
@@ -0,0 +1,103 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+
5
+ const METER_NAME = 'dd-trace-js/openfeature'
6
+ const COUNTER_NAME = 'feature_flag.evaluations'
7
+ const COUNTER_DESCRIPTION = 'Number of feature flag evaluations'
8
+ const COUNTER_UNIT = '{evaluation}'
9
+
10
+ /**
11
+ * OpenFeature hook that tracks feature flag evaluation metrics using an
12
+ * OpenTelemetry counter.
13
+ *
14
+ * Implements the OpenFeature `finally` hook interface so it can be pushed
15
+ * directly onto a provider's `hooks` array. We use the `finally` stage
16
+ * (not diagnostic channels inside the provider's `resolve*` methods) because
17
+ * the OpenFeature SDK short-circuits before calling the provider when it is in
18
+ * NOT_READY state; the `finally` hook still fires, ensuring all evaluations are
19
+ * captured. It also catches type-mismatch errors detected by the SDK client
20
+ * after the provider returns.
21
+ *
22
+ * The counter is created lazily on the first successful `finally()` call rather
23
+ * than in the constructor. This is necessary because `FlaggingProvider` is
24
+ * constructed eagerly by `proxy.js#updateTracing()`, which runs *before*
25
+ * `initializeOpenTelemetryMetrics()` sets the global OTel meter provider.
26
+ * Calling `getMeter()` in the constructor would return the noop meter and
27
+ * produce a noop counter that silently discards all measurements. By deferring
28
+ * to `finally()` time we give the meter provider a chance to be set up first.
29
+ *
30
+ * If counter creation fails (e.g. the OTel API is not yet available), the call
31
+ * is silently skipped and retried on the next `finally()` invocation.
32
+ *
33
+ * When `config.otelMetricsEnabled` is false, `finally()` is always a no-op.
34
+ */
35
+ class EvalMetricsHook {
36
+ #enabled = false
37
+ #counter = null
38
+
39
+ /**
40
+ * @param {import('../config')} config - Tracer configuration object
41
+ */
42
+ constructor (config) {
43
+ this.#enabled = config.otelMetricsEnabled === true
44
+ }
45
+
46
+ /**
47
+ * Returns the OTel counter, creating it on first successful call.
48
+ * Returns `null` if counter creation fails; will retry on next call.
49
+ *
50
+ * @returns {import('@opentelemetry/api').Counter | null}
51
+ */
52
+ #getCounter () {
53
+ if (this.#counter) return this.#counter
54
+
55
+ try {
56
+ const { metrics } = require('@opentelemetry/api')
57
+ const meter = metrics.getMeter(METER_NAME)
58
+ this.#counter = meter.createCounter(COUNTER_NAME, {
59
+ description: COUNTER_DESCRIPTION,
60
+ unit: COUNTER_UNIT,
61
+ })
62
+ } catch (e) {
63
+ log.warn('EvalMetricsHook: failed to create counter: %s', e.message)
64
+ }
65
+
66
+ return this.#counter
67
+ }
68
+
69
+ /**
70
+ * Called by the OpenFeature SDK after every flag evaluation (success or error).
71
+ *
72
+ * @param {{ flagKey: string }} hookContext - Hook context containing the flag key
73
+ * @param {{ variant?: string, reason?: string, errorCode?: string, flagMetadata?: object }} evaluationDetails
74
+ * - Full evaluation details
75
+ * @returns {void}
76
+ */
77
+ finally (hookContext, evaluationDetails) {
78
+ if (!this.#enabled) return
79
+
80
+ const counter = this.#getCounter()
81
+ if (!counter) return
82
+
83
+ const attributes = {
84
+ 'feature_flag.key': hookContext?.flagKey ?? '',
85
+ 'feature_flag.result.variant': evaluationDetails?.variant ?? '',
86
+ 'feature_flag.result.reason': evaluationDetails?.reason?.toLowerCase() ?? 'unknown',
87
+ }
88
+
89
+ const errorCode = evaluationDetails?.errorCode
90
+ if (errorCode) {
91
+ attributes['error.type'] = errorCode.toLowerCase()
92
+ }
93
+
94
+ const allocationKey = evaluationDetails?.flagMetadata?.allocationKey
95
+ if (allocationKey) {
96
+ attributes['feature_flag.result.allocation_key'] = allocationKey
97
+ }
98
+
99
+ counter.add(1, attributes)
100
+ }
101
+ }
102
+
103
+ module.exports = EvalMetricsHook
@@ -4,6 +4,7 @@ const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server'
4
4
  const { channel } = require('dc-polyfill')
5
5
  const log = require('../log')
6
6
  const { EXPOSURE_CHANNEL } = require('./constants/constants')
7
+ const EvalMetricsHook = require('./eval-metrics-hook')
7
8
 
8
9
  /**
9
10
  * OpenFeature provider that integrates with Datadog's feature flagging system.
@@ -24,6 +25,8 @@ class FlaggingProvider extends DatadogNodeServerProvider {
24
25
  this._tracer = tracer
25
26
  this._config = config
26
27
 
28
+ this.hooks.push(new EvalMetricsHook(config))
29
+
27
30
  log.debug('%s created with timeout: %dms', this.constructor.name,
28
31
  config.experimental.flaggingProvider.initializationTimeoutMs)
29
32
  }
@@ -61,7 +61,7 @@ function initializeOpenTelemetryLogs (config) {
61
61
  // Create OTLP exporter using resolved config values
62
62
  const exporter = new OtlpHttpLogExporter(
63
63
  config.otelLogsUrl,
64
- config.otelLogsHeaders,
64
+ config.OTEL_EXPORTER_OTLP_LOGS_HEADERS,
65
65
  config.otelLogsTimeout,
66
66
  config.otelLogsProtocol,
67
67
  resourceAttributes
@@ -22,13 +22,14 @@ class OtlpHttpLogExporter extends OtlpHttpExporterBase {
22
22
  * Creates a new OtlpHttpLogExporter instance.
23
23
  *
24
24
  * @param {string} url - OTLP endpoint URL
25
- * @param {string} headers - Additional HTTP headers as comma-separated key=value string
25
+ * @param {Record<string, string>|undefined} headers - Additional HTTP headers parsed from the
26
+ * corresponding `OTEL_EXPORTER_OTLP_*_HEADERS` env by the MAP parser.
26
27
  * @param {number} timeout - Request timeout in milliseconds
27
28
  * @param {string} protocol - OTLP protocol (http/protobuf or http/json)
28
29
  * @param {Resource} resource - Resource attributes
29
30
  */
30
31
  constructor (url, headers, timeout, protocol, resource) {
31
- super(url, headers, timeout, protocol, '/v1/logs', 'logs')
32
+ super(url, headers, timeout, protocol, 'logs')
32
33
  this.transformer = new OtlpTransformer(resource, protocol)
33
34
  }
34
35
 
@@ -58,7 +58,7 @@ function initializeOpenTelemetryMetrics (config) {
58
58
 
59
59
  const exporter = new OtlpHttpMetricExporter(
60
60
  config.otelMetricsUrl,
61
- config.otelMetricsHeaders,
61
+ config.OTEL_EXPORTER_OTLP_METRICS_HEADERS,
62
62
  config.otelMetricsTimeout,
63
63
  config.otelMetricsProtocol,
64
64
  resourceAttributes