dd-trace 6.0.0 → 6.1.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 (77) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/index.d.ts +21 -1
  3. package/package.json +32 -29
  4. package/packages/datadog-instrumentations/src/ai.js +45 -0
  5. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  6. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  7. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  9. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  10. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  12. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  16. package/packages/datadog-instrumentations/src/jest.js +713 -63
  17. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  18. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  19. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  20. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  21. package/packages/datadog-instrumentations/src/mongoose.js +4 -5
  22. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  23. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
  24. package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
  25. package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
  26. package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
  27. package/packages/datadog-plugin-ai/src/index.js +1 -1
  28. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  29. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  30. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
  31. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  32. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  33. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  34. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  35. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  36. package/packages/datadog-plugin-cucumber/src/index.js +14 -23
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
  38. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  39. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  40. package/packages/datadog-plugin-graphql/src/execute.js +21 -10
  41. package/packages/datadog-plugin-graphql/src/index.js +17 -1
  42. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  43. package/packages/datadog-plugin-graphql/src/validate.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +30 -14
  45. package/packages/datadog-plugin-mocha/src/index.js +39 -14
  46. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  47. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  48. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +3 -2
  50. package/packages/datadog-plugin-vitest/src/index.js +51 -9
  51. package/packages/dd-trace/src/aiguard/index.js +9 -14
  52. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  53. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  54. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  55. package/packages/dd-trace/src/appsec/graphql.js +9 -6
  56. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
  57. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  58. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  59. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
  60. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
  61. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
  62. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  63. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  64. package/packages/dd-trace/src/config/supported-configurations.json +37 -0
  65. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  66. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  68. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  71. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  72. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  73. package/packages/dd-trace/src/plugin_manager.js +16 -0
  74. package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
  75. package/packages/dd-trace/src/plugins/index.js +2 -0
  76. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  77. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
@@ -0,0 +1,363 @@
1
+ 'use strict'
2
+
3
+ const { parseModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
4
+ const BaseLLMObsPlugin = require('../base')
5
+ const {
6
+ getLlmObsSpanName,
7
+ getGenerationMetadataFromEvent,
8
+ getJsonStringValue,
9
+ getToolCallResultContent,
10
+ } = require('./util')
11
+
12
+ // TODO: add in embedMany once it has tracingChannel support
13
+ const SPAN_NAME_TO_KIND_MAPPING = {
14
+ // embeddings
15
+ embed: 'embedding',
16
+ // text generation
17
+ generateText: 'workflow',
18
+ streamText: 'workflow',
19
+ // llm operations
20
+ languageModelCall: 'llm',
21
+ // steps
22
+ step: 'step', // TODO: support step spans for manual instrumentation as well
23
+ // tools
24
+ executeTool: 'tool',
25
+ }
26
+
27
+ function nameFromOperation (operation, event) {
28
+ if (operation === 'executeTool') {
29
+ return event.toolCall?.toolName
30
+ }
31
+
32
+ return operation
33
+ }
34
+
35
+ function formatLanguageModelInputMessages (instructions, messages) {
36
+ if (!Array.isArray(messages)) return
37
+ const inputMessages = []
38
+
39
+ if (instructions) {
40
+ const systemPrompt = typeof instructions === 'string'
41
+ ? instructions
42
+ : Array.isArray(instructions)
43
+ ? instructions.map(instruction => instruction.content).join('')
44
+ : instructions.content
45
+
46
+ inputMessages.push({ role: 'system', content: systemPrompt })
47
+ }
48
+
49
+ for (const message of messages) {
50
+ const { role, content } = message
51
+
52
+ if (role === 'system') {
53
+ inputMessages.push({ role, content })
54
+ } else if (role === 'user') {
55
+ const userMessageContent =
56
+ typeof content === 'string'
57
+ ? content
58
+ : content
59
+ .filter(part => part.type === 'text')
60
+ .map(part => part.text)
61
+ .join('')
62
+
63
+ inputMessages.push({ role, content: userMessageContent })
64
+ } else if (role === 'assistant') {
65
+ if (typeof content === 'string') {
66
+ inputMessages.push({ role, content })
67
+ } else {
68
+ // re-use existing output message formatting
69
+ inputMessages.push(...formatLanguageModelOutputMessages(content))
70
+ }
71
+ } else if (role === 'tool') {
72
+ for (const part of content) {
73
+ if (part.type === 'tool-result') { // TODO: support tool approvals
74
+ const safeResult = getToolCallResultContent(part)
75
+
76
+ inputMessages.push({
77
+ role,
78
+ content: safeResult,
79
+ toolId: part.toolCallId,
80
+ })
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return inputMessages
87
+ }
88
+
89
+ function formatLanguageModelOutputMessages (content) {
90
+ if (!Array.isArray(content)) return
91
+
92
+ const outputMessages = []
93
+ const toolCalls = []
94
+
95
+ let textContent = ''
96
+ let reasoningContent = ''
97
+
98
+ for (const part of content) {
99
+ const { type } = part
100
+
101
+ if (type === 'text') {
102
+ textContent += part.text
103
+ } else if (type === 'reasoning') {
104
+ reasoningContent += part.text
105
+ } else if (type === 'tool-call') {
106
+ const toolCallArguments = typeof part.input === 'string'
107
+ ? getJsonStringValue(part.input, {})
108
+ : part.input
109
+
110
+ toolCalls.push({
111
+ arguments: toolCallArguments,
112
+ name: part.toolName,
113
+ type: 'function',
114
+ toolId: part.toolCallId,
115
+ })
116
+ }
117
+ }
118
+
119
+ if (reasoningContent) {
120
+ outputMessages.push({ role: 'reasoning', content: reasoningContent })
121
+ }
122
+
123
+ const finalTextMessage = { role: 'assistant' }
124
+
125
+ if (textContent) {
126
+ finalTextMessage.content = textContent
127
+ }
128
+
129
+ if (toolCalls.length) {
130
+ finalTextMessage.toolCalls = toolCalls
131
+ }
132
+
133
+ outputMessages.push(finalTextMessage)
134
+
135
+ return outputMessages
136
+ }
137
+
138
+ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
139
+ static id = 'ai_llmobs_vercel_telemetry'
140
+ static integration = 'ai'
141
+ static prefix = 'tracing:ai:telemetry'
142
+
143
+ /** @type {Map<string, Array<Record<string, unknown>>>} */
144
+ #lastOutputContentByCallId = new Map()
145
+
146
+ constructor () {
147
+ super(...arguments)
148
+
149
+ this.addSub('dd-trace:vercel-ai:chunk', ({ ctx, chunk, done }) => {
150
+ ctx.chunks ??= []
151
+ const chunks = ctx.chunks
152
+ if (chunk) chunks.push(chunk)
153
+
154
+ ctx.streamConsumed = done
155
+
156
+ if (!done) return
157
+
158
+ ctx.result ??= {}
159
+ const contentById = {}
160
+
161
+ for (const chunk of chunks) {
162
+ if (chunk.type === 'finish') {
163
+ ctx.result.usage = chunk.usage
164
+ } else if (chunk.type === 'reasoning-start') {
165
+ contentById[chunk.id] = { type: 'reasoning', text: '' }
166
+ } else if (chunk.type === 'text-start') {
167
+ contentById[chunk.id] = { type: 'text', text: '' }
168
+ } else if (chunk.type === 'reasoning-delta' || chunk.type === 'text-delta') {
169
+ if (!contentById[chunk.id]) {
170
+ // special-case handling when the start event did not come through as a chunk
171
+ contentById[chunk.id] = {
172
+ type: chunk.type === 'reasoning-delta' ? 'reasoning' : 'text',
173
+ text: '',
174
+ }
175
+ }
176
+ contentById[chunk.id].text += chunk.delta
177
+ } else if (chunk.type === 'tool-call') {
178
+ contentById[chunk.toolCallId] = chunk
179
+ }
180
+ }
181
+
182
+ const content = Object.values(contentById)
183
+ ctx.result.content = content
184
+
185
+ this.#lastOutputContentByCallId.set(ctx.event.callId, content)
186
+ })
187
+ }
188
+
189
+ /**
190
+ * @override
191
+ */
192
+ asyncEnd (ctx) {
193
+ // check if isStreamed and stream resolved
194
+ // this event will fire multiple times for the same channel
195
+ if (ctx.isStream && ctx.result?.stream && !ctx.streamConsumed) return
196
+
197
+ super.asyncEnd(ctx)
198
+ }
199
+
200
+ /**
201
+ * @override
202
+ */
203
+ getLLMObsSpanRegisterOptions (ctx) {
204
+ const span = ctx.currentStore?.span
205
+ if (!span) return
206
+
207
+ const { type: operation, event } = ctx
208
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
209
+ if (!kind) return
210
+
211
+ const normalizedName = nameFromOperation(operation, event) || operation
212
+
213
+ const options = {
214
+ kind, name: getLlmObsSpanName(normalizedName, event.functionId),
215
+ }
216
+
217
+ if (kind === 'llm' || kind === 'embedding') {
218
+ const modelName = event.modelId
219
+
220
+ options.modelName = modelName
221
+ options.modelProvider = parseModelProvider(event.provider, modelName)
222
+ }
223
+
224
+ return options
225
+ }
226
+
227
+ /**
228
+ * @override
229
+ */
230
+ setLLMObsTags (ctx) {
231
+ const span = ctx.currentStore?.span
232
+ if (!span) return
233
+
234
+ const { type: operation } = ctx
235
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
236
+ if (!kind) return
237
+
238
+ switch (operation) {
239
+ case 'embed':
240
+ this.setEmbeddingTags(span, ctx)
241
+ break
242
+ case 'generateText':
243
+ case 'streamText':
244
+ this.setTextGenerationTags(span, ctx)
245
+ break
246
+ case 'languageModelCall':
247
+ this.setLanguageModelCallTags(span, ctx)
248
+ break
249
+ case 'step':
250
+ this.setStepTags(span, ctx)
251
+ break
252
+ case 'executeTool':
253
+ this.setToolTags(span, ctx)
254
+ break
255
+ default:
256
+ break
257
+ }
258
+ }
259
+
260
+ setEmbeddingTags (span, ctx) {
261
+ const { event, result } = ctx
262
+
263
+ const input = event.value
264
+ const embedding = result?.embedding
265
+ const embeddingTextResult = embedding ? `[1 embedding(s) returned with size ${embedding.length}]` : ''
266
+
267
+ this._tagger.tagEmbeddingIO(span, input, embeddingTextResult)
268
+
269
+ this._tagger.tagMetrics(span, {
270
+ inputTokens: result?.usage?.tokens,
271
+ })
272
+ }
273
+
274
+ setTextGenerationTags (span, ctx) {
275
+ const { event, result } = ctx
276
+
277
+ const lastUserPrompt = event.messages.findLast(message => message.role === 'user')?.content
278
+ const input = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
279
+
280
+ const output =
281
+ ctx.isStream
282
+ ? this.#lastOutputContentByCallId.get(event.callId)?.find(part => part.type === 'text')?.text
283
+ : result?._output
284
+
285
+ this._tagger.tagTextIO(span, input, output)
286
+
287
+ const metadata = getGenerationMetadataFromEvent(event)
288
+ this._tagger.tagMetadata(span, metadata)
289
+
290
+ if (ctx.isStream) {
291
+ this.#lastOutputContentByCallId.delete(event.callId)
292
+ }
293
+ }
294
+
295
+ setStepTags (span, ctx) {
296
+ const { result, event } = ctx
297
+
298
+ const content = ctx.isStream ? this.#lastOutputContentByCallId.get(event.callId) : result?.content
299
+
300
+ // capture reasoning if applicable
301
+ const reasoning = content?.find(part => part.type === 'reasoning')?.text
302
+ if (reasoning) {
303
+ this._tagger.tagTextIO(span, reasoning)
304
+ }
305
+ }
306
+
307
+ setLanguageModelCallTags (span, ctx) {
308
+ const { event, result } = ctx
309
+
310
+ // input messages
311
+ const { instructions, messages } = event
312
+ const inputMessages = formatLanguageModelInputMessages(instructions, messages)
313
+
314
+ // output messages
315
+ const outputMessages =
316
+ result ? formatLanguageModelOutputMessages(result.content) : [{ role: 'assistant', content: '' }]
317
+
318
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
319
+
320
+ // tool definitions
321
+ const { tools } = event
322
+ if (Array.isArray(tools)) {
323
+ this._tagger.tagToolDefinitions(
324
+ span,
325
+ tools.map(({ inputSchema, ...rest }) => ({
326
+ ...rest,
327
+ schema: {
328
+ properties: inputSchema?.properties ?? {},
329
+ required: inputSchema?.required ?? [],
330
+ type: inputSchema?.type ?? '',
331
+ },
332
+ }))
333
+ )
334
+ }
335
+
336
+ // metadata
337
+ const metadata = getGenerationMetadataFromEvent(event)
338
+ this._tagger.tagMetadata(span, metadata)
339
+
340
+ if (!result) return
341
+
342
+ // metrics
343
+ const { usage } = result
344
+ this._tagger.tagMetrics(span, {
345
+ inputTokens: usage?.inputTokens?.total,
346
+ cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
347
+ cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
348
+ outputTokens: usage?.outputTokens?.total,
349
+ reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
350
+ })
351
+ }
352
+
353
+ setToolTags (span, ctx) {
354
+ const { event, result } = ctx
355
+
356
+ const { toolCall } = event
357
+ if (!toolCall) return
358
+
359
+ this._tagger.tagTextIO(span, toolCall.input, result?.output?.output)
360
+ }
361
+ }
362
+
363
+ module.exports = VercelAiTelemetryPlugin
@@ -0,0 +1,270 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('../base')
4
+ const { storage: llmobsStorage } = require('../../storage')
5
+ const { NAME, SESSION_ID } = require('../../constants/tags')
6
+ const { splitModel } = require('../../../../../datadog-plugin-claude-agent-sdk/src/util')
7
+
8
+ const subagentToolIds = new Set()
9
+
10
+ function normalizeToolOutputString (raw) {
11
+ const footerIndex = raw.search(/\s*agentId: [^\s]+ \(use SendMessage\b/)
12
+ if (footerIndex === -1) return raw
13
+
14
+ return raw.slice(0, footerIndex).trimEnd()
15
+ }
16
+
17
+ function getToolOutputText (raw) {
18
+ if (raw == null) return
19
+
20
+ if (Array.isArray(raw)) {
21
+ const output = []
22
+ for (const block of raw) {
23
+ const text = getToolOutputText(block)
24
+ if (text) output.push(text)
25
+ }
26
+ return output.join('\n') || undefined
27
+ }
28
+
29
+ if (raw.type === 'tool_result') return getToolOutputText(raw.content)
30
+ if (raw.type === 'text') return normalizeToolOutputString(raw.text)
31
+ if (raw.type === 'tool_reference') return raw.tool_name
32
+ if (raw.content !== undefined) return getToolOutputText(raw.content)
33
+
34
+ if (typeof raw === 'string') return normalizeToolOutputString(raw)
35
+
36
+ return JSON.stringify(raw)
37
+ }
38
+
39
+ function buildOutputMessages (chunks, llmStartIdx, llmEndIdx) {
40
+ let thinking = ''
41
+ let text = ''
42
+ const toolCalls = []
43
+
44
+ for (let i = llmStartIdx; i < llmEndIdx; i++) {
45
+ const c = chunks[i]
46
+ if (c.type !== 'assistant') continue
47
+ const block = c.message?.content?.[0]
48
+ if (block?.type === 'thinking') thinking += block.thinking ?? ''
49
+ else if (block?.type === 'text') text += block.text ?? ''
50
+ else if (block?.type === 'tool_use') {
51
+ toolCalls.push({ name: block.name, arguments: block.input ?? {}, toolId: block.id, type: block.type })
52
+ }
53
+ }
54
+
55
+ const messages = []
56
+ if (thinking) messages.push({ role: 'thinking', content: thinking })
57
+ const msg = { role: 'assistant', content: text }
58
+ if (toolCalls.length) msg.toolCalls = toolCalls
59
+ messages.push(msg)
60
+ return messages
61
+ }
62
+
63
+ class QueryLLMObsPlugin extends LLMObsPlugin {
64
+ static integration = 'claude-agent-sdk'
65
+ static id = 'llmobs_claude_agent_sdk_query'
66
+ static prefix = 'tracing:orchestrion:@anthropic-ai/claude-agent-sdk:query'
67
+
68
+ getLLMObsSpanRegisterOptions (ctx) {
69
+ return { kind: 'agent' }
70
+ }
71
+
72
+ start (ctx) {
73
+ super.start(ctx)
74
+ if (!this._tracerConfig.llmobs.DD_LLMOBS_ENABLED) return
75
+ const store = llmobsStorage.getStore()
76
+ const prev = ctx.runInContext ?? (fn => fn())
77
+ ctx.runInContext = fn => prev(() => llmobsStorage.run(store, fn))
78
+ }
79
+
80
+ asyncEnd (ctx) {
81
+ if (!ctx.streamResolved) return
82
+ super.asyncEnd(ctx)
83
+ }
84
+
85
+ setLLMObsTags (ctx) {
86
+ const span = ctx.currentStore?.span
87
+ if (!span) return
88
+
89
+ // post-populate session_id
90
+ if (ctx.session_id) this._tagger._setTag(span, SESSION_ID, ctx.session_id)
91
+ this._tagger.tagTextIO(span, ctx.arguments?.[0]?.prompt, ctx.output)
92
+
93
+ // metadata
94
+ const { cwd, permissionMode } = ctx
95
+ const metadata = {}
96
+
97
+ if (cwd) metadata.cwd = cwd
98
+ if (permissionMode) metadata.permissionMode = permissionMode
99
+
100
+ this._tagger.tagMetadata(span, metadata)
101
+ }
102
+ }
103
+
104
+ class StepLlmObsPlugin extends LLMObsPlugin {
105
+ static integration = 'claude-agent-sdk'
106
+ static id = 'claude_agent_sdk_step_llmobs'
107
+ static system = 'claude-agent-sdk'
108
+ static prefix = 'tracing:apm:claude-agent-sdk:step'
109
+
110
+ getLLMObsSpanRegisterOptions (ctx) {
111
+ if (ctx.parentToolUseId) subagentToolIds.add(ctx.parentToolUseId)
112
+ return { kind: 'step', name: `step-${ctx.stepIndex}`, sessionId: ctx.sessionId }
113
+ }
114
+
115
+ end (ctx) {
116
+ super.end(ctx)
117
+ super.asyncEnd(ctx)
118
+ }
119
+
120
+ setLLMObsTags (ctx) {
121
+ const span = ctx.currentStore?.span
122
+ if (!span) return
123
+
124
+ const { chunks, llmStartIdx, llmEndIdx, toolOutputs } = ctx
125
+ if (!chunks) return
126
+
127
+ const outputMessages = buildOutputMessages(chunks, llmStartIdx, llmEndIdx)
128
+ const thinking = outputMessages.find(m => m.role === 'thinking')?.content ?? ''
129
+
130
+ const output = toolOutputs?.length
131
+ ? getToolOutputText(toolOutputs)
132
+ : outputMessages.find(m => m.role === 'assistant')?.content ?? ''
133
+
134
+ this._tagger.tagTextIO(span, thinking, output)
135
+ }
136
+ }
137
+
138
+ class LlmLlmObsPlugin extends LLMObsPlugin {
139
+ static integration = 'claude-agent-sdk'
140
+ static id = 'claude_agent_sdk_llm_llmobs'
141
+ static system = 'claude-agent-sdk'
142
+ static prefix = 'tracing:apm:claude-agent-sdk:llm'
143
+
144
+ getLLMObsSpanRegisterOptions (ctx) {
145
+ const { modelName, modelProvider } = splitModel(ctx.model)
146
+ return { kind: 'llm', name: ctx.model, modelName, modelProvider, sessionId: ctx.sessionId }
147
+ }
148
+
149
+ end (ctx) {
150
+ super.end(ctx)
151
+ super.asyncEnd(ctx)
152
+ }
153
+
154
+ setLLMObsTags (ctx) {
155
+ const span = ctx.currentStore?.span
156
+ if (!span) return
157
+
158
+ const { chunks, llmStartIdx, llmEndIdx, parentToolUseId, initialPrompt, usage } = ctx
159
+
160
+ if (chunks) {
161
+ const inputMessages = this.#buildInputMessages(chunks, llmStartIdx, parentToolUseId, initialPrompt)
162
+ const outputMessages = buildOutputMessages(chunks, llmStartIdx, llmEndIdx)
163
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
164
+ }
165
+
166
+ if (usage) {
167
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
168
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0
169
+ const inputTokens = (usage.input_tokens ?? 0) + cacheWriteTokens + cacheReadTokens
170
+ const outputTokens = usage.output_tokens ?? 0
171
+ this._tagger.tagMetrics(span, {
172
+ input_tokens: inputTokens,
173
+ output_tokens: outputTokens,
174
+ cache_read_input_tokens: cacheReadTokens,
175
+ cache_write_input_tokens: cacheWriteTokens,
176
+ total_tokens: inputTokens + outputTokens,
177
+ })
178
+ }
179
+ }
180
+
181
+ #buildInputMessages (chunks, llmStartIdx, parentToolUseId, initialPrompt) {
182
+ const messages = []
183
+ if (initialPrompt) messages.push({ role: 'user', content: initialPrompt })
184
+ const seenIds = new Set()
185
+
186
+ for (let i = 0; i < llmStartIdx; i++) {
187
+ const c = chunks[i]
188
+ if (c.parent_tool_use_id !== parentToolUseId) continue
189
+
190
+ if (c.type === 'assistant') {
191
+ const msgId = c.message?.id
192
+ if (!msgId || seenIds.has(msgId)) continue
193
+ seenIds.add(msgId)
194
+
195
+ let thinking = ''
196
+ let text = ''
197
+ const toolCalls = []
198
+ for (let j = i; j < llmStartIdx; j++) {
199
+ const cc = chunks[j]
200
+ if (cc.type !== 'assistant' || cc.message?.id !== msgId) break
201
+ const block = cc.message?.content?.[0]
202
+ if (block?.type === 'thinking') thinking += block.thinking ?? ''
203
+ else if (block?.type === 'text') text += block.text ?? ''
204
+ else if (block?.type === 'tool_use') {
205
+ toolCalls.push({ name: block.name, arguments: block.input ?? {}, toolId: block.id, type: block.type })
206
+ }
207
+ }
208
+ if (thinking) messages.push({ role: 'thinking', content: thinking })
209
+ const msg = { role: 'assistant', content: text }
210
+ if (toolCalls.length) msg.toolCalls = toolCalls
211
+ messages.push(msg)
212
+ } else if (c.type === 'user') {
213
+ const content = c.message?.content
214
+ if (!content) continue
215
+ for (const block of content) {
216
+ if (block.type === 'text') {
217
+ messages.push({ role: 'user', content: block.text ?? '' })
218
+ } else if (block.type === 'tool_result') {
219
+ const text = getToolOutputText(block.content) ?? ''
220
+ messages.push({ role: 'tool', content: text })
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return messages
227
+ }
228
+ }
229
+
230
+ class ToolLlmObsPlugin extends LLMObsPlugin {
231
+ static integration = 'claude-agent-sdk'
232
+ static id = 'claude_agent_sdk_tool_llmobs'
233
+ static system = 'claude-agent-sdk'
234
+ static prefix = 'tracing:apm:claude-agent-sdk:tool'
235
+
236
+ getLLMObsSpanRegisterOptions (ctx) {
237
+ return { kind: 'tool', name: ctx.name, sessionId: ctx.sessionId }
238
+ }
239
+
240
+ end (ctx) {
241
+ super.end(ctx)
242
+ super.asyncEnd(ctx)
243
+ }
244
+
245
+ setLLMObsTags (ctx) {
246
+ const span = ctx.currentStore?.span
247
+ if (!span) return
248
+
249
+ if (subagentToolIds.has(ctx.id)) {
250
+ subagentToolIds.delete(ctx.id)
251
+ const description = ctx.input?.description
252
+ this._tagger.changeKind(span, 'agent')
253
+ if (description) this._tagger._setTag(span, NAME, `${ctx.name} (${description})`)
254
+ const output = getToolOutputText(ctx.output)
255
+ this._tagger.tagTextIO(span, ctx.input?.prompt, output)
256
+ return
257
+ }
258
+
259
+ const input = ctx.input ? JSON.stringify(ctx.input) : undefined
260
+ const output = getToolOutputText(ctx.output)
261
+ this._tagger.tagTextIO(span, input, output)
262
+ }
263
+ }
264
+
265
+ module.exports = [
266
+ QueryLLMObsPlugin,
267
+ StepLlmObsPlugin,
268
+ ToolLlmObsPlugin,
269
+ LlmLlmObsPlugin,
270
+ ]
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ function splitModel (model) {
4
+ if (!model) return { modelName: undefined, modelProvider: 'anthropic' }
5
+ const idx = model.indexOf('/')
6
+ if (idx === -1) return { modelName: model, modelProvider: 'anthropic' }
7
+ return { modelName: model.slice(idx + 1), modelProvider: model.slice(0, idx) }
8
+ }
9
+
10
+ module.exports = { splitModel }
@@ -152,6 +152,11 @@ class Span extends BridgeSpanBase {
152
152
  hostname: _tracer._hostname,
153
153
  integrationName: parentTracer?._isOtelLibrary ? 'otel.library' : 'otel',
154
154
  tags: {
155
+ // Apply global/resource tags (DD_TAGS, OTEL_RESOURCE_ATTRIBUTES) as
156
+ // defaults, mirroring the native path in opentracing/tracer.js. The
157
+ // explicit service/resource/span.kind below take precedence, and any
158
+ // user-set OTel attributes applied via setAttributes() still win.
159
+ ..._tracer._config.tags,
155
160
  [SERVICE_NAME]: _tracer._service,
156
161
  [RESOURCE_NAME]: spanName,
157
162
  [SPAN_KIND]: spanKindNames[kind],
@@ -169,6 +169,10 @@ module.exports = class PluginManager {
169
169
  clientIpHeader,
170
170
  DD_TRACE_MEMCACHED_COMMAND_ENABLED,
171
171
  DD_TRACE_OTEL_SEMANTICS_ENABLED,
172
+ DD_TRACE_GRAPHQL_COLLAPSE,
173
+ DD_TRACE_GRAPHQL_DEPTH,
174
+ DD_TRACE_GRAPHQL_VARIABLES,
175
+ DD_TRACE_GRAPHQL_ERROR_EXTENSIONS,
172
176
  DD_TEST_SESSION_NAME,
173
177
  DD_AGENTLESS_LOG_SUBMISSION_ENABLED,
174
178
  testOptimization,
@@ -225,6 +229,18 @@ module.exports = class PluginManager {
225
229
  sharedConfig.middleware = middlewareTracingEnabled
226
230
  }
227
231
 
232
+ // The graphql `DD_TRACE_GRAPHQL_*` options are global on purpose: they feed
233
+ // the plugin config as a base that a programmatic `tracer.use('graphql', …)`
234
+ // overrides, and stay on the Config singleton so remote config and config
235
+ // telemetry observe them. Forwarded only for graphql so other plugins do not
236
+ // carry keys they ignore. The plugin-facing names drop the prefix.
237
+ if (name === 'graphql') {
238
+ sharedConfig.collapse = DD_TRACE_GRAPHQL_COLLAPSE
239
+ sharedConfig.depth = DD_TRACE_GRAPHQL_DEPTH
240
+ sharedConfig.variables = DD_TRACE_GRAPHQL_VARIABLES
241
+ sharedConfig.errorExtensions = DD_TRACE_GRAPHQL_ERROR_EXTENSIONS
242
+ }
243
+
228
244
  return sharedConfig
229
245
  }
230
246
  }