dd-trace 5.108.0 → 5.109.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 +22 -1
- package/package.json +2 -1
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/next.js +34 -0
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/vitest.js +60 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-vitest/src/index.js +4 -1
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
const log = require('../../dd-trace/src/log')
|
|
4
4
|
const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
|
|
5
5
|
const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams')
|
|
6
|
+
const { getFilter } = require('./filter')
|
|
7
|
+
|
|
8
|
+
const filteredJobs = Symbol('bullmq.filteredJobs')
|
|
6
9
|
|
|
7
10
|
// Customer-controlled metadata may be malformed JSON. Returning a fresh `{}`
|
|
8
11
|
// on parse failure keeps the publish path alive instead of throwing into
|
|
@@ -31,6 +34,19 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
bindStart (ctx) {
|
|
37
|
+
let instrument = true
|
|
38
|
+
if (this.config.producerFilter) {
|
|
39
|
+
try {
|
|
40
|
+
instrument = this.shouldInstrument(ctx)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!instrument) {
|
|
47
|
+
return { noop: true }
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const { resource, meta } = this.getSpanData(ctx)
|
|
35
51
|
const span = this.startSpan({
|
|
36
52
|
resource,
|
|
@@ -48,6 +64,17 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
48
64
|
return ctx.currentStore
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
configure (config) {
|
|
68
|
+
if (typeof config === 'boolean') {
|
|
69
|
+
return super.configure(config)
|
|
70
|
+
}
|
|
71
|
+
return super.configure({ ...config, producerFilter: getFilter(config) })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
shouldInstrument (ctx) {
|
|
75
|
+
throw new Error('shouldInstrument must be implemented by subclass')
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
getSpanData (ctx) {
|
|
52
79
|
throw new Error('getSpanData must be implemented by subclass')
|
|
53
80
|
}
|
|
@@ -87,6 +114,15 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
87
114
|
class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
88
115
|
static prefix = 'tracing:orchestrion:bullmq:Queue_add'
|
|
89
116
|
|
|
117
|
+
shouldInstrument (ctx) {
|
|
118
|
+
return this.config.producerFilter({
|
|
119
|
+
name: ctx.arguments?.[0],
|
|
120
|
+
data: ctx.arguments?.[1],
|
|
121
|
+
opts: ctx.arguments?.[2],
|
|
122
|
+
queueName: ctx.self?.name,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
getSpanData (ctx) {
|
|
91
127
|
const queueName = ctx.self?.name || 'bullmq'
|
|
92
128
|
return {
|
|
@@ -128,6 +164,40 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
|
128
164
|
class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
129
165
|
static prefix = 'tracing:orchestrion:bullmq:Queue_addBulk'
|
|
130
166
|
|
|
167
|
+
shouldInstrument (ctx) {
|
|
168
|
+
const jobs = this.#getFilteredJobs(ctx)
|
|
169
|
+
// Empty bulk calls are publish attempts and should keep producing a span.
|
|
170
|
+
return jobs === undefined || jobs.length > 0 || ctx.arguments?.[0].length === 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#getFilteredJobs (ctx) {
|
|
174
|
+
if (Object.hasOwn(ctx, filteredJobs)) return ctx[filteredJobs]
|
|
175
|
+
|
|
176
|
+
const jobs = ctx.arguments?.[0]
|
|
177
|
+
if (!Array.isArray(jobs)) return
|
|
178
|
+
|
|
179
|
+
let allowedJobs
|
|
180
|
+
if (this.config.producerFilter) {
|
|
181
|
+
const queueName = ctx.self?.name
|
|
182
|
+
try {
|
|
183
|
+
allowedJobs = []
|
|
184
|
+
for (const job of jobs) {
|
|
185
|
+
if (job && this.config.producerFilter({ name: job.name, data: job.data, opts: job.opts, queueName })) {
|
|
186
|
+
allowedJobs.push(job)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
|
|
191
|
+
allowedJobs = jobs.filter(Boolean)
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
allowedJobs = jobs
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ctx[filteredJobs] = allowedJobs
|
|
198
|
+
return allowedJobs
|
|
199
|
+
}
|
|
200
|
+
|
|
131
201
|
operationName () {
|
|
132
202
|
return 'bullmq.addBulk'
|
|
133
203
|
}
|
|
@@ -139,14 +209,14 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
139
209
|
resource: queueName,
|
|
140
210
|
meta: {
|
|
141
211
|
'messaging.destination.name': ctx.self?.name,
|
|
142
|
-
'messaging.batch.message_count':
|
|
212
|
+
'messaging.batch.message_count': jobs?.length,
|
|
143
213
|
},
|
|
144
214
|
}
|
|
145
215
|
}
|
|
146
216
|
|
|
147
217
|
injectTraceContext (span, ctx) {
|
|
148
|
-
const jobs = ctx
|
|
149
|
-
if (!
|
|
218
|
+
const jobs = this.#getFilteredJobs(ctx)
|
|
219
|
+
if (!jobs) return
|
|
150
220
|
|
|
151
221
|
const cache = []
|
|
152
222
|
for (let i = 0; i < jobs.length; i++) {
|
|
@@ -159,7 +229,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
159
229
|
}
|
|
160
230
|
|
|
161
231
|
setProducerCheckpoint (span, ctx) {
|
|
162
|
-
const jobs = ctx
|
|
232
|
+
const jobs = this.#getFilteredJobs(ctx) || []
|
|
163
233
|
const queueName = ctx.self?.name || 'bullmq'
|
|
164
234
|
const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
|
|
165
235
|
const cache = ctx._ddMetadata
|
|
@@ -180,6 +250,16 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
180
250
|
class FlowProducerAddPlugin extends BaseBullmqProducerPlugin {
|
|
181
251
|
static prefix = 'tracing:orchestrion:bullmq:FlowProducer_add'
|
|
182
252
|
|
|
253
|
+
shouldInstrument (ctx) {
|
|
254
|
+
const flow = ctx.arguments?.[0]
|
|
255
|
+
return this.config.producerFilter({
|
|
256
|
+
name: flow?.name,
|
|
257
|
+
data: flow?.data,
|
|
258
|
+
opts: flow?.opts,
|
|
259
|
+
queueName: flow?.queueName,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
183
263
|
getSpanData (ctx) {
|
|
184
264
|
const flow = ctx.arguments?.[0]
|
|
185
265
|
const queueName = flow?.queueName || 'bullmq'
|
|
@@ -28,7 +28,7 @@ class RedisPlugin extends CachePlugin {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
bindStart (ctx) {
|
|
31
|
-
const {
|
|
31
|
+
const { command, args, argsStartIndex, connectionOptions, connectionName } = ctx
|
|
32
32
|
|
|
33
33
|
const resource = command
|
|
34
34
|
const normalizedCommand = command.toUpperCase()
|
|
@@ -55,7 +55,6 @@ class RedisPlugin extends CachePlugin {
|
|
|
55
55
|
type: this._spanType,
|
|
56
56
|
meta: {
|
|
57
57
|
'db.type': this._spanType,
|
|
58
|
-
'db.name': db || '0',
|
|
59
58
|
[this.#rawCommandKey]: formatCommand(normalizedCommand, args, argsStartIndex),
|
|
60
59
|
'out.host': connectionOptions.host,
|
|
61
60
|
[CLIENT_PORT_KEY]: connectionOptions.port,
|
|
@@ -63,6 +63,8 @@ class VitestPlugin extends CiPlugin {
|
|
|
63
63
|
testSessionId: testSessionSpanContext?.toTraceId(),
|
|
64
64
|
testModuleId: testModuleSpanContext?.toSpanId(),
|
|
65
65
|
testCommand: this.command,
|
|
66
|
+
repositoryRoot: this.repositoryRoot,
|
|
67
|
+
codeOwnersEntries: this.codeOwnersEntries,
|
|
66
68
|
})
|
|
67
69
|
})
|
|
68
70
|
|
|
@@ -307,10 +309,11 @@ class VitestPlugin extends CiPlugin {
|
|
|
307
309
|
})
|
|
308
310
|
|
|
309
311
|
this.addBind('ci:vitest:test-suite:start', (ctx) => {
|
|
310
|
-
const { testSuiteAbsolutePath, frameworkVersion } = ctx
|
|
312
|
+
const { codeOwnersEntries, repositoryRoot, testSuiteAbsolutePath, frameworkVersion } = ctx
|
|
311
313
|
|
|
312
314
|
const testCommand = ctx.testCommand || 'vitest run'
|
|
313
315
|
const { testSessionId, testModuleId } = ctx
|
|
316
|
+
this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)
|
|
314
317
|
this.command = testCommand
|
|
315
318
|
this.frameworkVersion = frameworkVersion
|
|
316
319
|
const testSessionSpanContext = testSessionId && testModuleId
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const log = require('../log')
|
|
4
|
-
const { incomingHttpRequestStart
|
|
4
|
+
const { incomingHttpRequestStart } = require('./channels')
|
|
5
|
+
const openaiIntegration = require('./integrations/openai')
|
|
6
|
+
const vercelAiIntegration = require('./integrations/vercel-ai')
|
|
5
7
|
const AIGuard = require('./sdk')
|
|
6
|
-
const { SOURCE_AUTO, INTEGRATION_NONE } = require('./tags')
|
|
7
8
|
|
|
8
9
|
let isEnabled = false
|
|
9
10
|
let aiguard
|
|
10
11
|
let block
|
|
12
|
+
let disableOpenAIIntegration
|
|
13
|
+
let disableVercelAiIntegration
|
|
11
14
|
|
|
12
15
|
function onIncomingHttpRequestStart () {
|
|
13
16
|
// No-op: subscribing ensures the HTTP plugin spreads req onto the store
|
|
@@ -21,7 +24,8 @@ function enable (tracer, config) {
|
|
|
21
24
|
block = config.experimental?.aiguard?.block !== false
|
|
22
25
|
|
|
23
26
|
incomingHttpRequestStart.subscribe(onIncomingHttpRequestStart)
|
|
24
|
-
|
|
27
|
+
disableOpenAIIntegration = openaiIntegration.enable(aiguard, block)
|
|
28
|
+
disableVercelAiIntegration = vercelAiIntegration.enable(aiguard, block)
|
|
25
29
|
|
|
26
30
|
isEnabled = true
|
|
27
31
|
} catch (err) {
|
|
@@ -34,56 +38,14 @@ function disable () {
|
|
|
34
38
|
if (!isEnabled) return
|
|
35
39
|
|
|
36
40
|
incomingHttpRequestStart.unsubscribe(onIncomingHttpRequestStart)
|
|
37
|
-
|
|
41
|
+
disableOpenAIIntegration?.()
|
|
42
|
+
disableVercelAiIntegration?.()
|
|
38
43
|
|
|
39
44
|
aiguard = undefined
|
|
40
45
|
isEnabled = false
|
|
41
46
|
block = false
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Handles channel messages with pre-converted messages.
|
|
46
|
-
*
|
|
47
|
-
* @param {object} ctx
|
|
48
|
-
* @param {Array<object>} ctx.messages
|
|
49
|
-
* @param {string} [ctx.integration]
|
|
50
|
-
* @param {object} [ctx.parentSpan] - LLM span to parent the `ai_guard` span under.
|
|
51
|
-
* @param {AbortController} ctx.abortController
|
|
52
|
-
* @param {Array<Promise<void>>} ctx.pending - Subscribers push only when they evaluate.
|
|
53
|
-
*/
|
|
54
|
-
function onEvaluate (ctx) {
|
|
55
|
-
// Decline to evaluate empty payloads by not pushing to pending.
|
|
56
|
-
if (!ctx.messages?.length) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const opts = {
|
|
61
|
-
block,
|
|
62
|
-
source: SOURCE_AUTO,
|
|
63
|
-
integration: ctx.integration || INTEGRATION_NONE,
|
|
64
|
-
childOf: ctx.parentSpan,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
ctx.pending.push(aiguard.evaluate(ctx.messages, opts).catch(handleEvaluationError.bind(null, ctx)))
|
|
69
|
-
} catch (err) {
|
|
70
|
-
ctx.pending.push(Promise.resolve().then(() => handleEvaluationError(ctx, err)))
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Handles an AI Guard evaluation failure.
|
|
76
|
-
*
|
|
77
|
-
* @param {object} ctx
|
|
78
|
-
* @param {AbortController} ctx.abortController
|
|
79
|
-
* @param {Error} err
|
|
80
|
-
*/
|
|
81
|
-
function handleEvaluationError (ctx, err) {
|
|
82
|
-
if (err.name === 'AIGuardAbortError') {
|
|
83
|
-
ctx.abortController.abort(err)
|
|
84
|
-
} else {
|
|
85
|
-
log.error('AIGuard: unexpected error during evaluation: %s', err.message)
|
|
86
|
-
}
|
|
47
|
+
disableOpenAIIntegration = undefined
|
|
48
|
+
disableVercelAiIntegration = undefined
|
|
87
49
|
}
|
|
88
50
|
|
|
89
51
|
module.exports = { enable, disable }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Starts one AI Guard evaluation for a lifecycle ctx.
|
|
7
|
+
*
|
|
8
|
+
* Async evaluations are pushed synchronously during channel publication. If
|
|
9
|
+
* evaluation throws synchronously, the error is handled before publish returns.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} ctx
|
|
12
|
+
* @param {AbortController} ctx.abortController
|
|
13
|
+
* @param {object} [ctx.parentSpan]
|
|
14
|
+
* @param {Array<Promise<void>>} ctx.pending
|
|
15
|
+
* @param {object} aiguard
|
|
16
|
+
* @param {Array<object>|undefined} messages
|
|
17
|
+
* @param {object} opts
|
|
18
|
+
*/
|
|
19
|
+
function pushEvaluation (ctx, aiguard, messages, opts) {
|
|
20
|
+
if (!messages?.length) return
|
|
21
|
+
|
|
22
|
+
const evaluateOpts = ctx.parentSpan ? { ...opts, childOf: ctx.parentSpan } : opts
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
ctx.pending.push(aiguard.evaluate(messages, evaluateOpts).catch(handleEvaluationError.bind(null, ctx)))
|
|
26
|
+
} catch (err) {
|
|
27
|
+
handleEvaluationError(ctx, err)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handles an AI Guard evaluation failure.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} ctx
|
|
35
|
+
* @param {AbortController} ctx.abortController
|
|
36
|
+
* @param {Error} err
|
|
37
|
+
*/
|
|
38
|
+
function handleEvaluationError (ctx, err) {
|
|
39
|
+
if (err.name === 'AIGuardAbortError') {
|
|
40
|
+
ctx.abortController.abort(err)
|
|
41
|
+
} else {
|
|
42
|
+
log.error('AIGuard: unexpected error during evaluation: %s', err.message)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { pushEvaluation }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { channel } = require('dc-polyfill')
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
getChatCompletionsInputMessages,
|
|
7
|
+
getChatCompletionsOutputMessages,
|
|
8
|
+
getResponsesInputMessages,
|
|
9
|
+
getResponsesOutputMessages,
|
|
10
|
+
} = require('../messages/openai')
|
|
11
|
+
const { SOURCE_AUTO } = require('../tags')
|
|
12
|
+
const { pushEvaluation } = require('./evaluate')
|
|
13
|
+
|
|
14
|
+
const chatCompletionsBeforeChannel = channel('dd-trace:openai:chat.completions:before')
|
|
15
|
+
const chatCompletionsAfterChannel = channel('dd-trace:openai:chat.completions:after')
|
|
16
|
+
const responsesBeforeChannel = channel('dd-trace:openai:responses:before')
|
|
17
|
+
const responsesAfterChannel = channel('dd-trace:openai:responses:after')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Subscribes AI Guard to OpenAI lifecycle channels.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} aiguard
|
|
23
|
+
* @param {boolean} block
|
|
24
|
+
* @returns {() => void}
|
|
25
|
+
*/
|
|
26
|
+
function enable (aiguard, block) {
|
|
27
|
+
const opts = { block, source: SOURCE_AUTO, integration: 'openai' }
|
|
28
|
+
|
|
29
|
+
function onChatCompletionsBefore (ctx) {
|
|
30
|
+
pushEvaluation(ctx, aiguard, getChatCompletionsInputMessages(ctx.args?.[0]), opts)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function onChatCompletionsAfter (ctx) {
|
|
34
|
+
const inputMessages = getChatCompletionsInputMessages(ctx.args?.[0])
|
|
35
|
+
if (!inputMessages?.length) return
|
|
36
|
+
for (const message of getChatCompletionsOutputMessages(ctx.body)) {
|
|
37
|
+
pushEvaluation(ctx, aiguard, [...inputMessages, message], opts)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onResponsesBefore (ctx) {
|
|
42
|
+
pushEvaluation(ctx, aiguard, getResponsesInputMessages(ctx.args?.[0]), opts)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onResponsesAfter (ctx) {
|
|
46
|
+
const inputMessages = getResponsesInputMessages(ctx.args?.[0])
|
|
47
|
+
if (!inputMessages?.length) return
|
|
48
|
+
const outputMessages = getResponsesOutputMessages(ctx.body)
|
|
49
|
+
if (!outputMessages.length) return
|
|
50
|
+
pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
chatCompletionsBeforeChannel.subscribe(onChatCompletionsBefore)
|
|
54
|
+
chatCompletionsAfterChannel.subscribe(onChatCompletionsAfter)
|
|
55
|
+
responsesBeforeChannel.subscribe(onResponsesBefore)
|
|
56
|
+
responsesAfterChannel.subscribe(onResponsesAfter)
|
|
57
|
+
|
|
58
|
+
return function disable () {
|
|
59
|
+
chatCompletionsBeforeChannel.unsubscribe(onChatCompletionsBefore)
|
|
60
|
+
chatCompletionsAfterChannel.unsubscribe(onChatCompletionsAfter)
|
|
61
|
+
responsesBeforeChannel.unsubscribe(onResponsesBefore)
|
|
62
|
+
responsesAfterChannel.unsubscribe(onResponsesAfter)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { enable }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { channel } = require('dc-polyfill')
|
|
4
|
+
|
|
5
|
+
const { buildOutputMessages, convertVercelPromptToMessages } = require('../messages/vercel-ai')
|
|
6
|
+
const { SOURCE_AUTO } = require('../tags')
|
|
7
|
+
const { pushEvaluation } = require('./evaluate')
|
|
8
|
+
|
|
9
|
+
const doGenerateBeforeChannel = channel('dd-trace:vercel-ai:doGenerate:before')
|
|
10
|
+
const doGenerateAfterChannel = channel('dd-trace:vercel-ai:doGenerate:after')
|
|
11
|
+
const doStreamBeforeChannel = channel('dd-trace:vercel-ai:doStream:before')
|
|
12
|
+
const doStreamAfterChannel = channel('dd-trace:vercel-ai:doStream:after')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Subscribes AI Guard to Vercel AI lifecycle channels.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} aiguard
|
|
18
|
+
* @param {boolean} block
|
|
19
|
+
* @returns {() => void}
|
|
20
|
+
*/
|
|
21
|
+
function enable (aiguard, block) {
|
|
22
|
+
const opts = { block, source: SOURCE_AUTO, integration: 'ai' }
|
|
23
|
+
|
|
24
|
+
function onBefore (ctx) {
|
|
25
|
+
pushEvaluation(ctx, aiguard, convertVercelPromptToMessages(ctx.prompt), opts)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onGenerateAfter (ctx) {
|
|
29
|
+
const inputMessages = convertVercelPromptToMessages(ctx.prompt)
|
|
30
|
+
if (!inputMessages.length || !ctx.result?.content?.length) return
|
|
31
|
+
|
|
32
|
+
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, ctx.result.content), opts)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function onStreamAfter (ctx) {
|
|
36
|
+
const inputMessages = convertVercelPromptToMessages(ctx.prompt)
|
|
37
|
+
if (!inputMessages.length || !ctx.chunks?.length) return
|
|
38
|
+
|
|
39
|
+
pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, getStreamContent(ctx.chunks)), opts)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
doGenerateBeforeChannel.subscribe(onBefore)
|
|
43
|
+
doGenerateAfterChannel.subscribe(onGenerateAfter)
|
|
44
|
+
doStreamBeforeChannel.subscribe(onBefore)
|
|
45
|
+
doStreamAfterChannel.subscribe(onStreamAfter)
|
|
46
|
+
|
|
47
|
+
return function disable () {
|
|
48
|
+
doGenerateBeforeChannel.unsubscribe(onBefore)
|
|
49
|
+
doGenerateAfterChannel.unsubscribe(onGenerateAfter)
|
|
50
|
+
doStreamBeforeChannel.unsubscribe(onBefore)
|
|
51
|
+
doStreamAfterChannel.unsubscribe(onStreamAfter)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts Vercel stream chunks into the content shape used by doGenerate results.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array<object>} chunks
|
|
59
|
+
* @returns {Array<object>}
|
|
60
|
+
*/
|
|
61
|
+
function getStreamContent (chunks) {
|
|
62
|
+
const toolCalls = []
|
|
63
|
+
const textParts = []
|
|
64
|
+
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
if (chunk?.type === 'tool-call') {
|
|
67
|
+
toolCalls.push(chunk)
|
|
68
|
+
} else if (chunk?.type === 'text-delta') {
|
|
69
|
+
textParts.push(chunk.textDelta)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (toolCalls.length) return toolCalls
|
|
74
|
+
const text = textParts.join('')
|
|
75
|
+
return text ? [{ type: 'text', text }] : []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { enable }
|