dd-trace 6.17.0 → 6.19.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 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +55 -11
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +50 -9
- package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
- package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +30 -13
- package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
- package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
- package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
- package/packages/datadog-instrumentations/src/playwright.js +73 -6
- package/packages/datadog-instrumentations/src/undici.js +7 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
- package/packages/datadog-instrumentations/src/ws.js +41 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +22 -8
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
- package/packages/datadog-plugin-cypress/src/index.js +3 -1
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-jest/src/index.js +3 -4
- package/packages/datadog-plugin-mocha/src/index.js +3 -4
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-playwright/src/index.js +11 -6
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/datadog-plugin-vitest/src/index.js +3 -4
- package/packages/datadog-plugin-ws/src/producer.js +1 -1
- package/packages/datadog-shimmer/src/shimmer.js +14 -10
- package/packages/datadog-turbopack/index.js +4 -1
- package/packages/datadog-turbopack/src/loader.js +6 -2
- package/packages/dd-trace/src/agent/info.js +5 -4
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
- package/packages/dd-trace/src/config/index.js +7 -6
- package/packages/dd-trace/src/config/supported-configurations.json +16 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/debugger/index.js +1 -1
- package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
- package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
- package/packages/dd-trace/src/evp_proxy/path.js +21 -7
- package/packages/dd-trace/src/exporters/common/url.js +33 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +11 -40
- package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
- package/packages/dd-trace/src/openfeature/index.js +6 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
- package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/opentracing/tracer.js +3 -3
- package/packages/dd-trace/src/plugin_manager.js +27 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/test.js +13 -4
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/proxy.js +2 -2
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/packages/dd-trace/src/telemetry/send-data.js +2 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/packages/dd-trace/src/tracer.js +5 -4
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
- package/packages/datadog-instrumentations/src/bullmq.js +0 -11
- package/packages/datadog-instrumentations/src/langchain.js +0 -7
- package/packages/datadog-instrumentations/src/langgraph.js +0 -7
- package/packages/datadog-instrumentations/src/mercurius.js +0 -11
|
@@ -26,6 +26,7 @@ class CypressPlugin extends Plugin {
|
|
|
26
26
|
userAfterRunHandlers,
|
|
27
27
|
userAfterScreenshotHandlers,
|
|
28
28
|
cleanupWrapper,
|
|
29
|
+
registerBeforeRun,
|
|
29
30
|
} = payload
|
|
30
31
|
|
|
31
32
|
const registerAfterRunWithCleanup = (afterRunHandler) => {
|
|
@@ -79,6 +80,7 @@ class CypressPlugin extends Plugin {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
if (cypressPlugin._isInit) {
|
|
83
|
+
registerBeforeRun()
|
|
82
84
|
// Already initialized by manual plugin call — just chain user handlers.
|
|
83
85
|
// Pass the plugin's afterScreenshot so chaining a user handler doesn't drop the upload
|
|
84
86
|
// (the chained registration replaces the one plugin.js set, so it must include it).
|
|
@@ -94,7 +96,7 @@ class CypressPlugin extends Plugin {
|
|
|
94
96
|
return
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
registerBeforeRun(cypressPlugin.beforeRun.bind(cypressPlugin))
|
|
98
100
|
registerAfterScreenshot(datadogAfterScreenshotHandler)
|
|
99
101
|
|
|
100
102
|
on('after:spec', (spec, results) => {
|
|
@@ -17,6 +17,9 @@ class FetchPlugin extends HttpClientPlugin {
|
|
|
17
17
|
|
|
18
18
|
const store = super.bindStart(ctx)
|
|
19
19
|
|
|
20
|
+
// Native undici diagnostics also fire for requests already traced by this wrapper.
|
|
21
|
+
store.skipUndici = true
|
|
22
|
+
|
|
20
23
|
for (const name in options.headers) {
|
|
21
24
|
if (!req.headers.has(name)) {
|
|
22
25
|
req.headers.set(name, options.headers[name])
|
|
@@ -117,19 +117,18 @@ class JestPlugin extends CiPlugin {
|
|
|
117
117
|
isEarlyFlakeDetectionEnabled,
|
|
118
118
|
isEarlyFlakeDetectionFaulty,
|
|
119
119
|
isTestManagementTestsEnabled,
|
|
120
|
-
|
|
120
|
+
testSessionEmptyReason,
|
|
121
121
|
onDone,
|
|
122
122
|
}) => {
|
|
123
123
|
const finishSession = () => {
|
|
124
124
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
125
125
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
126
126
|
|
|
127
|
-
if (
|
|
127
|
+
if (testSessionEmptyReason) {
|
|
128
128
|
setExpectedEmptyTestSessionTags(
|
|
129
129
|
this.testSessionSpan,
|
|
130
130
|
this.testModuleSpan,
|
|
131
|
-
|
|
132
|
-
'zero_tests'
|
|
131
|
+
testSessionEmptyReason
|
|
133
132
|
)
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -885,7 +885,7 @@ class MochaPlugin extends CiPlugin {
|
|
|
885
885
|
isTestManagementEnabled,
|
|
886
886
|
isParallel,
|
|
887
887
|
isFrameworkError,
|
|
888
|
-
|
|
888
|
+
testSessionEmptyReason,
|
|
889
889
|
onDone,
|
|
890
890
|
}) => {
|
|
891
891
|
this._exportPendingWorkerTraces()
|
|
@@ -898,12 +898,11 @@ class MochaPlugin extends CiPlugin {
|
|
|
898
898
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
899
899
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
900
900
|
|
|
901
|
-
if (
|
|
901
|
+
if (testSessionEmptyReason) {
|
|
902
902
|
setExpectedEmptyTestSessionTags(
|
|
903
903
|
this.testSessionSpan,
|
|
904
904
|
this.testModuleSpan,
|
|
905
|
-
|
|
906
|
-
'zero_tests'
|
|
905
|
+
testSessionEmptyReason
|
|
907
906
|
)
|
|
908
907
|
}
|
|
909
908
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { URL } = require('url')
|
|
4
4
|
const log = require('../../dd-trace/src/log')
|
|
5
5
|
|
|
6
|
+
const easyConnectProtocol = /^tcps?:\/\//i
|
|
7
|
+
|
|
6
8
|
function parseOracleDescriptor (descriptor) {
|
|
7
9
|
const hostnameMatch = descriptor.match(/HOST\s*=\s*([^)]+)/i)
|
|
8
10
|
const hostname = hostnameMatch?.[1] || 'localhost' // Default Oracle hostname
|
|
@@ -24,7 +26,7 @@ module.exports = function getDBInformation (connAttrs) {
|
|
|
24
26
|
return parseOracleDescriptor(connectString)
|
|
25
27
|
}
|
|
26
28
|
try {
|
|
27
|
-
const url = new URL(`oracle://${connectString}`)
|
|
29
|
+
const url = new URL(easyConnectProtocol.test(connectString) ? connectString : `oracle://${connectString}`)
|
|
28
30
|
return {
|
|
29
31
|
hostname: url.hostname || 'localhost', // Default Oracle hostname
|
|
30
32
|
port: url.port || '1521', // Default Oracle port
|
|
@@ -70,8 +70,6 @@ const log = require('../../dd-trace/src/log')
|
|
|
70
70
|
|
|
71
71
|
const PLAYWRIGHT_FAILURE_SCREENSHOT_RE = /^test-failed-\d+\.png$/
|
|
72
72
|
const PLAYWRIGHT_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm'])
|
|
73
|
-
const EMPTY_SHARD_SKIP_REASON = 'No tests were assigned to this shard'
|
|
74
|
-
const EMPTY_SHARD_REASON = 'zero_test_shard'
|
|
75
73
|
const RETRY_TEST_ID = '_dd.playwright.retry_test_id'
|
|
76
74
|
const DEFER_FINAL_STATUS = '_dd.playwright.defer_final_status'
|
|
77
75
|
const noop = () => {}
|
|
@@ -186,7 +184,7 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
186
184
|
isEarlyFlakeDetectionEnabled,
|
|
187
185
|
isEarlyFlakeDetectionFaulty,
|
|
188
186
|
isTestManagementTestsEnabled,
|
|
189
|
-
|
|
187
|
+
testSessionEmptyReason,
|
|
190
188
|
error,
|
|
191
189
|
onDone,
|
|
192
190
|
}) => {
|
|
@@ -212,12 +210,11 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
212
210
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
213
211
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
214
212
|
|
|
215
|
-
if (
|
|
213
|
+
if (testSessionEmptyReason) {
|
|
216
214
|
setExpectedEmptyTestSessionTags(
|
|
217
215
|
this.testSessionSpan,
|
|
218
216
|
this.testModuleSpan,
|
|
219
|
-
|
|
220
|
-
EMPTY_SHARD_REASON
|
|
217
|
+
testSessionEmptyReason
|
|
221
218
|
)
|
|
222
219
|
}
|
|
223
220
|
|
|
@@ -526,6 +523,8 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
526
523
|
this.addSub('ci:playwright:test:finish', ({
|
|
527
524
|
span,
|
|
528
525
|
testStatus,
|
|
526
|
+
isExpectedFailure,
|
|
527
|
+
hasNonRetriableError,
|
|
529
528
|
steps,
|
|
530
529
|
error,
|
|
531
530
|
extraTags,
|
|
@@ -565,6 +564,12 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
565
564
|
if (extraTags) {
|
|
566
565
|
span.addTags(extraTags)
|
|
567
566
|
}
|
|
567
|
+
if (isExpectedFailure) {
|
|
568
|
+
span.setTag('test.result', 'xfail')
|
|
569
|
+
}
|
|
570
|
+
if (hasNonRetriableError === true) {
|
|
571
|
+
span.setTag('test.playwright.has_non_retriable_error', 'true')
|
|
572
|
+
}
|
|
568
573
|
if (isNew) {
|
|
569
574
|
span.setTag(TEST_IS_NEW, 'true')
|
|
570
575
|
if (isEfdRetry) {
|
|
@@ -47,6 +47,8 @@ class UndiciPlugin extends HttpClientPlugin {
|
|
|
47
47
|
if (!request) return
|
|
48
48
|
|
|
49
49
|
const store = storage('legacy').getStore()
|
|
50
|
+
if (store?.skipUndici) return
|
|
51
|
+
|
|
50
52
|
const { origin = '', path = '/' } = request
|
|
51
53
|
const method = request.method?.toUpperCase() ?? 'GET'
|
|
52
54
|
|
|
@@ -207,6 +209,9 @@ class UndiciPlugin extends HttpClientPlugin {
|
|
|
207
209
|
|
|
208
210
|
const store = super.bindStart(ctx)
|
|
209
211
|
|
|
212
|
+
// Undici 5.0 emits native diagnostics even though its fetch needs the legacy wrapper.
|
|
213
|
+
store.skipUndici = true
|
|
214
|
+
|
|
210
215
|
// Inject trace headers back into the request
|
|
211
216
|
for (const name of Object.keys(options.headers)) {
|
|
212
217
|
if (!req.headers.has(name)) {
|
|
@@ -580,7 +580,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
580
580
|
requestErrorTags,
|
|
581
581
|
vitestPool,
|
|
582
582
|
isVitestNoWorkerInitActive,
|
|
583
|
-
|
|
583
|
+
testSessionEmptyReason,
|
|
584
584
|
onDone,
|
|
585
585
|
}) => {
|
|
586
586
|
for (const [tag, value] of Object.entries(requestErrorTags)) {
|
|
@@ -589,12 +589,11 @@ class VitestPlugin extends CiPlugin {
|
|
|
589
589
|
}
|
|
590
590
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
591
591
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
592
|
-
if (
|
|
592
|
+
if (testSessionEmptyReason) {
|
|
593
593
|
setExpectedEmptyTestSessionTags(
|
|
594
594
|
this.testSessionSpan,
|
|
595
595
|
this.testModuleSpan,
|
|
596
|
-
|
|
597
|
-
'zero_tests'
|
|
596
|
+
testSessionEmptyReason
|
|
598
597
|
)
|
|
599
598
|
}
|
|
600
599
|
if (error) {
|
|
@@ -21,7 +21,7 @@ class WSProducerPlugin extends TracingPlugin {
|
|
|
21
21
|
|
|
22
22
|
bindStart (ctx) {
|
|
23
23
|
const { byteLength, socket, binary } = ctx
|
|
24
|
-
if (!socket
|
|
24
|
+
if (!socket?.spanContext) return
|
|
25
25
|
|
|
26
26
|
const spanTags = socket.spanTags
|
|
27
27
|
const path = getSegment(spanTags['resource.name'], ' ', 1)
|
|
@@ -53,8 +53,15 @@ function copyProperties (original, wrapped) {
|
|
|
53
53
|
const descriptor = /** @type {Descriptor} */ (Object.getOwnPropertyDescriptor(original, key))
|
|
54
54
|
if (descriptor.writable && descriptor.enumerable && descriptor.configurable) {
|
|
55
55
|
wrapped[key] = original[key]
|
|
56
|
-
} else
|
|
57
|
-
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
Object.defineProperty(wrapped, key, descriptor)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const wrappedDescriptor = Object.getOwnPropertyDescriptor(wrapped, key)
|
|
61
|
+
if (wrappedDescriptor?.configurable !== false || wrappedDescriptor.writable) {
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
}
|
|
@@ -320,19 +327,16 @@ function assertMethod (target, name, method) {
|
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
/**
|
|
323
|
-
* Asserts that a target is not
|
|
330
|
+
* Asserts that a target is not an identifiable class constructor.
|
|
324
331
|
*
|
|
325
332
|
* @param {Function} target - The target function.
|
|
326
|
-
* @throws {
|
|
333
|
+
* @throws {TypeError} If the target is an identifiable class constructor.
|
|
327
334
|
*/
|
|
328
335
|
function assertNotClass (target) {
|
|
329
|
-
//
|
|
330
|
-
// writable one and arrows / async / method-shorthand have none at all. The
|
|
331
|
-
// `'prototype' in target` gate skips the descriptor lookup for the no-prototype
|
|
332
|
-
// shapes; the `in` operator is cheaper than reading `target.prototype` since
|
|
333
|
-
// it returns a boolean instead of materialising the prototype reference.
|
|
336
|
+
// Frozen functions also have a non-writable `prototype`; inspect their source only then.
|
|
334
337
|
if ('prototype' in target &&
|
|
335
|
-
Object.getOwnPropertyDescriptor(target, 'prototype').writable === false
|
|
338
|
+
Object.getOwnPropertyDescriptor(target, 'prototype').writable === false &&
|
|
339
|
+
(!Object.isFrozen(target) || Function.prototype.toString.call(target).startsWith('class'))) {
|
|
336
340
|
throw new TypeError('Target is a native class constructor and cannot be wrapped.')
|
|
337
341
|
}
|
|
338
342
|
}
|
|
@@ -6,6 +6,7 @@ const path = require('node:path')
|
|
|
6
6
|
|
|
7
7
|
const satisfies = require('../../vendor/dist/semifies')
|
|
8
8
|
const hooks = require('../datadog-instrumentations/src/helpers/hooks')
|
|
9
|
+
const { getRewriteTargetNames } = require('../datadog-instrumentations/src/helpers/rewriter/targets')
|
|
9
10
|
const { SYNTHETIC_EXTENSION } = require('./src/constants')
|
|
10
11
|
|
|
11
12
|
const BUILTIN_MODULES = new Set(Module.builtinModules)
|
|
@@ -158,7 +159,9 @@ function hasDatadogLoader (value) {
|
|
|
158
159
|
*/
|
|
159
160
|
function createPackagePathPattern () {
|
|
160
161
|
const names = []
|
|
161
|
-
|
|
162
|
+
const targetPackages = new Set(Object.keys(hooks))
|
|
163
|
+
for (const name of getRewriteTargetNames()) targetPackages.add(name)
|
|
164
|
+
for (const name of targetPackages) {
|
|
162
165
|
if (!name.startsWith('.') && !BUILTIN_MODULES.has(name)) names.push(escapeRegExp(name))
|
|
163
166
|
}
|
|
164
167
|
names.sort()
|
|
@@ -11,7 +11,10 @@ const hooks = require('../../datadog-instrumentations/src/helpers/hooks')
|
|
|
11
11
|
const instrumentations = require('../../datadog-instrumentations/src/helpers/instrumentations')
|
|
12
12
|
const { filename, matchesInstrumentation } = require('../../datadog-instrumentations/src/helpers/instrumentation-utils')
|
|
13
13
|
const { createBundlerRewriter } = require('../../datadog-instrumentations/src/helpers/rewriter')
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
getRewriteTarget,
|
|
16
|
+
getRewriteTargetNames,
|
|
17
|
+
} = require('../../datadog-instrumentations/src/helpers/rewriter/targets')
|
|
15
18
|
const { isESMFile } = require('../../datadog-esbuild/src/utils')
|
|
16
19
|
const { SYNTHETIC_EXTENSION } = require('./constants')
|
|
17
20
|
|
|
@@ -21,6 +24,7 @@ const MODULE_BINDING = 2
|
|
|
21
24
|
const REQUIRE_BINDING = 4
|
|
22
25
|
const COMMONJS_BINDINGS = MODULE_BINDING | REQUIRE_BINDING
|
|
23
26
|
const targetPackages = new Set(Object.keys(hooks))
|
|
27
|
+
for (const name of getRewriteTargetNames()) targetPackages.add(name)
|
|
24
28
|
const entrypoints = new Map()
|
|
25
29
|
const loadedHooks = new Set()
|
|
26
30
|
const packageCache = new Map()
|
|
@@ -98,7 +102,7 @@ module.exports = function loader (source, inputSourceMap) {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
if (publications.length > 0) code = appendCommonJsPublications(code, publications, dcModule)
|
|
101
|
-
if (rewritten && publications.length === 0) {
|
|
105
|
+
if (rewritten && publications.length === 0 && (rewriteTarget.activate || hooks[rewriteTarget.moduleName])) {
|
|
102
106
|
if (!esm && hasUnsafeCommonJsBindings(code)) {
|
|
103
107
|
this.emitWarning?.(new Error(`Skipped CommonJS activation for unsafe wrapper bindings in ${resourcePath}`))
|
|
104
108
|
this.callback(undefined, source, inputSourceMap)
|
|
@@ -17,21 +17,22 @@ module.exports = {
|
|
|
17
17
|
* Fetches agent information from the /info endpoint
|
|
18
18
|
* @param {URL} url - The agent URL
|
|
19
19
|
* @param {Function} callback - Callback function with signature (err, agentInfo)
|
|
20
|
-
* @param {{ deadline?: number, signal?: AbortSignal }} [options]
|
|
20
|
+
* @param {{ deadline?: number, signal?: AbortSignal, path?: string, retry?: boolean }} [options]
|
|
21
|
+
* Request finalization and routing options
|
|
21
22
|
* @param {Function} [makeRequest] - Request implementation
|
|
22
23
|
*/
|
|
23
24
|
function fetchAgentInfo (url, callback, options = {}, makeRequest = request) {
|
|
24
|
-
const urlKey = url.href
|
|
25
|
+
const urlKey = JSON.stringify([url.href, options.path ?? '/info'])
|
|
25
26
|
|
|
26
27
|
if (cachedUrl !== null && cachedUrl !== urlKey) {
|
|
27
|
-
//
|
|
28
|
+
// Different consumers may query different paths on the same Agent URL.
|
|
28
29
|
clearCache()
|
|
29
30
|
} else if (cachedData !== null && (Date.now() - cachedTimestamp) < CACHE_TTL_MS) {
|
|
30
31
|
// Return cached result if still valid
|
|
31
32
|
return process.nextTick(callback, null, cachedData)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
options.path
|
|
35
|
+
options.path ??= '/info'
|
|
35
36
|
options.url = url
|
|
36
37
|
makeRequest('', options, (err, res) => {
|
|
37
38
|
if (err) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const tracerVersion = require('../../../../package.json').version
|
|
4
|
+
const { createAgents } = require('../exporters/common/agents')
|
|
5
|
+
const request = require('../exporters/common/request')
|
|
4
6
|
const { AIGuardClientError } = require('./errors')
|
|
5
7
|
const { parseEvaluationResponse } = require('./evaluation')
|
|
6
8
|
const TAGS = require('./tags')
|
|
7
9
|
|
|
10
|
+
// Evaluations can run concurrently on the application's request path.
|
|
11
|
+
const { httpAgent, httpsAgent } = createAgents(16)
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Resolves the AI Guard host for a Datadog site.
|
|
10
15
|
*
|
|
@@ -18,29 +23,43 @@ function aiGuardHost (site) {
|
|
|
18
23
|
* Sends a request to the AI Guard service.
|
|
19
24
|
*
|
|
20
25
|
* @param {object} body
|
|
21
|
-
* @param {{ url: string, headers: Record<string, string>, timeout: number }} opts
|
|
26
|
+
* @param {{ url: string, headers: Record<string, string|undefined>, timeout: number }} opts
|
|
22
27
|
* @returns {Promise<{ status: number, body: unknown }>}
|
|
23
28
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
function executeRequest (body, opts) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const postData = JSON.stringify(body)
|
|
32
|
+
const url = new URL(opts.url)
|
|
33
|
+
request(postData, {
|
|
34
|
+
url,
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
...opts.headers,
|
|
39
|
+
},
|
|
40
|
+
agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
|
|
41
|
+
timeout: opts.timeout,
|
|
42
|
+
signal: AbortSignal.timeout(opts.timeout),
|
|
43
|
+
retry: false,
|
|
44
|
+
includeErrorResponseBody: true,
|
|
45
|
+
}, (error, result, status) => {
|
|
46
|
+
if (status === undefined) {
|
|
47
|
+
reject(error || new Error('AI Guard request completed without a status'))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
let responseBody
|
|
52
|
+
try {
|
|
53
|
+
const responseText = error ? /** @type {Error & { responseBody?: string }} */ (error).responseBody : result
|
|
54
|
+
if (typeof responseText !== 'string') throw new TypeError('AI Guard response body is missing')
|
|
55
|
+
responseBody = JSON.parse(responseText)
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
reject(cause)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
resolve({ status, body: responseBody })
|
|
61
|
+
})
|
|
37
62
|
})
|
|
38
|
-
|
|
39
|
-
const responseBody = await response.json()
|
|
40
|
-
return {
|
|
41
|
-
status: response.status,
|
|
42
|
-
body: responseBody,
|
|
43
|
-
}
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
class AIGuardClient {
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
getMessagesInputMessages,
|
|
7
|
+
getMessagesOutputMessages,
|
|
8
|
+
getStreamedMessagesOutputMessages,
|
|
9
|
+
} = require('../messages/anthropic')
|
|
6
10
|
const { decode } = require('../messages/utils')
|
|
7
11
|
const { SOURCE_AUTO } = require('../tags')
|
|
8
12
|
const { evaluate } = require('./evaluate')
|
|
13
|
+
const { interceptStream } = require('./stream')
|
|
9
14
|
|
|
10
15
|
const messagesPrepareChannel = channel('dd-trace:anthropic:messages:prepare')
|
|
11
16
|
const messagesInterceptChannel = channel('dd-trace:anthropic:messages:intercept')
|
|
@@ -13,18 +18,21 @@ const messagesInterceptChannel = channel('dd-trace:anthropic:messages:intercept'
|
|
|
13
18
|
let isEnabled = false
|
|
14
19
|
let aiguard
|
|
15
20
|
let opts
|
|
21
|
+
let analyzeStreamResponses
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Subscribes AI Guard to the Anthropic interception channels.
|
|
19
25
|
*
|
|
20
26
|
* @param {object} aiguardInstance
|
|
21
27
|
* @param {boolean} block
|
|
28
|
+
* @param {boolean} analyzeStreams
|
|
22
29
|
*/
|
|
23
|
-
function enable (aiguardInstance, block) {
|
|
30
|
+
function enable (aiguardInstance, block, analyzeStreams) {
|
|
24
31
|
if (isEnabled) return
|
|
25
32
|
|
|
26
33
|
aiguard = aiguardInstance
|
|
27
34
|
opts = { block, source: SOURCE_AUTO, integration: 'anthropic' }
|
|
35
|
+
analyzeStreamResponses = analyzeStreams
|
|
28
36
|
|
|
29
37
|
messagesPrepareChannel.subscribe(onMessagesPrepare)
|
|
30
38
|
messagesInterceptChannel.subscribe(onMessagesIntercept)
|
|
@@ -40,6 +48,7 @@ function disable () {
|
|
|
40
48
|
|
|
41
49
|
aiguard = undefined
|
|
42
50
|
opts = undefined
|
|
51
|
+
analyzeStreamResponses = undefined
|
|
43
52
|
isEnabled = false
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -76,19 +85,35 @@ function onMessagesIntercept (ctx) {
|
|
|
76
85
|
return inputEvaluation
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
const isStream = ctx.arguments[0].stream
|
|
89
|
+
if (isStream && !analyzeStreamResponses) return
|
|
90
|
+
|
|
79
91
|
// One model call has one output however many readers observe it: `parse`, `json()`, `text()`
|
|
80
92
|
// and every `clone()` of the raw response share this callback.
|
|
81
93
|
let outputEvaluation
|
|
82
94
|
ctx.onResult = body => {
|
|
83
95
|
if (!isEnabled) return body
|
|
84
96
|
|
|
97
|
+
if (isStream) {
|
|
98
|
+
outputEvaluation ??= interceptStream(body, chunks => {
|
|
99
|
+
if (!isEnabled) return
|
|
100
|
+
const outputMessages = decode(
|
|
101
|
+
() => getStreamedMessagesOutputMessages(chunks),
|
|
102
|
+
null,
|
|
103
|
+
'AIGuard: unable to decode the streamed Anthropic response: %s'
|
|
104
|
+
)
|
|
105
|
+
if (!outputMessages?.length) return
|
|
106
|
+
return evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
|
|
107
|
+
})
|
|
108
|
+
return outputEvaluation
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
const outputMessages = decode(
|
|
86
112
|
() => getMessagesOutputMessages(body),
|
|
87
113
|
null,
|
|
88
114
|
'AIGuard: unable to decode Anthropic response body: %s'
|
|
89
115
|
)
|
|
90
|
-
if (outputMessages
|
|
91
|
-
if (!outputMessages.length) return body
|
|
116
|
+
if (!outputMessages?.length) return body
|
|
92
117
|
|
|
93
118
|
outputEvaluation ??= evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
|
|
94
119
|
return outputEvaluation.then(() => body)
|
|
@@ -16,7 +16,7 @@ let isEnabled = false
|
|
|
16
16
|
function enable (aiguard, block, analyzeStreamResponses) {
|
|
17
17
|
if (isEnabled) return
|
|
18
18
|
|
|
19
|
-
anthropic.enable(aiguard, block)
|
|
19
|
+
anthropic.enable(aiguard, block, analyzeStreamResponses)
|
|
20
20
|
openai.enable(aiguard, block, analyzeStreamResponses)
|
|
21
21
|
vercelAi.enable(aiguard, block, analyzeStreamResponses)
|
|
22
22
|
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
const { decode } = require('../messages/utils')
|
|
14
14
|
const { SOURCE_AUTO } = require('../tags')
|
|
15
15
|
const { evaluate } = require('./evaluate')
|
|
16
|
+
const { interceptStream } = require('./stream')
|
|
16
17
|
|
|
17
18
|
const chatCompletionsInterceptChannel = channel('dd-trace:openai:chat.completions:intercept')
|
|
18
19
|
const responsesInterceptChannel = channel('dd-trace:openai:responses:intercept')
|
|
@@ -74,9 +75,16 @@ function onChatCompletions (ctx) {
|
|
|
74
75
|
|
|
75
76
|
if (isStream) {
|
|
76
77
|
outputEvaluation ??= interceptStream(body, chunks => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
if (!isEnabled) return
|
|
79
|
+
const conversations = decode(
|
|
80
|
+
() => getStreamedChatCompletionsOutputMessages(chunks)
|
|
81
|
+
.map(message => [...inputMessages, message]),
|
|
82
|
+
null,
|
|
83
|
+
'AIGuard: unable to decode the streamed OpenAI response: %s'
|
|
84
|
+
)
|
|
85
|
+
if (!conversations?.length) return
|
|
86
|
+
return evaluate(ctx, aiguard, conversations, opts)
|
|
87
|
+
})
|
|
80
88
|
return outputEvaluation
|
|
81
89
|
}
|
|
82
90
|
|
|
@@ -112,9 +120,15 @@ function onResponses (ctx) {
|
|
|
112
120
|
|
|
113
121
|
if (isStream) {
|
|
114
122
|
outputEvaluation ??= interceptStream(body, chunks => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
if (!isEnabled) return
|
|
124
|
+
const outputMessages = decode(
|
|
125
|
+
() => getStreamedResponsesOutputMessages(chunks),
|
|
126
|
+
null,
|
|
127
|
+
'AIGuard: unable to decode the streamed OpenAI response: %s'
|
|
128
|
+
)
|
|
129
|
+
if (!outputMessages?.length) return
|
|
130
|
+
return evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
|
|
131
|
+
})
|
|
118
132
|
return outputEvaluation
|
|
119
133
|
}
|
|
120
134
|
|
|
@@ -130,54 +144,4 @@ function onResponses (ctx) {
|
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
/**
|
|
134
|
-
* Uses the OpenAI SDK's stream splitting support to evaluate one branch and return the other.
|
|
135
|
-
*
|
|
136
|
-
* @param {object} stream
|
|
137
|
-
* @param {(chunks: Array<object>) => Array<Array<object>>} getConversations
|
|
138
|
-
* @param {object} ctx
|
|
139
|
-
* @returns {object|Promise<object>}
|
|
140
|
-
*/
|
|
141
|
-
function interceptStream (stream, getConversations, ctx) {
|
|
142
|
-
if (typeof stream?.tee !== 'function') return stream
|
|
143
|
-
|
|
144
|
-
let branches
|
|
145
|
-
try {
|
|
146
|
-
branches = stream.tee()
|
|
147
|
-
} catch {
|
|
148
|
-
return stream
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const [evaluationStream, resultStream] = branches
|
|
152
|
-
return drainStream(evaluationStream).then(chunks => {
|
|
153
|
-
if (!isEnabled) return resultStream
|
|
154
|
-
const conversations = decode(
|
|
155
|
-
() => getConversations(chunks),
|
|
156
|
-
null,
|
|
157
|
-
'AIGuard: unable to decode the streamed OpenAI response: %s'
|
|
158
|
-
)
|
|
159
|
-
if (!conversations?.length) return resultStream
|
|
160
|
-
return evaluate(ctx, aiguard, conversations, opts).then(() => resultStream)
|
|
161
|
-
}, () => resultStream)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* @param {object} stream
|
|
166
|
-
* @returns {Promise<Array<object>>}
|
|
167
|
-
*/
|
|
168
|
-
function drainStream (stream) {
|
|
169
|
-
const chunks = []
|
|
170
|
-
const iterator = stream[Symbol.asyncIterator]()
|
|
171
|
-
|
|
172
|
-
function readAll () {
|
|
173
|
-
return iterator.next().then(({ done, value }) => {
|
|
174
|
-
if (done) return chunks
|
|
175
|
-
chunks.push(value)
|
|
176
|
-
return readAll()
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return readAll()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
147
|
module.exports = { enable, disable }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits an SDK stream, consumes one branch, and returns the other after inspection.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} stream
|
|
9
|
+
* @param {(chunks: Array<object>) => void|Promise<void>} inspect
|
|
10
|
+
* @returns {object|Promise<object>}
|
|
11
|
+
*/
|
|
12
|
+
function interceptStream (stream, inspect) {
|
|
13
|
+
if (typeof stream?.tee !== 'function') return stream
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const [inspectionStream, resultStream] = stream.tee()
|
|
17
|
+
return drainStream(inspectionStream).then(chunks => {
|
|
18
|
+
return Promise.resolve(inspect(chunks)).then(() => resultStream)
|
|
19
|
+
})
|
|
20
|
+
} catch {
|
|
21
|
+
return stream
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Buffers the whole stream. A stream that ends early still yields what it delivered: truncated
|
|
27
|
+
* output can carry the very violation the evaluation is looking for, so it is judged rather than
|
|
28
|
+
* skipped, and the read failure is logged because nothing downstream reports it.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} stream
|
|
31
|
+
* @returns {Promise<Array<object>>}
|
|
32
|
+
*/
|
|
33
|
+
function drainStream (stream) {
|
|
34
|
+
const chunks = []
|
|
35
|
+
const iterator = stream[Symbol.asyncIterator]()
|
|
36
|
+
|
|
37
|
+
function onReadError (error) {
|
|
38
|
+
log.error('AIGuard: the streamed response ended after %s chunks: %s', chunks.length, error)
|
|
39
|
+
return chunks
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readAll () {
|
|
43
|
+
let next
|
|
44
|
+
try {
|
|
45
|
+
next = iterator.next()
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return Promise.resolve(onReadError(error))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next.then(({ done, value }) => {
|
|
51
|
+
if (done) return chunks
|
|
52
|
+
chunks.push(value)
|
|
53
|
+
return readAll()
|
|
54
|
+
}, onReadError)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return readAll()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { interceptStream }
|