dd-trace 6.0.0 → 6.2.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 +28 -1
- package/initialize.mjs +4 -2
- package/package.json +34 -30
- 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 +3 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
- 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 +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mercurius.js +11 -0
- 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/otel-sdk-trace.js +15 -0
- 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 +26 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +103 -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 +25 -25
- package/packages/datadog-plugin-graphql/src/index.js +23 -1
- package/packages/datadog-plugin-graphql/src/request.js +104 -0
- package/packages/datadog-plugin-graphql/src/utils.js +186 -2
- package/packages/datadog-plugin-graphql/src/validate.js +25 -2
- 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 +43 -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/appsec/lambda.js +8 -8
- 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/datastreams/writer.js +14 -2
- package/packages/dd-trace/src/encode/0.4.js +23 -2
- package/packages/dd-trace/src/encode/0.5.js +12 -1
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
- package/packages/dd-trace/src/exporters/common/writer.js +17 -1
- package/packages/dd-trace/src/guardrails/index.js +3 -1
- package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
- 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/msgpack/chunk.js +33 -1
- package/packages/dd-trace/src/msgpack/index.js +6 -1
- 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 +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
- package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
|
@@ -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 }
|
|
@@ -10,6 +10,33 @@ const SHRINK_AFTER_FLUSHES = 32
|
|
|
10
10
|
// shape: after a halving step the post-shrink fill is the prior peak doubled,
|
|
11
11
|
// still under 50 %.
|
|
12
12
|
const SHRINK_USAGE_RATIO = 4
|
|
13
|
+
// Hard cap on chunk growth. The agent's trace intake rejects payloads over
|
|
14
|
+
// 50 MiB, so anything past that is dead on arrival anyway. A pathological
|
|
15
|
+
// single trace (unsanitized meta tag the size of a media file, multi-MB
|
|
16
|
+
// stack trace) used to grow the buffer without limit until either the
|
|
17
|
+
// allocation failed or the process tripped its memory ceiling. `reserve`
|
|
18
|
+
// now refuses the growth with a tagged `RangeError`; `AgentEncoder` catches
|
|
19
|
+
// it, resets the in-flight payload, and logs.
|
|
20
|
+
const MAX_SIZE = 50 * 1024 * 1024 // 50 MiB
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when a chunk — or an assembled payload stitched from several chunks —
|
|
24
|
+
* would cross `MAX_SIZE`. Shared so the `reserve` cap and the per-encoder
|
|
25
|
+
* assembled-size guards (`0.5`, agentless CI Visibility) raise the same `code`,
|
|
26
|
+
* which every writer's `flush` recognises to drop the payload instead of
|
|
27
|
+
* crashing the host.
|
|
28
|
+
*/
|
|
29
|
+
class OverflowError extends RangeError {
|
|
30
|
+
code = 'ERR_MSGPACK_CHUNK_OVERFLOW'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {number} needed Requested total byte size.
|
|
34
|
+
*/
|
|
35
|
+
constructor (needed) {
|
|
36
|
+
super(`MsgpackChunk capped at ${MAX_SIZE} bytes; requested ${needed}`)
|
|
37
|
+
this.name = 'OverflowError'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
13
40
|
|
|
14
41
|
/**
|
|
15
42
|
* Resizable msgpack write buffer. Owns the byte-layout primitives the encoder
|
|
@@ -100,11 +127,14 @@ class MsgpackChunk {
|
|
|
100
127
|
const needed = this.length + size
|
|
101
128
|
|
|
102
129
|
if (needed > this.buffer.length) {
|
|
130
|
+
if (needed > MAX_SIZE) {
|
|
131
|
+
throw new OverflowError(needed)
|
|
132
|
+
}
|
|
103
133
|
let newSize = this.buffer.length
|
|
104
134
|
// `*= 2` instead of `<<= 1`: `1073741824 << 1` is negative as int32,
|
|
105
135
|
// and msgpack values can legitimately reach the multi-GiB range.
|
|
106
136
|
while (newSize < needed) newSize *= 2
|
|
107
|
-
this.#resize(newSize)
|
|
137
|
+
this.#resize(Math.min(newSize, MAX_SIZE))
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
this.length += size
|
|
@@ -454,3 +484,5 @@ class MsgpackChunk {
|
|
|
454
484
|
}
|
|
455
485
|
|
|
456
486
|
module.exports = MsgpackChunk
|
|
487
|
+
module.exports.MAX_SIZE = MAX_SIZE
|
|
488
|
+
module.exports.OverflowError = OverflowError
|