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 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.95.0",
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.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 usingLambdaExtension = inAWSLambda && fs.existsSync(constants.DATADOG_LAMBDA_EXTENSION_PATH)
29
- return require(inAWSLambda && !usingLambdaExtension ? './exporters/log' : './exporters/agent')
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
- throw new Error(
70
- 'Cannot send LLM Observability data without a running agent or without both a Datadog API key and site.\n' +
71
- 'Ensure these configurations are set before running your application.'
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
  }