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.
- package/LICENSE-3rdparty.csv +1 -2
- package/index.d.ts +21 -1
- package/package.json +32 -29
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +21 -10
- package/packages/datadog-plugin-graphql/src/index.js +17 -1
- package/packages/datadog-plugin-graphql/src/utils.js +9 -2
- package/packages/datadog-plugin-graphql/src/validate.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +51 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- 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
|
}
|