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.
Files changed (77) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/index.d.ts +21 -1
  3. package/package.json +32 -29
  4. package/packages/datadog-instrumentations/src/ai.js +45 -0
  5. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  6. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  7. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  9. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  10. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  12. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  16. package/packages/datadog-instrumentations/src/jest.js +713 -63
  17. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  18. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  19. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  20. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  21. package/packages/datadog-instrumentations/src/mongoose.js +4 -5
  22. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  23. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
  24. package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
  25. package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
  26. package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
  27. package/packages/datadog-plugin-ai/src/index.js +1 -1
  28. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  29. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  30. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
  31. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  32. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  33. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  34. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  35. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  36. package/packages/datadog-plugin-cucumber/src/index.js +14 -23
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
  38. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  39. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  40. package/packages/datadog-plugin-graphql/src/execute.js +21 -10
  41. package/packages/datadog-plugin-graphql/src/index.js +17 -1
  42. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  43. package/packages/datadog-plugin-graphql/src/validate.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +30 -14
  45. package/packages/datadog-plugin-mocha/src/index.js +39 -14
  46. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  47. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  48. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +3 -2
  50. package/packages/datadog-plugin-vitest/src/index.js +51 -9
  51. package/packages/dd-trace/src/aiguard/index.js +9 -14
  52. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  53. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  54. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  55. package/packages/dd-trace/src/appsec/graphql.js +9 -6
  56. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
  57. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  58. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  59. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
  60. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
  61. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
  62. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  63. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  64. package/packages/dd-trace/src/config/supported-configurations.json +37 -0
  65. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  66. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  68. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  71. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  72. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  73. package/packages/dd-trace/src/plugin_manager.js +16 -0
  74. package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
  75. package/packages/dd-trace/src/plugins/index.js +2 -0
  76. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  77. 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 = require('os').platform === 'linux'
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