dd-trace 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +1 -2
- package/index.d.ts +21 -1
- package/package.json +32 -29
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +21 -10
- package/packages/datadog-plugin-graphql/src/index.js +17 -1
- package/packages/datadog-plugin-graphql/src/utils.js +9 -2
- package/packages/datadog-plugin-graphql/src/validate.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +51 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { readFileSync } = require('node:fs')
|
|
4
|
+
const { extname } = require('node:path')
|
|
5
|
+
|
|
6
|
+
const getConfig = require('../../config')
|
|
7
|
+
const request = require('../../exporters/common/request')
|
|
8
|
+
const log = require('../../log')
|
|
9
|
+
|
|
10
|
+
const UPLOAD_TIMEOUT_MS = 30_000
|
|
11
|
+
const TEST_SCREENSHOT_ENDPOINT_PREFIX = '/api/v2/ci/test-runs/'
|
|
12
|
+
const TEST_SCREENSHOT_ENDPOINT_SUFFIX = '/media'
|
|
13
|
+
const UINT64_MAX = 18_446_744_073_709_551_615n
|
|
14
|
+
|
|
15
|
+
function getContentType (filePath) {
|
|
16
|
+
const extension = extname(filePath).toLowerCase()
|
|
17
|
+
if (extension === '.gif') {
|
|
18
|
+
return 'image/gif'
|
|
19
|
+
}
|
|
20
|
+
if (extension === '.jpg' || extension === '.jpeg') {
|
|
21
|
+
return 'image/jpeg'
|
|
22
|
+
}
|
|
23
|
+
if (extension === '.webp') {
|
|
24
|
+
return 'image/webp'
|
|
25
|
+
}
|
|
26
|
+
return 'image/png'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isValidTraceId (traceId) {
|
|
30
|
+
if (!/^[1-9]\d*$/.test(traceId)) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
return BigInt(traceId) <= UINT64_MAX
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Renders the idempotency key (`${traceId}:${basename(filePath)}`) into a value safe to carry in
|
|
38
|
+
* the upload's query string. The Agent's evp_proxy validates the forwarded query against a
|
|
39
|
+
* restrictive charset and rejects a raw Cypress filename (spaces, parens, non-ASCII), so the
|
|
40
|
+
* filename part is hex-encoded to [0-9a-f]; the decimal trace id and ':' separator are already in
|
|
41
|
+
* the allowed set and stay readable. Deterministic, so a retried upload reproduces the same key
|
|
42
|
+
* and the backend's UUIDv5 overwrite-on-retry holds.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} idempotencyKey - The raw idempotency key (`<traceId>:<filename>`)
|
|
45
|
+
* @returns {string} A query-safe, deterministic representation of the key
|
|
46
|
+
*/
|
|
47
|
+
function toIdempotencyQueryValue (idempotencyKey) {
|
|
48
|
+
const separatorIndex = idempotencyKey.indexOf(':')
|
|
49
|
+
if (separatorIndex === -1) {
|
|
50
|
+
return Buffer.from(idempotencyKey, 'utf8').toString('hex')
|
|
51
|
+
}
|
|
52
|
+
const traceIdPart = idempotencyKey.slice(0, separatorIndex)
|
|
53
|
+
const filenamePart = idempotencyKey.slice(separatorIndex + 1)
|
|
54
|
+
return `${traceIdPart}:${Buffer.from(filenamePart, 'utf8').toString('hex')}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Uploads a single test screenshot to the Test Optimization media intake.
|
|
59
|
+
* The trace id is included in the request path and the body is the raw image bytes.
|
|
60
|
+
*
|
|
61
|
+
* The media service requires two values from the tracer, sent as query params so they
|
|
62
|
+
* survive the Agent's evp_proxy (which forwards only an allow-listed header set and would
|
|
63
|
+
* otherwise strip the metadata, leaving the backend with an empty idempotency key):
|
|
64
|
+
* - `idempotencyKey`: stable per artifact and reused on retry, so a retried
|
|
65
|
+
* upload overwrites the same stored object instead of creating a duplicate.
|
|
66
|
+
* - `capturedAtMs`: the capture time in epoch milliseconds, stamped once at
|
|
67
|
+
* capture and resent unchanged on retry (it is part of the stored object key).
|
|
68
|
+
*
|
|
69
|
+
* @param {object} options - Upload options
|
|
70
|
+
* @param {string} options.filePath - Path to the screenshot file
|
|
71
|
+
* @param {string} options.traceId - Test trace id used as the screenshot key
|
|
72
|
+
* @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
|
|
73
|
+
* @param {number} options.capturedAtMs - Capture time in epoch milliseconds
|
|
74
|
+
* @param {URL} options.url - The base URL for the screenshot upload
|
|
75
|
+
* @param {boolean} [options.isEvpProxy] - Whether to upload through the Agent's evp_proxy
|
|
76
|
+
* @param {string} [options.evpProxyPrefix] - The evp_proxy path prefix (e.g. '/evp_proxy/v4')
|
|
77
|
+
* @param {Function} callback - Callback function (err)
|
|
78
|
+
*/
|
|
79
|
+
function uploadTestScreenshot (
|
|
80
|
+
{ filePath, traceId, idempotencyKey, capturedAtMs, url, isEvpProxy, evpProxyPrefix },
|
|
81
|
+
callback
|
|
82
|
+
) {
|
|
83
|
+
const { DD_API_KEY } = getConfig()
|
|
84
|
+
|
|
85
|
+
if (!isValidTraceId(traceId)) {
|
|
86
|
+
return callback(new Error('A non-zero decimal uint64 trace_id is required for test screenshot upload'))
|
|
87
|
+
}
|
|
88
|
+
if (!DD_API_KEY && !isEvpProxy) {
|
|
89
|
+
return callback(new Error('DD_API_KEY is required for test screenshot upload'))
|
|
90
|
+
}
|
|
91
|
+
if (!idempotencyKey) {
|
|
92
|
+
return callback(new Error('An idempotency key is required for test screenshot upload'))
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isInteger(capturedAtMs) || capturedAtMs <= 0) {
|
|
95
|
+
return callback(new Error('A positive captured-at timestamp (epoch ms) is required for test screenshot upload'))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let screenshotContent
|
|
99
|
+
try {
|
|
100
|
+
screenshotContent = readFileSync(filePath)
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return callback(new Error(`Failed to read screenshot at ${filePath}: ${err.message}`))
|
|
103
|
+
}
|
|
104
|
+
if (screenshotContent.length === 0) {
|
|
105
|
+
return callback(new Error(`Screenshot at ${filePath} is empty`))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Metadata rides the query string, not X-Dd-* headers: the Agent's evp_proxy strips
|
|
109
|
+
// non-allow-listed headers, so header-borne metadata reached the backend empty. The key is
|
|
110
|
+
// rendered proxy-safe (see toIdempotencyQueryValue) because evp_proxy also validates the
|
|
111
|
+
// forwarded query against a restrictive charset. capturedAtMs is a plain integer and part of the
|
|
112
|
+
// stored object key, so it (and the key) must stay stable across retries.
|
|
113
|
+
const query = new URLSearchParams({
|
|
114
|
+
idempotency_key: toIdempotencyQueryValue(idempotencyKey),
|
|
115
|
+
captured_at_ms: String(capturedAtMs),
|
|
116
|
+
}).toString()
|
|
117
|
+
const basePath = `${TEST_SCREENSHOT_ENDPOINT_PREFIX}${traceId}${TEST_SCREENSHOT_ENDPOINT_SUFFIX}`
|
|
118
|
+
|
|
119
|
+
const contentType = getContentType(filePath)
|
|
120
|
+
const options = {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': contentType,
|
|
124
|
+
},
|
|
125
|
+
path: `${basePath}?${query}`,
|
|
126
|
+
timeout: UPLOAD_TIMEOUT_MS,
|
|
127
|
+
url,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isEvpProxy) {
|
|
131
|
+
// Agent mode: prefix the evp_proxy path, tell the proxy which subdomain to forward to, and
|
|
132
|
+
// drop the API key — the Agent injects it. The query params survive the proxy.
|
|
133
|
+
options.path = `${evpProxyPrefix}${basePath}?${query}`
|
|
134
|
+
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
|
|
135
|
+
} else {
|
|
136
|
+
options.headers['DD-API-KEY'] = DD_API_KEY
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.debug('Uploading test screenshot %s to %s', filePath, new URL(options.path, url).href)
|
|
140
|
+
|
|
141
|
+
request(screenshotContent, options, (err, res, statusCode) => {
|
|
142
|
+
if (err) {
|
|
143
|
+
log.error('Error uploading test screenshot: %s', err.message)
|
|
144
|
+
return callback(err)
|
|
145
|
+
}
|
|
146
|
+
if (statusCode === undefined) {
|
|
147
|
+
const uploadError = new Error('Test screenshot upload request was dropped before it was sent')
|
|
148
|
+
log.error('Error uploading test screenshot: %s', uploadError.message)
|
|
149
|
+
return callback(uploadError)
|
|
150
|
+
}
|
|
151
|
+
log.debug('Test screenshot uploaded successfully (status: %d)', statusCode)
|
|
152
|
+
callback(null)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { TEST_SCREENSHOT_ENDPOINT_PREFIX, TEST_SCREENSHOT_ENDPOINT_SUFFIX, uploadTestScreenshot }
|
|
@@ -213,6 +213,7 @@ export interface GeneratedConfig {
|
|
|
213
213
|
DD_TRACE_BUNYAN_ENABLED: boolean;
|
|
214
214
|
DD_TRACE_CASSANDRA_DRIVER_ENABLED: boolean;
|
|
215
215
|
DD_TRACE_CHILD_PROCESS_ENABLED: boolean;
|
|
216
|
+
DD_TRACE_CLAUDE_AGENT_SDK_ENABLED: boolean;
|
|
216
217
|
DD_TRACE_COLLECTIONS_ENABLED: boolean;
|
|
217
218
|
DD_TRACE_COMMONPLUGIN_ENABLED: boolean;
|
|
218
219
|
DD_TRACE_CONFLUENTINC_KAFKA_JAVASCRIPT_ENABLED: boolean;
|
|
@@ -252,11 +253,14 @@ export interface GeneratedConfig {
|
|
|
252
253
|
DD_TRACE_GOOGLE_CLOUD_VERTEXAI_ENABLED: boolean;
|
|
253
254
|
DD_TRACE_GOOGLE_GAX_ENABLED: boolean;
|
|
254
255
|
DD_TRACE_GOOGLE_GENAI_ENABLED: boolean;
|
|
256
|
+
DD_TRACE_GRAPHQL_COLLAPSE: boolean;
|
|
257
|
+
DD_TRACE_GRAPHQL_DEPTH: number;
|
|
255
258
|
DD_TRACE_GRAPHQL_ENABLED: boolean;
|
|
256
259
|
DD_TRACE_GRAPHQL_ERROR_EXTENSIONS: string[];
|
|
257
260
|
DD_TRACE_GRAPHQL_TAG_ENABLED: boolean;
|
|
258
261
|
DD_TRACE_GRAPHQL_TOOLS_ENABLED: boolean;
|
|
259
262
|
DD_TRACE_GRAPHQL_TOOLS_EXECUTOR_ENABLED: boolean;
|
|
263
|
+
DD_TRACE_GRAPHQL_VARIABLES: string[];
|
|
260
264
|
DD_TRACE_GRAPHQL_YOGA_ENABLED: boolean;
|
|
261
265
|
DD_TRACE_GRPC_ENABLED: boolean;
|
|
262
266
|
DD_TRACE_GRPC_GRPC_JS_ENABLED: boolean;
|
|
@@ -558,6 +562,7 @@ export interface GeneratedConfig {
|
|
|
558
562
|
DD_CIVISIBILITY_TEST_MODULE_ID: string | undefined;
|
|
559
563
|
DD_CIVISIBILITY_TEST_SESSION_ID: string | undefined;
|
|
560
564
|
DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
|
|
565
|
+
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
|
|
561
566
|
DD_TEST_FLEET_CONFIG_PATH: string | undefined;
|
|
562
567
|
DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
|
|
563
568
|
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
|
|
@@ -790,6 +795,7 @@ export interface GeneratedEnvVarConfig {
|
|
|
790
795
|
DD_TELEMETRY_LOG_COLLECTION_ENABLED: boolean;
|
|
791
796
|
DD_TELEMETRY_METRICS_ENABLED: boolean;
|
|
792
797
|
DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
|
|
798
|
+
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
|
|
793
799
|
DD_TEST_FLEET_CONFIG_PATH: string | undefined;
|
|
794
800
|
DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
|
|
795
801
|
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
|
|
@@ -868,6 +874,7 @@ export interface GeneratedEnvVarConfig {
|
|
|
868
874
|
DD_TRACE_BUNYAN_ENABLED: boolean;
|
|
869
875
|
DD_TRACE_CASSANDRA_DRIVER_ENABLED: boolean;
|
|
870
876
|
DD_TRACE_CHILD_PROCESS_ENABLED: boolean;
|
|
877
|
+
DD_TRACE_CLAUDE_AGENT_SDK_ENABLED: boolean;
|
|
871
878
|
DD_TRACE_CLIENT_IP_ENABLED: boolean;
|
|
872
879
|
DD_TRACE_CLIENT_IP_HEADER: string | undefined;
|
|
873
880
|
DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH: number;
|
|
@@ -917,11 +924,14 @@ export interface GeneratedEnvVarConfig {
|
|
|
917
924
|
DD_TRACE_GOOGLE_CLOUD_VERTEXAI_ENABLED: boolean;
|
|
918
925
|
DD_TRACE_GOOGLE_GAX_ENABLED: boolean;
|
|
919
926
|
DD_TRACE_GOOGLE_GENAI_ENABLED: boolean;
|
|
927
|
+
DD_TRACE_GRAPHQL_COLLAPSE: boolean;
|
|
928
|
+
DD_TRACE_GRAPHQL_DEPTH: number;
|
|
920
929
|
DD_TRACE_GRAPHQL_ENABLED: boolean;
|
|
921
930
|
DD_TRACE_GRAPHQL_ERROR_EXTENSIONS: string[];
|
|
922
931
|
DD_TRACE_GRAPHQL_TAG_ENABLED: boolean;
|
|
923
932
|
DD_TRACE_GRAPHQL_TOOLS_ENABLED: boolean;
|
|
924
933
|
DD_TRACE_GRAPHQL_TOOLS_EXECUTOR_ENABLED: boolean;
|
|
934
|
+
DD_TRACE_GRAPHQL_VARIABLES: string[];
|
|
925
935
|
DD_TRACE_GRAPHQL_YOGA_ENABLED: boolean;
|
|
926
936
|
DD_TRACE_GRPC_ENABLED: boolean;
|
|
927
937
|
DD_TRACE_GRPC_GRPC_JS_ENABLED: boolean;
|
|
@@ -1819,6 +1819,14 @@
|
|
|
1819
1819
|
"namespace": "testOptimization"
|
|
1820
1820
|
}
|
|
1821
1821
|
],
|
|
1822
|
+
"DD_TEST_FAILURE_SCREENSHOTS_ENABLED": [
|
|
1823
|
+
{
|
|
1824
|
+
"implementation": "A",
|
|
1825
|
+
"type": "boolean",
|
|
1826
|
+
"default": null,
|
|
1827
|
+
"namespace": "testOptimization"
|
|
1828
|
+
}
|
|
1829
|
+
],
|
|
1822
1830
|
"DD_TEST_FLEET_CONFIG_PATH": [
|
|
1823
1831
|
{
|
|
1824
1832
|
"implementation": "A",
|
|
@@ -1946,6 +1954,13 @@
|
|
|
1946
1954
|
"default": "true"
|
|
1947
1955
|
}
|
|
1948
1956
|
],
|
|
1957
|
+
"DD_TRACE_CLAUDE_AGENT_SDK_ENABLED": [
|
|
1958
|
+
{
|
|
1959
|
+
"implementation": "A",
|
|
1960
|
+
"type": "boolean",
|
|
1961
|
+
"default": "true"
|
|
1962
|
+
}
|
|
1963
|
+
],
|
|
1949
1964
|
"DD_TRACE_APOLLO_ENABLED": [
|
|
1950
1965
|
{
|
|
1951
1966
|
"implementation": "A",
|
|
@@ -2742,6 +2757,21 @@
|
|
|
2742
2757
|
"default": "true"
|
|
2743
2758
|
}
|
|
2744
2759
|
],
|
|
2760
|
+
"DD_TRACE_GRAPHQL_COLLAPSE": [
|
|
2761
|
+
{
|
|
2762
|
+
"implementation": "A",
|
|
2763
|
+
"type": "boolean",
|
|
2764
|
+
"default": "true"
|
|
2765
|
+
}
|
|
2766
|
+
],
|
|
2767
|
+
"DD_TRACE_GRAPHQL_DEPTH": [
|
|
2768
|
+
{
|
|
2769
|
+
"implementation": "A",
|
|
2770
|
+
"type": "int",
|
|
2771
|
+
"default": "-1",
|
|
2772
|
+
"allowed": "-1|\\d+"
|
|
2773
|
+
}
|
|
2774
|
+
],
|
|
2745
2775
|
"DD_TRACE_GRAPHQL_ENABLED": [
|
|
2746
2776
|
{
|
|
2747
2777
|
"implementation": "A",
|
|
@@ -2777,6 +2807,13 @@
|
|
|
2777
2807
|
"default": "true"
|
|
2778
2808
|
}
|
|
2779
2809
|
],
|
|
2810
|
+
"DD_TRACE_GRAPHQL_VARIABLES": [
|
|
2811
|
+
{
|
|
2812
|
+
"implementation": "A",
|
|
2813
|
+
"type": "array",
|
|
2814
|
+
"default": ""
|
|
2815
|
+
}
|
|
2816
|
+
],
|
|
2780
2817
|
"DD_TRACE_GRAPHQL_YOGA_ENABLED": [
|
|
2781
2818
|
{
|
|
2782
2819
|
"implementation": "A",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { EOL } = require('node:os')
|
|
3
|
+
const { EOL, platform } = require('node:os')
|
|
4
4
|
|
|
5
5
|
// Load binding first to not import other modules if it throws
|
|
6
6
|
const libdatadog = require('@datadog/libdatadog')
|
|
@@ -71,7 +71,7 @@ class Crashtracker {
|
|
|
71
71
|
|
|
72
72
|
// Out-of-process symbolication currently works on
|
|
73
73
|
// Linux only, does not work on Mac.
|
|
74
|
-
const resolveMode =
|
|
74
|
+
const resolveMode = platform() === 'linux'
|
|
75
75
|
? 'EnabledWithSymbolsInReceiver'
|
|
76
76
|
: 'EnabledWithInprocessSymbols'
|
|
77
77
|
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { channel } = require('dc-polyfill')
|
|
4
|
+
const BaseLLMObsPlugin = require('../base')
|
|
5
|
+
const { getModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
|
|
6
|
+
|
|
7
|
+
const toolCreationCh = channel('tracing:orchestrion:ai:tool:start')
|
|
8
|
+
const setAttributesCh = channel('dd-trace:vercel-ai:span:setAttributes')
|
|
9
|
+
|
|
10
|
+
const { MODEL_NAME, MODEL_PROVIDER, NAME } = require('../../constants/tags')
|
|
11
|
+
const {
|
|
12
|
+
getSpanTags,
|
|
13
|
+
getOperation,
|
|
14
|
+
getUsage,
|
|
15
|
+
getJsonStringValue,
|
|
16
|
+
getModelMetadata,
|
|
17
|
+
getGenerationMetadata,
|
|
18
|
+
getToolNameFromTags,
|
|
19
|
+
getToolCallResultContent,
|
|
20
|
+
getLlmObsSpanName,
|
|
21
|
+
getTelemetryMetadata,
|
|
22
|
+
} = require('./util')
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Record<string, unknown> & { description?: string, id?: string }} AvailableToolArgs
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {string | number | boolean | null | undefined | string[] | number[] | boolean[]} TagValue
|
|
30
|
+
* @typedef {Record<string, TagValue>} SpanTags
|
|
31
|
+
*
|
|
32
|
+
* @typedef {{ name?: string, description?: string }} ToolForModel
|
|
33
|
+
*
|
|
34
|
+
* @typedef {{ type: 'text' | 'reasoning' | 'redacted-reasoning', text?: string, data?: string }} TextPart
|
|
35
|
+
* @typedef {{ type: 'tool-call', toolName: string, toolCallId: string, args?: unknown, input?: unknown }} ToolCallPart
|
|
36
|
+
* @typedef {(
|
|
37
|
+
* { type: 'tool-result', toolCallId: string, output?: { type: string, value?: unknown }, result?: unknown } &
|
|
38
|
+
* Record<string, unknown>
|
|
39
|
+
* )} ToolResultPart
|
|
40
|
+
*
|
|
41
|
+
* @typedef {{
|
|
42
|
+
* role: 'system',
|
|
43
|
+
* content: string
|
|
44
|
+
* } | {
|
|
45
|
+
* role: 'user',
|
|
46
|
+
* content: TextPart[]
|
|
47
|
+
* } | {
|
|
48
|
+
* role: 'assistant',
|
|
49
|
+
* content: Array<TextPart | ToolCallPart>
|
|
50
|
+
* } | {
|
|
51
|
+
* role: 'tool',
|
|
52
|
+
* content: ToolResultPart[]
|
|
53
|
+
* }} AiSdkMessage
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const SPAN_NAME_TO_KIND_MAPPING = {
|
|
57
|
+
// embeddings
|
|
58
|
+
embed: 'workflow',
|
|
59
|
+
embedMany: 'workflow',
|
|
60
|
+
doEmbed: 'embedding',
|
|
61
|
+
// object generation
|
|
62
|
+
generateObject: 'workflow',
|
|
63
|
+
streamObject: 'workflow',
|
|
64
|
+
// text generation
|
|
65
|
+
generateText: 'workflow',
|
|
66
|
+
streamText: 'workflow',
|
|
67
|
+
// llm operations
|
|
68
|
+
doGenerate: 'llm',
|
|
69
|
+
doStream: 'llm',
|
|
70
|
+
// tools
|
|
71
|
+
toolCall: 'tool',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class DdTelemetryPlugin extends BaseLLMObsPlugin {
|
|
75
|
+
static id = 'ai_llmobs_dd_telemetry'
|
|
76
|
+
static integration = 'ai'
|
|
77
|
+
static prefix = 'tracing:dd-trace:vercel-ai'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The available tools within the runtime scope of this integration.
|
|
81
|
+
* This essentially acts as a global registry for all tools made through the Vercel AI SDK.
|
|
82
|
+
* @type {Set<AvailableToolArgs>}
|
|
83
|
+
*/
|
|
84
|
+
#availableTools
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A mapping of tool call IDs to tool names.
|
|
88
|
+
* This is used to map the tool call ID to the tool name for the output message.
|
|
89
|
+
* @type {Record<string, string>}
|
|
90
|
+
*/
|
|
91
|
+
#toolCallIdsToName
|
|
92
|
+
|
|
93
|
+
constructor (...args) {
|
|
94
|
+
super(...args)
|
|
95
|
+
|
|
96
|
+
this.#toolCallIdsToName = {}
|
|
97
|
+
this.#availableTools = new Set()
|
|
98
|
+
toolCreationCh.subscribe(ctx => {
|
|
99
|
+
const toolArgs = ctx.arguments
|
|
100
|
+
const tool = toolArgs[0] ?? {}
|
|
101
|
+
this.#availableTools.add(tool)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
setAttributesCh.subscribe(({ ctx, attributes }) => {
|
|
105
|
+
Object.assign(ctx.attributes, attributes)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Does a best-effort attempt to find the right tool name for the given tool description.
|
|
111
|
+
* This is because the Vercel AI SDK does not tag tools by name properly, but
|
|
112
|
+
* rather by the index they were passed in. Tool names appear nowhere in the span tags.
|
|
113
|
+
*
|
|
114
|
+
* We use the tool description as the next best identifier for a tool.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} toolName
|
|
117
|
+
* @param {string | undefined} toolDescription
|
|
118
|
+
* @returns {string | undefined}
|
|
119
|
+
*/
|
|
120
|
+
findToolName (toolName, toolDescription) {
|
|
121
|
+
if (Number.isNaN(Number.parseInt(toolName))) return toolName
|
|
122
|
+
|
|
123
|
+
for (const availableTool of this.#availableTools) {
|
|
124
|
+
const description = availableTool.description
|
|
125
|
+
if (description === toolDescription && availableTool.id) {
|
|
126
|
+
return availableTool.id
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @override
|
|
133
|
+
*/
|
|
134
|
+
getLLMObsSpanRegisterOptions (ctx) {
|
|
135
|
+
const span = ctx.currentStore?.span
|
|
136
|
+
const operation = getOperation(span)
|
|
137
|
+
const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
|
|
138
|
+
if (!kind) return
|
|
139
|
+
|
|
140
|
+
return { kind, name: getLlmObsSpanName(operation, ctx.attributes['ai.telemetry.functionId']) }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @override
|
|
145
|
+
*/
|
|
146
|
+
setLLMObsTags (ctx) {
|
|
147
|
+
const span = ctx.currentStore?.span
|
|
148
|
+
if (!span) return
|
|
149
|
+
|
|
150
|
+
const operation = getOperation(span)
|
|
151
|
+
const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
|
|
152
|
+
if (!kind) return
|
|
153
|
+
|
|
154
|
+
const tags = getSpanTags(ctx)
|
|
155
|
+
|
|
156
|
+
if (['embedding', 'llm'].includes(kind)) {
|
|
157
|
+
this._tagger._setTag(span, MODEL_NAME, tags['ai.model.id'])
|
|
158
|
+
this._tagger._setTag(span, MODEL_PROVIDER, getModelProvider(tags))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
switch (operation) {
|
|
162
|
+
case 'embed':
|
|
163
|
+
case 'embedMany':
|
|
164
|
+
this.setEmbeddingWorkflowTags(span, tags)
|
|
165
|
+
break
|
|
166
|
+
case 'doEmbed':
|
|
167
|
+
this.setEmbeddingTags(span, tags)
|
|
168
|
+
break
|
|
169
|
+
case 'generateObject':
|
|
170
|
+
case 'streamObject':
|
|
171
|
+
this.setObjectGenerationTags(span, tags)
|
|
172
|
+
break
|
|
173
|
+
case 'generateText':
|
|
174
|
+
case 'streamText':
|
|
175
|
+
this.setTextGenerationTags(span, tags)
|
|
176
|
+
break
|
|
177
|
+
case 'doGenerate':
|
|
178
|
+
case 'doStream':
|
|
179
|
+
this.setLLMOperationTags(span, tags)
|
|
180
|
+
break
|
|
181
|
+
case 'toolCall':
|
|
182
|
+
this.setToolTags(span, tags)
|
|
183
|
+
break
|
|
184
|
+
default:
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setEmbeddingWorkflowTags (span, tags) {
|
|
190
|
+
const inputs = tags['ai.value'] ?? tags['ai.values']
|
|
191
|
+
const parsedInputs = Array.isArray(inputs)
|
|
192
|
+
? inputs.map(input => getJsonStringValue(input, ''))
|
|
193
|
+
: getJsonStringValue(inputs, '')
|
|
194
|
+
|
|
195
|
+
const embeddingsOutput = tags['ai.embedding'] ?? tags['ai.embeddings']
|
|
196
|
+
const isSingleEmbedding = !Array.isArray(embeddingsOutput)
|
|
197
|
+
const numberOfEmbeddings = isSingleEmbedding ? 1 : embeddingsOutput.length
|
|
198
|
+
const embeddingsLength = getJsonStringValue(isSingleEmbedding ? embeddingsOutput : embeddingsOutput?.[0], []).length
|
|
199
|
+
const output = `[${numberOfEmbeddings} embedding(s) returned with size ${embeddingsLength}]`
|
|
200
|
+
|
|
201
|
+
this._tagger.tagTextIO(span, parsedInputs, output)
|
|
202
|
+
|
|
203
|
+
const metadata = getGenerationMetadata(tags)
|
|
204
|
+
this._tagger.tagMetadata(span, metadata)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setEmbeddingTags (span, tags) {
|
|
208
|
+
const inputs = tags['ai.values']
|
|
209
|
+
if (!Array.isArray(inputs)) return
|
|
210
|
+
|
|
211
|
+
const parsedInputs = inputs.map(input => getJsonStringValue(input, ''))
|
|
212
|
+
|
|
213
|
+
const embeddingsOutput = tags['ai.embeddings']
|
|
214
|
+
const numberOfEmbeddings = embeddingsOutput?.length
|
|
215
|
+
const embeddingsLength = getJsonStringValue(embeddingsOutput?.[0], []).length
|
|
216
|
+
const output = `[${numberOfEmbeddings} embedding(s) returned with size ${embeddingsLength}]`
|
|
217
|
+
|
|
218
|
+
this._tagger.tagEmbeddingIO(span, parsedInputs, output)
|
|
219
|
+
|
|
220
|
+
const metadata = getTelemetryMetadata(tags)
|
|
221
|
+
this._tagger.tagMetadata(span, metadata)
|
|
222
|
+
|
|
223
|
+
const usage = tags['ai.usage.tokens']
|
|
224
|
+
this._tagger.tagMetrics(span, {
|
|
225
|
+
inputTokens: usage,
|
|
226
|
+
totalTokens: usage,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setObjectGenerationTags (span, tags) {
|
|
231
|
+
const promptInfo = getJsonStringValue(tags['ai.prompt'], {})
|
|
232
|
+
const lastUserPrompt =
|
|
233
|
+
promptInfo.prompt ??
|
|
234
|
+
promptInfo.messages.reverse().find(message => message.role === 'user')?.content
|
|
235
|
+
const prompt = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
|
|
236
|
+
|
|
237
|
+
const output = tags['ai.response.object']
|
|
238
|
+
|
|
239
|
+
this._tagger.tagTextIO(span, prompt, output)
|
|
240
|
+
|
|
241
|
+
const metadata = getGenerationMetadata(tags)
|
|
242
|
+
metadata.schema = getJsonStringValue(tags['ai.schema'], {})
|
|
243
|
+
this._tagger.tagMetadata(span, metadata)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setTextGenerationTags (span, tags) {
|
|
247
|
+
const promptInfo = getJsonStringValue(tags['ai.prompt'], {})
|
|
248
|
+
const lastUserPrompt =
|
|
249
|
+
promptInfo.prompt ??
|
|
250
|
+
promptInfo.messages.reverse().find(message => message.role === 'user')?.content
|
|
251
|
+
const prompt = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
|
|
252
|
+
|
|
253
|
+
const output = tags['ai.response.text']
|
|
254
|
+
|
|
255
|
+
this._tagger.tagTextIO(span, prompt, output)
|
|
256
|
+
|
|
257
|
+
const metadata = getGenerationMetadata(tags)
|
|
258
|
+
this._tagger.tagMetadata(span, metadata)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {import('../../../opentracing/span')} span
|
|
263
|
+
* @param {SpanTags} tags
|
|
264
|
+
*/
|
|
265
|
+
setLLMOperationTags (span, tags) {
|
|
266
|
+
const toolsForModel = tags['ai.prompt.tools']?.map(getJsonStringValue)
|
|
267
|
+
|
|
268
|
+
const inputMessages = getJsonStringValue(tags['ai.prompt.messages'], [])
|
|
269
|
+
const parsedInputMessages = []
|
|
270
|
+
for (const message of inputMessages) {
|
|
271
|
+
const formattedMessages = this.formatMessage(message, toolsForModel)
|
|
272
|
+
parsedInputMessages.push(...formattedMessages)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const outputMessage = this.formatOutputMessage(tags, toolsForModel)
|
|
276
|
+
|
|
277
|
+
this._tagger.tagLLMIO(span, parsedInputMessages, outputMessage)
|
|
278
|
+
|
|
279
|
+
const metadata = getModelMetadata(tags)
|
|
280
|
+
this._tagger.tagMetadata(span, metadata)
|
|
281
|
+
|
|
282
|
+
const usage = getUsage(tags)
|
|
283
|
+
this._tagger.tagMetrics(span, usage)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setToolTags (span, tags) {
|
|
287
|
+
const toolCallId = tags['ai.toolCall.id']
|
|
288
|
+
const name = getToolNameFromTags(tags) ?? this.#toolCallIdsToName[toolCallId]
|
|
289
|
+
if (name) this._tagger._setTag(span, NAME, name)
|
|
290
|
+
|
|
291
|
+
const input = tags['ai.toolCall.args']
|
|
292
|
+
const output = tags['ai.toolCall.result']
|
|
293
|
+
|
|
294
|
+
this._tagger.tagTextIO(span, input, output)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
formatOutputMessage (tags, toolsForModel) {
|
|
298
|
+
const outputMessageText = tags['ai.response.text'] ?? tags['ai.response.object']
|
|
299
|
+
const outputMessageToolCalls = getJsonStringValue(tags['ai.response.toolCalls'], [])
|
|
300
|
+
|
|
301
|
+
const formattedToolCalls = []
|
|
302
|
+
for (const toolCall of outputMessageToolCalls) {
|
|
303
|
+
const toolArgs = toolCall.args ?? toolCall.input
|
|
304
|
+
const toolCallArgs = typeof toolArgs === 'string' ? getJsonStringValue(toolArgs, {}) : toolArgs
|
|
305
|
+
const toolDescription = toolsForModel?.find(tool => toolCall.toolName === tool.name)?.description
|
|
306
|
+
const name = this.findToolName(toolCall.toolName, toolDescription)
|
|
307
|
+
this.#toolCallIdsToName[toolCall.toolCallId] = name
|
|
308
|
+
|
|
309
|
+
formattedToolCalls.push({
|
|
310
|
+
arguments: toolCallArgs,
|
|
311
|
+
name,
|
|
312
|
+
toolId: toolCall.toolCallId,
|
|
313
|
+
type: toolCall.toolCallType ?? 'function',
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
role: 'assistant',
|
|
319
|
+
content: outputMessageText,
|
|
320
|
+
toolCalls: formattedToolCalls,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Returns a list of formatted messages from a message object.
|
|
326
|
+
* Most of these will just be one entry, but in the case of a "tool" role,
|
|
327
|
+
* it is possible to have multiple tool call results in a single message that we
|
|
328
|
+
* need to split into multiple messages.
|
|
329
|
+
*
|
|
330
|
+
* @param {AiSdkMessage} message
|
|
331
|
+
* @param {ToolForModel[] | null | undefined} toolsForModel
|
|
332
|
+
* @returns {Array<{role: string, content: string, toolId?: string,
|
|
333
|
+
* toolCalls?: Array<{arguments: string, name: string, toolId: string, type: string}>}>}
|
|
334
|
+
*/
|
|
335
|
+
formatMessage (message, toolsForModel) {
|
|
336
|
+
const { role, content } = message
|
|
337
|
+
|
|
338
|
+
if (role === 'system') {
|
|
339
|
+
return [{ role, content }]
|
|
340
|
+
} else if (role === 'user') {
|
|
341
|
+
let finalContent = ''
|
|
342
|
+
for (const part of content) {
|
|
343
|
+
const { type } = part
|
|
344
|
+
if (type === 'text') {
|
|
345
|
+
finalContent += part.text
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return [{ role, content: finalContent }]
|
|
350
|
+
} else if (role === 'assistant') {
|
|
351
|
+
const toolCalls = []
|
|
352
|
+
let finalContent = ''
|
|
353
|
+
|
|
354
|
+
for (const part of content) {
|
|
355
|
+
const { type } = part
|
|
356
|
+
// TODO(sabrenner): do we want to include reasoning?
|
|
357
|
+
if (['text', 'reasoning', 'redacted-reasoning'].includes(type)) {
|
|
358
|
+
finalContent += part.text ?? part.data
|
|
359
|
+
} else if (type === 'tool-call') {
|
|
360
|
+
const toolDescription = toolsForModel?.find(tool => part.toolName === tool.name)?.description
|
|
361
|
+
const name = this.findToolName(part.toolName, toolDescription)
|
|
362
|
+
|
|
363
|
+
toolCalls.push({
|
|
364
|
+
arguments: part.args ?? part.input,
|
|
365
|
+
name,
|
|
366
|
+
toolId: part.toolCallId,
|
|
367
|
+
type: 'function',
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const finalMessage = {
|
|
373
|
+
role,
|
|
374
|
+
content: finalContent,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (toolCalls.length) {
|
|
378
|
+
finalMessage.toolCalls = toolCalls.length ? toolCalls : undefined
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return [finalMessage]
|
|
382
|
+
} else if (role === 'tool') {
|
|
383
|
+
const finalMessages = []
|
|
384
|
+
for (const part of content) {
|
|
385
|
+
if (part.type === 'tool-result') {
|
|
386
|
+
const safeResult = getToolCallResultContent(part)
|
|
387
|
+
|
|
388
|
+
finalMessages.push({
|
|
389
|
+
role,
|
|
390
|
+
content: safeResult,
|
|
391
|
+
toolId: part.toolCallId,
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return finalMessages
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return []
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = DdTelemetryPlugin
|