dd-trace 5.95.0 → 5.96.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/index.d.ts +9 -0
- package/package.json +2 -2
- package/packages/datadog-instrumentations/src/ai.js +112 -0
- package/packages/datadog-instrumentations/src/helpers/ai-messages.js +182 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +10 -0
- package/packages/dd-trace/src/aiguard/index.js +64 -0
- package/packages/dd-trace/src/ci-visibility/lage.js +39 -0
- package/packages/dd-trace/src/config/index.js +5 -2
- package/packages/dd-trace/src/config/supported-configurations.json +17 -0
- package/packages/dd-trace/src/constants.js +1 -0
- package/packages/dd-trace/src/exporter.js +5 -2
- package/packages/dd-trace/src/llmobs/constants/text.js +3 -0
- package/packages/dd-trace/src/llmobs/index.js +9 -4
- package/packages/dd-trace/src/plugins/util/test.js +5 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/startup-log.js +9 -0
package/index.d.ts
CHANGED
|
@@ -786,6 +786,15 @@ declare namespace tracer {
|
|
|
786
786
|
* Programmatic configuration takes precedence over the environment variables listed above.
|
|
787
787
|
*/
|
|
788
788
|
enabled?: boolean,
|
|
789
|
+
/**
|
|
790
|
+
* Whether to request blocking mode when evaluating prompts via auto-instrumentation.
|
|
791
|
+
* When `true`, AI Guard will block requests that violate security policies.
|
|
792
|
+
* When `false`, AI Guard evaluates but never blocks (monitor-only mode).
|
|
793
|
+
* @default false
|
|
794
|
+
* @env DD_AI_GUARD_BLOCK
|
|
795
|
+
* Programmatic configuration takes precedence over the environment variables listed above.
|
|
796
|
+
*/
|
|
797
|
+
block?: boolean,
|
|
789
798
|
/**
|
|
790
799
|
* URL of the AI Guard REST API.
|
|
791
800
|
* @env DD_AI_GUARD_ENDPOINT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.96.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
],
|
|
140
140
|
"dependencies": {
|
|
141
141
|
"dc-polyfill": "^0.1.10",
|
|
142
|
-
"import-in-the-middle": "^3.0.
|
|
142
|
+
"import-in-the-middle": "^3.0.1"
|
|
143
143
|
},
|
|
144
144
|
"optionalDependencies": {
|
|
145
145
|
"@datadog/libdatadog": "0.9.2",
|
|
@@ -3,11 +3,113 @@
|
|
|
3
3
|
const { channel, tracingChannel } = require('dc-polyfill')
|
|
4
4
|
const shimmer = require('../../datadog-shimmer')
|
|
5
5
|
const { addHook, getHooks } = require('./helpers/instrument')
|
|
6
|
+
const { convertVercelPromptToMessages, buildOutputMessages } = require('./helpers/ai-messages')
|
|
6
7
|
|
|
7
8
|
const vercelAiTracingChannel = tracingChannel('dd-trace:vercel-ai')
|
|
8
9
|
const vercelAiSpanSetAttributesChannel = channel('dd-trace:vercel-ai:span:setAttributes')
|
|
10
|
+
const aiguardChannel = channel('dd-trace:ai:aiguard')
|
|
9
11
|
|
|
10
12
|
const tracers = new WeakSet()
|
|
13
|
+
const wrappedModels = new WeakSet()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Publishes already-converted AI guard style messages to the AIGuard channel.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<object>} messages - AI guard style messages to evaluate
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
function publishToAIGuard (messages) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
aiguardChannel.publish({ messages, resolve, reject })
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps a Vercel AI language model's doGenerate and doStream methods to evaluate
|
|
29
|
+
* messages with AIGuard.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} model - A Vercel AI language model instance
|
|
32
|
+
*/
|
|
33
|
+
function wrapModelWithAIGuard (model) {
|
|
34
|
+
if (!model || wrappedModels.has(model)) return
|
|
35
|
+
wrappedModels.add(model)
|
|
36
|
+
|
|
37
|
+
if (typeof model.doGenerate === 'function') {
|
|
38
|
+
shimmer.wrap(model, 'doGenerate', function (original) {
|
|
39
|
+
return function (options) {
|
|
40
|
+
const originalResult = original.call(this, options)
|
|
41
|
+
|
|
42
|
+
if (!aiguardChannel.hasSubscribers) return originalResult
|
|
43
|
+
if (!options.prompt?.length) return originalResult
|
|
44
|
+
|
|
45
|
+
const inputMessages = convertVercelPromptToMessages(options.prompt)
|
|
46
|
+
if (!inputMessages.length) return originalResult
|
|
47
|
+
|
|
48
|
+
// Run AI Guard input evaluation and LLM call in parallel.
|
|
49
|
+
// The LLM has no side effects so it is safe to discard its result if AI Guard blocks.
|
|
50
|
+
return Promise.all([publishToAIGuard(inputMessages), originalResult])
|
|
51
|
+
.then(([, result]) => {
|
|
52
|
+
if (!result.content?.length) return result
|
|
53
|
+
return publishToAIGuard(buildOutputMessages(inputMessages, result.content))
|
|
54
|
+
.then(() => result)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof model.doStream === 'function') {
|
|
61
|
+
shimmer.wrap(model, 'doStream', function (original) {
|
|
62
|
+
return function (options) {
|
|
63
|
+
const originalResult = original.call(this, options)
|
|
64
|
+
|
|
65
|
+
if (!aiguardChannel.hasSubscribers) return originalResult
|
|
66
|
+
if (!options.prompt?.length) return originalResult
|
|
67
|
+
|
|
68
|
+
const inputMessages = convertVercelPromptToMessages(options.prompt)
|
|
69
|
+
if (!inputMessages.length) return originalResult
|
|
70
|
+
|
|
71
|
+
// Run AI Guard input evaluation and LLM call in parallel.
|
|
72
|
+
// The LLM has no side effects so it is safe to discard its result if AI Guard blocks.
|
|
73
|
+
return Promise.all([publishToAIGuard(inputMessages), originalResult])
|
|
74
|
+
.then(([, result]) => {
|
|
75
|
+
const chunks = []
|
|
76
|
+
const reader = result.stream.getReader()
|
|
77
|
+
|
|
78
|
+
function readAll () {
|
|
79
|
+
return reader.read().then(({ done, value }) => {
|
|
80
|
+
if (done) return
|
|
81
|
+
chunks.push(value)
|
|
82
|
+
return readAll()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return readAll().then(() => {
|
|
87
|
+
const toolCalls = chunks.filter(c => c?.type === 'tool-call')
|
|
88
|
+
const text = chunks.filter(c => c?.type === 'text-delta').map(c => c.textDelta).join('')
|
|
89
|
+
const content = toolCalls.length ? toolCalls : text ? [{ type: 'text', text }] : []
|
|
90
|
+
|
|
91
|
+
const evaluate = content.length
|
|
92
|
+
? publishToAIGuard(buildOutputMessages(inputMessages, content))
|
|
93
|
+
: Promise.resolve()
|
|
94
|
+
|
|
95
|
+
return evaluate.then(() => {
|
|
96
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
97
|
+
const stream = new ReadableStream({
|
|
98
|
+
start (controller) {
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
controller.enqueue(chunk)
|
|
101
|
+
}
|
|
102
|
+
controller.close()
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
return { ...result, stream }
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
11
113
|
|
|
12
114
|
function wrapTracer (tracer) {
|
|
13
115
|
if (tracers.has(tracer)) {
|
|
@@ -114,6 +216,16 @@ for (const hook of getHooks('ai')) {
|
|
|
114
216
|
},
|
|
115
217
|
})
|
|
116
218
|
|
|
219
|
+
// resolveLanguageModel is called by all LLM entry points (generateText, streamText,
|
|
220
|
+
// generateObject, streamObject)
|
|
221
|
+
tracingChannel('orchestrion:ai:resolveLanguageModel').subscribe({
|
|
222
|
+
end (ctx) {
|
|
223
|
+
wrapModelWithAIGuard(ctx.result)
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
117
227
|
return exports
|
|
118
228
|
})
|
|
119
229
|
}
|
|
230
|
+
|
|
231
|
+
module.exports = { wrapModelWithAIGuard }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a LanguageModelV2FilePart with an image mediaType to an AI guard style image_url content part.
|
|
5
|
+
*
|
|
6
|
+
* @param {{type: 'file', data: URL|string|Uint8Array, mediaType: string}} part
|
|
7
|
+
* @returns {{type: 'image_url', image_url: {url: string}}|undefined}
|
|
8
|
+
*/
|
|
9
|
+
function convertFilePartToImageUrl (part) {
|
|
10
|
+
const { data, mediaType } = part
|
|
11
|
+
|
|
12
|
+
if (data instanceof URL) {
|
|
13
|
+
return { type: 'image_url', image_url: { url: data.toString() } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof data === 'string') {
|
|
17
|
+
if (data.startsWith('http') || data.startsWith('data:')) {
|
|
18
|
+
return { type: 'image_url', image_url: { url: data } }
|
|
19
|
+
}
|
|
20
|
+
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${data}` } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (data instanceof Uint8Array) {
|
|
24
|
+
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${Buffer.from(data).toString('base64')}` } }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts a LanguageModelV2Prompt to the AI guard style message format.
|
|
30
|
+
*
|
|
31
|
+
* Vercel AI v2 prompt entries use content arrays with typed parts (e.g. { type: 'text', text },
|
|
32
|
+
* { type: 'file', data, mediaType }). This function converts them to AI guard style messages.
|
|
33
|
+
* When file parts with image media types are present, the content is an array of text and
|
|
34
|
+
* image_url parts; otherwise it is a plain string.
|
|
35
|
+
*
|
|
36
|
+
* @param {Array<{role: string, content: string|Array<{type: string}>}>} prompt
|
|
37
|
+
* @returns {Array<{role: string, content?: string|Array<{type: string}>, tool_calls?: Array, tool_call_id?: string}>}
|
|
38
|
+
*/
|
|
39
|
+
function convertVercelPromptToMessages (prompt) {
|
|
40
|
+
if (!Array.isArray(prompt)) return []
|
|
41
|
+
|
|
42
|
+
const messages = []
|
|
43
|
+
for (const msg of prompt) {
|
|
44
|
+
switch (msg.role) {
|
|
45
|
+
case 'system':
|
|
46
|
+
messages.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' })
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
case 'user': {
|
|
50
|
+
if (!Array.isArray(msg.content)) break
|
|
51
|
+
|
|
52
|
+
const contentParts = []
|
|
53
|
+
for (const part of msg.content) {
|
|
54
|
+
if (part.type === 'text') {
|
|
55
|
+
contentParts.push({ type: 'text', text: part.text })
|
|
56
|
+
} else if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
|
|
57
|
+
const converted = convertFilePartToImageUrl(part)
|
|
58
|
+
if (converted) contentParts.push(converted)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (contentParts.length === 0) break
|
|
63
|
+
|
|
64
|
+
const hasImages = contentParts.some(p => p.type === 'image_url')
|
|
65
|
+
if (hasImages) {
|
|
66
|
+
messages.push({ role: 'user', content: contentParts })
|
|
67
|
+
} else {
|
|
68
|
+
messages.push({ role: 'user', content: contentParts.map(p => p.text).join('\n') })
|
|
69
|
+
}
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'assistant': {
|
|
74
|
+
const textParts = []
|
|
75
|
+
const toolCalls = []
|
|
76
|
+
if (!Array.isArray(msg.content)) break
|
|
77
|
+
|
|
78
|
+
for (const part of msg.content) {
|
|
79
|
+
if (part.type === 'text') {
|
|
80
|
+
textParts.push(part.text)
|
|
81
|
+
} else if (part.type === 'tool-call') {
|
|
82
|
+
const args = part.args ?? part.input
|
|
83
|
+
toolCalls.push({
|
|
84
|
+
id: part.toolCallId,
|
|
85
|
+
function: {
|
|
86
|
+
name: part.toolName,
|
|
87
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args),
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (toolCalls.length > 0) {
|
|
94
|
+
messages.push({ role: 'assistant', tool_calls: toolCalls })
|
|
95
|
+
} else if (textParts.length > 0) {
|
|
96
|
+
messages.push({ role: 'assistant', content: textParts.join('\n') })
|
|
97
|
+
}
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'tool': {
|
|
102
|
+
if (!Array.isArray(msg.content)) break
|
|
103
|
+
|
|
104
|
+
for (const part of msg.content) {
|
|
105
|
+
if (part.type === 'tool-result') {
|
|
106
|
+
const result = part.result ?? part.output
|
|
107
|
+
messages.push({
|
|
108
|
+
role: 'tool',
|
|
109
|
+
tool_call_id: part.toolCallId,
|
|
110
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return messages
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Converts LLM output tool calls to AI guard style message format.
|
|
123
|
+
*
|
|
124
|
+
* @param {Array<object>} inputMessages - The input messages already in AI guard style format
|
|
125
|
+
* @param {Array<{toolCallId: string, toolName: string, args?: unknown, input?: unknown}>} toolCalls
|
|
126
|
+
* @returns {Array<object>}
|
|
127
|
+
*/
|
|
128
|
+
function buildToolCallOutputMessages (inputMessages, toolCalls) {
|
|
129
|
+
return [
|
|
130
|
+
...inputMessages,
|
|
131
|
+
{
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
tool_calls: toolCalls.map(tc => {
|
|
134
|
+
const args = tc.args ?? tc.input
|
|
135
|
+
return {
|
|
136
|
+
id: tc.toolCallId,
|
|
137
|
+
function: {
|
|
138
|
+
name: tc.toolName,
|
|
139
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds OpenAI-style output messages for the assistant's text response.
|
|
149
|
+
*
|
|
150
|
+
* @param {Array<object>} inputMessages - The input messages already in AI guard style format
|
|
151
|
+
* @param {string} text - The assistant's text response
|
|
152
|
+
* @returns {Array<object>}
|
|
153
|
+
*/
|
|
154
|
+
function buildTextOutputMessages (inputMessages, text) {
|
|
155
|
+
return [
|
|
156
|
+
...inputMessages,
|
|
157
|
+
{ role: 'assistant', content: text },
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parses a Vercel AI content array and dispatches to the appropriate output message builder.
|
|
163
|
+
*
|
|
164
|
+
* @param {Array<object>} inputMessages - The input messages already in AI guard style format
|
|
165
|
+
* @param {Array<{type: string}>} content - Vercel AI content array from doGenerate/doStream result
|
|
166
|
+
* @returns {Array<object>}
|
|
167
|
+
*/
|
|
168
|
+
function buildOutputMessages (inputMessages, content) {
|
|
169
|
+
const toolCalls = content.filter(c => c.type === 'tool-call')
|
|
170
|
+
const text = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
171
|
+
if (toolCalls.length) return buildToolCallOutputMessages(inputMessages, toolCalls)
|
|
172
|
+
if (text) return buildTextOutputMessages(inputMessages, text)
|
|
173
|
+
return inputMessages
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
convertVercelPromptToMessages,
|
|
178
|
+
convertFilePartToImageUrl,
|
|
179
|
+
buildToolCallOutputMessages,
|
|
180
|
+
buildTextOutputMessages,
|
|
181
|
+
buildOutputMessages,
|
|
182
|
+
}
|
|
@@ -75,6 +75,31 @@ module.exports = [
|
|
|
75
75
|
},
|
|
76
76
|
channelName: 'selectTelemetryAttributes',
|
|
77
77
|
},
|
|
78
|
+
// resolveLanguageModel called by all LLM entry points, its result is the resolved model instance.
|
|
79
|
+
{
|
|
80
|
+
module: {
|
|
81
|
+
name: 'ai',
|
|
82
|
+
versionRange: '>=6.0.0',
|
|
83
|
+
filePath: 'dist/index.js',
|
|
84
|
+
},
|
|
85
|
+
functionQuery: {
|
|
86
|
+
functionName: 'resolveLanguageModel',
|
|
87
|
+
kind: 'Sync',
|
|
88
|
+
},
|
|
89
|
+
channelName: 'resolveLanguageModel',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
module: {
|
|
93
|
+
name: 'ai',
|
|
94
|
+
versionRange: '>=6.0.0',
|
|
95
|
+
filePath: 'dist/index.mjs',
|
|
96
|
+
},
|
|
97
|
+
functionQuery: {
|
|
98
|
+
functionName: 'resolveLanguageModel',
|
|
99
|
+
kind: 'Sync',
|
|
100
|
+
},
|
|
101
|
+
channelName: 'resolveLanguageModel',
|
|
102
|
+
},
|
|
78
103
|
// tool
|
|
79
104
|
{
|
|
80
105
|
module: {
|
|
@@ -384,6 +384,11 @@ function getOnTestEndHandler (config) {
|
|
|
384
384
|
})
|
|
385
385
|
} else if (ctx) { // if there is an afterEach to run, let's store the finalStatus for getOnHookEndHandler
|
|
386
386
|
ctx.finalStatus = finalStatus
|
|
387
|
+
ctx.hasFailedAllRetries = hasFailedAllRetries
|
|
388
|
+
ctx.attemptToFixPassed = attemptToFixPassed
|
|
389
|
+
ctx.attemptToFixFailed = attemptToFixFailed
|
|
390
|
+
ctx.isAttemptToFixRetry = isAttemptToFixRetry
|
|
391
|
+
ctx.isAtrRetry = isAtrRetry
|
|
387
392
|
}
|
|
388
393
|
}
|
|
389
394
|
}
|
|
@@ -404,6 +409,11 @@ function getOnHookEndHandler () {
|
|
|
404
409
|
status,
|
|
405
410
|
hasBeenRetried: isMochaRetry(test),
|
|
406
411
|
isLastRetry: getIsLastRetry(test),
|
|
412
|
+
hasFailedAllRetries: ctx.hasFailedAllRetries,
|
|
413
|
+
attemptToFixPassed: ctx.attemptToFixPassed,
|
|
414
|
+
attemptToFixFailed: ctx.attemptToFixFailed,
|
|
415
|
+
isAttemptToFixRetry: ctx.isAttemptToFixRetry,
|
|
416
|
+
isAtrRetry: ctx.isAtrRetry,
|
|
407
417
|
...ctx.currentStore,
|
|
408
418
|
finalStatus: ctx.finalStatus,
|
|
409
419
|
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { channel } = require('dc-polyfill')
|
|
4
|
+
const log = require('../log')
|
|
5
|
+
const AIGuard = require('./sdk')
|
|
6
|
+
|
|
7
|
+
const aiguardChannel = channel('dd-trace:ai:aiguard')
|
|
8
|
+
|
|
9
|
+
let isEnabled = false
|
|
10
|
+
let aiguard
|
|
11
|
+
let block
|
|
12
|
+
|
|
13
|
+
function enable (tracer, config) {
|
|
14
|
+
if (isEnabled) return
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
aiguard = new AIGuard(tracer, config)
|
|
18
|
+
block = config.experimental?.aiguard?.block !== false
|
|
19
|
+
|
|
20
|
+
aiguardChannel.subscribe(onEvaluate)
|
|
21
|
+
|
|
22
|
+
isEnabled = true
|
|
23
|
+
} catch (err) {
|
|
24
|
+
log.error('AIGuard: unexpected error during initialization: %s', err.message)
|
|
25
|
+
disable()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function disable () {
|
|
30
|
+
if (!isEnabled) return
|
|
31
|
+
|
|
32
|
+
aiguardChannel.unsubscribe(onEvaluate)
|
|
33
|
+
|
|
34
|
+
aiguard = undefined
|
|
35
|
+
isEnabled = false
|
|
36
|
+
block = false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handles channel messages with pre-converted messages.
|
|
41
|
+
*
|
|
42
|
+
* @param {{messages: Array<object>, resolve: Function, reject: Function}} ctx
|
|
43
|
+
*/
|
|
44
|
+
function onEvaluate (ctx) {
|
|
45
|
+
if (!ctx.messages?.length) {
|
|
46
|
+
ctx.resolve()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
aiguard.evaluate(ctx.messages, { block })
|
|
51
|
+
.then(() => {
|
|
52
|
+
ctx.resolve()
|
|
53
|
+
})
|
|
54
|
+
.catch(err => {
|
|
55
|
+
if (err.name === 'AIGuardAbortError') {
|
|
56
|
+
ctx.reject(err)
|
|
57
|
+
} else {
|
|
58
|
+
log.error('AIGuard: unexpected error during evaluation: %s', err.message)
|
|
59
|
+
ctx.resolve()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { enable, disable }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getEnvironmentVariable } = require('../config/helper')
|
|
4
|
+
const { isTrue } = require('../util')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the current Lage package name if the Lage package name override is enabled.
|
|
8
|
+
*
|
|
9
|
+
* @returns {string|undefined}
|
|
10
|
+
*/
|
|
11
|
+
function getLagePackageName () {
|
|
12
|
+
if (!isTrue(getEnvironmentVariable('DD_ENABLE_LAGE_PACKAGE_NAME'))) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const packageName = getEnvironmentVariable('LAGE_PACKAGE_NAME')
|
|
17
|
+
if (!packageName) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return packageName
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the current Lage package name as the test session name unless the user set one explicitly.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string|undefined}
|
|
28
|
+
*/
|
|
29
|
+
function getLageTestSessionName () {
|
|
30
|
+
if (getEnvironmentVariable('DD_TEST_SESSION_NAME')) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return getLagePackageName()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
getLageTestSessionName,
|
|
39
|
+
}
|
|
@@ -21,7 +21,7 @@ const {
|
|
|
21
21
|
getIsAzureFunction,
|
|
22
22
|
enableGCPPubSubPushSubscription,
|
|
23
23
|
} = require('../serverless')
|
|
24
|
-
const { ORIGIN_KEY } = require('../constants')
|
|
24
|
+
const { ORIGIN_KEY, DATADOG_MINI_AGENT_PATH } = require('../constants')
|
|
25
25
|
const { appendRules } = require('../payload-tagging/config')
|
|
26
26
|
const { getGitMetadataFromGitProperties, removeUserSensitiveInfo, getRemoteOriginURL, resolveGitHeadSHA } =
|
|
27
27
|
require('./git_properties')
|
|
@@ -251,6 +251,7 @@ class Config {
|
|
|
251
251
|
const {
|
|
252
252
|
AWS_LAMBDA_FUNCTION_NAME,
|
|
253
253
|
DD_AGENT_HOST,
|
|
254
|
+
DD_AI_GUARD_BLOCK,
|
|
254
255
|
DD_AI_GUARD_ENABLED,
|
|
255
256
|
DD_AI_GUARD_ENDPOINT,
|
|
256
257
|
DD_AI_GUARD_MAX_CONTENT_SIZE,
|
|
@@ -608,6 +609,7 @@ class Config {
|
|
|
608
609
|
maybeInt(DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS)
|
|
609
610
|
}
|
|
610
611
|
setBoolean(target, 'traceEnabled', DD_TRACE_ENABLED)
|
|
612
|
+
setBoolean(target, 'experimental.aiguard.block', DD_AI_GUARD_BLOCK)
|
|
611
613
|
setBoolean(target, 'experimental.aiguard.enabled', DD_AI_GUARD_ENABLED)
|
|
612
614
|
setString(target, 'experimental.aiguard.endpoint', DD_AI_GUARD_ENDPOINT)
|
|
613
615
|
target['experimental.aiguard.maxContentSize'] = maybeInt(DD_AI_GUARD_MAX_CONTENT_SIZE)
|
|
@@ -618,7 +620,7 @@ class Config {
|
|
|
618
620
|
unprocessedTarget['experimental.aiguard.timeout'] = DD_AI_GUARD_TIMEOUT
|
|
619
621
|
setBoolean(target, 'experimental.enableGetRumData', DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED)
|
|
620
622
|
setString(target, 'experimental.exporter', DD_TRACE_EXPERIMENTAL_EXPORTER)
|
|
621
|
-
if (AWS_LAMBDA_FUNCTION_NAME) {
|
|
623
|
+
if (AWS_LAMBDA_FUNCTION_NAME && !fs.existsSync(DATADOG_MINI_AGENT_PATH)) {
|
|
622
624
|
target.flushInterval = 0
|
|
623
625
|
} else if (DD_TRACE_FLUSH_INTERVAL) {
|
|
624
626
|
target.flushInterval = maybeInt(DD_TRACE_FLUSH_INTERVAL)
|
|
@@ -939,6 +941,7 @@ class Config {
|
|
|
939
941
|
this.#optsUnprocessed['dynamicInstrumentation.uploadIntervalSeconds'] =
|
|
940
942
|
options.dynamicInstrumentation?.uploadIntervalSeconds
|
|
941
943
|
setString(opts, 'env', options.env || tags.env)
|
|
944
|
+
setBoolean(opts, 'experimental.aiguard.block', options.experimental?.aiguard?.block)
|
|
942
945
|
setBoolean(opts, 'experimental.aiguard.enabled', options.experimental?.aiguard?.enabled)
|
|
943
946
|
setString(opts, 'experimental.aiguard.endpoint', options.experimental?.aiguard?.endpoint)
|
|
944
947
|
opts['experimental.aiguard.maxMessagesLength'] = maybeInt(options.experimental?.aiguard?.maxMessagesLength)
|
|
@@ -46,6 +46,16 @@
|
|
|
46
46
|
]
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
+
"DD_AI_GUARD_BLOCK": [
|
|
50
|
+
{
|
|
51
|
+
"implementation": "B",
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"configurationNames": [
|
|
54
|
+
"experimental.aiguard.block"
|
|
55
|
+
],
|
|
56
|
+
"default": "false"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
49
59
|
"DD_AI_GUARD_ENABLED": [
|
|
50
60
|
{
|
|
51
61
|
"implementation": "A",
|
|
@@ -510,6 +520,13 @@
|
|
|
510
520
|
]
|
|
511
521
|
}
|
|
512
522
|
],
|
|
523
|
+
"DD_ENABLE_LAGE_PACKAGE_NAME": [
|
|
524
|
+
{
|
|
525
|
+
"implementation": "A",
|
|
526
|
+
"type": "boolean",
|
|
527
|
+
"default": "false"
|
|
528
|
+
}
|
|
529
|
+
],
|
|
513
530
|
"DD_CIVISIBILITY_MANUAL_API_ENABLED": [
|
|
514
531
|
{
|
|
515
532
|
"implementation": "A",
|
|
@@ -22,6 +22,7 @@ module.exports = {
|
|
|
22
22
|
SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate',
|
|
23
23
|
SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second',
|
|
24
24
|
DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent',
|
|
25
|
+
DATADOG_MINI_AGENT_PATH: '/tmp/datadog/mini_agent_ready',
|
|
25
26
|
DECISION_MAKER_KEY: '_dd.p.dm',
|
|
26
27
|
SAMPLING_KNUTH_RATE: '_dd.p.ksr',
|
|
27
28
|
PROCESS_ID: 'process_id',
|
|
@@ -25,8 +25,11 @@ module.exports = function getExporter (name) {
|
|
|
25
25
|
return require('./ci-visibility/exporters/test-worker')
|
|
26
26
|
default: {
|
|
27
27
|
const inAWSLambda = getEnvironmentVariable('AWS_LAMBDA_FUNCTION_NAME') !== undefined
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const usingAgent = inAWSLambda && (
|
|
29
|
+
fs.existsSync(constants.DATADOG_LAMBDA_EXTENSION_PATH) ||
|
|
30
|
+
fs.existsSync(constants.DATADOG_MINI_AGENT_PATH)
|
|
31
|
+
)
|
|
32
|
+
return require(inAWSLambda && !usingAgent ? './exporters/log' : './exporters/agent')
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -3,4 +3,7 @@
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
DROPPED_VALUE_TEXT: "[This value has been dropped because this span's size exceeds the 5MB size limit.]",
|
|
5
5
|
UNSERIALIZABLE_VALUE_TEXT: 'Unserializable value',
|
|
6
|
+
INCOMPATIBLE_INITIALIZATION:
|
|
7
|
+
'Cannot send LLM Observability data without a running agent or without both a Datadog API key and site. ' +
|
|
8
|
+
'Ensure these configurations are set before running your application.',
|
|
6
9
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
5
|
const log = require('../log')
|
|
6
|
+
const { DD_MAJOR } = require('../../../../version')
|
|
7
|
+
const startupLogs = require('../startup-log')
|
|
6
8
|
const {
|
|
7
9
|
ML_APP,
|
|
8
10
|
PROPAGATED_ML_APP_KEY,
|
|
@@ -15,6 +17,7 @@ const LLMObsEvalMetricsWriter = require('./writers/evaluations')
|
|
|
15
17
|
const LLMObsTagger = require('./tagger')
|
|
16
18
|
const LLMObsSpanWriter = require('./writers/spans')
|
|
17
19
|
const { setAgentStrategy } = require('./writers/util')
|
|
20
|
+
const { INCOMPATIBLE_INITIALIZATION } = require('./constants/text')
|
|
18
21
|
|
|
19
22
|
const spanFinishCh = channel('dd-trace:span:finish')
|
|
20
23
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
@@ -66,10 +69,12 @@ function enable (config) {
|
|
|
66
69
|
|
|
67
70
|
setAgentStrategy(config, useAgentless => {
|
|
68
71
|
if (useAgentless && !(config.apiKey && config.site)) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if (DD_MAJOR < 6 || !config?.startupLogs) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.error(INCOMPATIBLE_INITIALIZATION)
|
|
75
|
+
} else {
|
|
76
|
+
startupLogs.logGenericError(INCOMPATIBLE_INITIALIZATION)
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
evalWriter?.setAgentless(useAgentless)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const fs = require('fs')
|
|
5
5
|
const { URL } = require('url')
|
|
6
|
+
const { getLageTestSessionName } = require('../../ci-visibility/lage')
|
|
6
7
|
const log = require('../../log')
|
|
7
8
|
const { getEnvironmentVariable } = require('../../config/helper')
|
|
8
9
|
const satisfies = require('../../../../../vendor/dist/semifies')
|
|
@@ -934,6 +935,10 @@ function getTestSessionName (config, trimmedCommand, envTags) {
|
|
|
934
935
|
if (config.ciVisibilityTestSessionName) {
|
|
935
936
|
return config.ciVisibilityTestSessionName
|
|
936
937
|
}
|
|
938
|
+
const lageTestSessionName = getLageTestSessionName()
|
|
939
|
+
if (lageTestSessionName) {
|
|
940
|
+
return lageTestSessionName
|
|
941
|
+
}
|
|
937
942
|
if (envTags[CI_JOB_NAME]) {
|
|
938
943
|
return `${envTags[CI_JOB_NAME]}-${trimmedCommand}`
|
|
939
944
|
}
|
|
@@ -85,6 +85,7 @@ class Tracer extends NoopProxy {
|
|
|
85
85
|
// these requires must work with esm bundler
|
|
86
86
|
this._modules = {
|
|
87
87
|
appsec: new LazyModule(() => require('./appsec')),
|
|
88
|
+
aiguard: new LazyModule(() => require('./aiguard')),
|
|
88
89
|
iast: new LazyModule(() => require('./appsec/iast')),
|
|
89
90
|
llmobs: new LazyModule(() => require('./llmobs')),
|
|
90
91
|
rewriter: new LazyModule(() => require('./appsec/iast/taint-tracking/rewriter')),
|
|
@@ -272,7 +273,9 @@ class Tracer extends NoopProxy {
|
|
|
272
273
|
this.dataStreamsCheckpointer = this._tracer.dataStreamsCheckpointer
|
|
273
274
|
lazyProxy(this, 'appsec', () => require('./appsec/sdk'), this._tracer, config)
|
|
274
275
|
lazyProxy(this, 'llmobs', () => require('./llmobs/sdk'), this._tracer, this._modules.llmobs, config)
|
|
276
|
+
|
|
275
277
|
if (config.experimental?.aiguard?.enabled) {
|
|
278
|
+
this._modules.aiguard.enable(this._tracer, config)
|
|
276
279
|
lazyProxy(this, 'aiguard', () => require('./aiguard/sdk'), this._tracer, config)
|
|
277
280
|
}
|
|
278
281
|
this._tracingInitialized = true
|
|
@@ -287,6 +290,7 @@ class Tracer extends NoopProxy {
|
|
|
287
290
|
// This needs to be after the IAST module is enabled
|
|
288
291
|
} else if (this._tracingInitialized) {
|
|
289
292
|
this._modules.appsec.disable()
|
|
293
|
+
this._modules.aiguard.disable()
|
|
290
294
|
this._modules.iast.disable()
|
|
291
295
|
this._modules.llmobs.disable()
|
|
292
296
|
this._modules.openfeature.disable()
|
|
@@ -63,6 +63,14 @@ function logAgentError (agentError) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function logGenericError (message) {
|
|
67
|
+
if (!config?.startupLogs) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
warn('DATADOG TRACER DIAGNOSTIC - Generic Error: ' + message)
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Returns config info without integrations (used by startupLog).
|
|
68
76
|
* @returns {Record<string, unknown>}
|
|
@@ -143,4 +151,5 @@ module.exports = {
|
|
|
143
151
|
setSamplingRules,
|
|
144
152
|
tracerInfo,
|
|
145
153
|
errors,
|
|
154
|
+
logGenericError,
|
|
146
155
|
}
|