dd-trace 6.14.0 → 6.16.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 (98) hide show
  1. package/README.md +15 -0
  2. package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
  3. package/ci/test-optimization-validation/package-check.js +4 -1
  4. package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
  5. package/index.d.ts +12 -0
  6. package/next.d.ts +27 -0
  7. package/next.js +3 -0
  8. package/package.json +17 -13
  9. package/packages/datadog-esbuild/index.js +14 -1
  10. package/packages/datadog-esbuild/src/resolver.js +284 -0
  11. package/packages/datadog-esbuild/src/utils.js +163 -119
  12. package/packages/datadog-instrumentations/src/ai.js +39 -97
  13. package/packages/datadog-instrumentations/src/anthropic.js +76 -134
  14. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
  15. package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
  16. package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
  17. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  18. package/packages/datadog-instrumentations/src/bullmq.js +1 -1
  19. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  20. package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
  21. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  22. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
  23. package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
  24. package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
  25. package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +1 -5
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
  33. package/packages/datadog-instrumentations/src/jest.js +141 -25
  34. package/packages/datadog-instrumentations/src/langchain.js +1 -1
  35. package/packages/datadog-instrumentations/src/langgraph.js +1 -1
  36. package/packages/datadog-instrumentations/src/mercurius.js +1 -1
  37. package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
  38. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
  39. package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
  40. package/packages/datadog-instrumentations/src/openai.js +54 -76
  41. package/packages/datadog-instrumentations/src/playwright.js +4 -0
  42. package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
  43. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
  44. package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
  45. package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
  46. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
  47. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
  48. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
  49. package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
  50. package/packages/datadog-turbopack/index.js +212 -0
  51. package/packages/datadog-turbopack/src/constants.js +5 -0
  52. package/packages/datadog-turbopack/src/loader.js +500 -0
  53. package/packages/dd-trace/src/aiguard/index.js +2 -1
  54. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
  55. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
  56. package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
  57. package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
  58. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
  59. package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
  60. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
  61. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  62. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
  63. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
  64. package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
  65. package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
  66. package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
  67. package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
  68. package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
  69. package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
  70. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  71. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
  72. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  73. package/packages/dd-trace/src/config/helper.js +10 -2
  74. package/packages/dd-trace/src/config/index.js +20 -5
  75. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  76. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
  77. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
  78. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
  79. package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
  80. package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
  81. package/packages/dd-trace/src/evp_proxy/path.js +12 -3
  82. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
  83. package/packages/dd-trace/src/exporters/common/docker.js +4 -1
  84. package/packages/dd-trace/src/exporters/common/proxy.js +55 -0
  85. package/packages/dd-trace/src/exporters/common/request.js +11 -3
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  87. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
  88. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
  89. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +12 -1
  90. package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
  91. package/packages/dd-trace/src/plugin_manager.js +9 -3
  92. package/packages/dd-trace/src/plugins/index.js +1 -0
  93. package/packages/dd-trace/src/plugins/util/test.js +4 -1
  94. package/packages/dd-trace/src/priority_sampler.js +4 -0
  95. package/packages/dd-trace/src/sampling_rule.js +3 -1
  96. package/packages/dd-trace/src/span_processor.js +18 -3
  97. package/packages/dd-trace/src/telemetry/send-data.js +5 -4
  98. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  // The content of this file is copied from the `import-in-the-middle` package with minor modifications (https://www.npmjs.com/package/import-in-the-middle)
4
- const { pathToFileURL, fileURLToPath } = require('node:url')
5
4
  const fs = require('node:fs')
6
5
  const path = require('node:path')
7
- const { NODE_MAJOR, NODE_MINOR } = require('../../../version.js')
6
+ const { fileURLToPath, pathToFileURL } = require('node:url')
8
7
 
9
- const LOAD_OPERATION = 0
10
- const RESOLVE_OPERATION = 1
8
+ const { createEsmResolver, driveGetExportsGenerator } = require('./resolver')
9
+
10
+ /** @typedef {ReturnType<typeof createEsmResolver>} EsmResolver */
11
11
 
12
12
  const getExportsImporting = (url) => import(url).then(Object.keys)
13
13
  let getExportsModulePromise
@@ -19,16 +19,33 @@ const loadGetExportsModule = () => {
19
19
  return getExportsModulePromise
20
20
  }
21
21
 
22
- const getExports = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)
23
- ? async (srcUrl, context, getSource) => {
24
- const mod = await loadGetExportsModule()
25
- const exportNames = mod.getExports(srcUrl, context, getSource)
26
- if (exportNames?.next) {
27
- return driveGetExportsGenerator(exportNames, getSource)
28
- }
29
- return exportNames
22
+ /**
23
+ * @param {URL} srcUrl
24
+ * @param {object} context
25
+ * @param {(url: URL, context: object) => { source: string, format: string }} getSource
26
+ * @param {EsmResolver} resolver
27
+ * @returns {Promise<{
28
+ * exportNames: Iterable<string>,
29
+ * hasModuleExportsCJSDefault: boolean,
30
+ * starReexports?: Array<{ specifier: string, parentURL: string }>
31
+ * }>}
32
+ */
33
+ const getExports = async (srcUrl, context, getSource, resolver) => {
34
+ const mod = await loadGetExportsModule()
35
+ const exportsGenerator = mod.getExports(srcUrl, context, getSource)
36
+ /**
37
+ * @param {string} specifier
38
+ * @param {{ parentURL: URL }} operationContext
39
+ * @returns {Promise<{ format: string, url: URL }>}
40
+ */
41
+ const resolve = (specifier, operationContext) => resolveModule(specifier, operationContext, resolver)
42
+ const result = await driveGetExportsGenerator(exportsGenerator, getSource, resolve)
43
+ return {
44
+ exportNames: result.exportNames,
45
+ hasModuleExportsCJSDefault: mod.hasModuleExportsCJSDefault,
46
+ starReexports: result.starReexports,
30
47
  }
31
- : getExportsImporting
48
+ }
32
49
 
33
50
  function isStarExportLine (line) {
34
51
  return /^\* from /.test(line)
@@ -58,82 +75,37 @@ function isBareSpecifier (specifier) {
58
75
  }
59
76
  }
60
77
 
61
- function resolve (specifier, context) {
62
- // This comes from an import, that is why import makes preference
63
- const conditions = ['import']
64
-
65
- if (specifier.startsWith('file://')) {
66
- specifier = fileURLToPath(specifier)
67
- }
68
-
69
- const resolved = require.resolve(specifier, {
70
- paths: [fileURLToPath(context.parentURL)],
71
- // @ts-expect-error - Node.js 22+ unofficially supports a conditions option
72
- conditions,
73
- })
74
-
75
- return {
76
- url: pathToFileURL(resolved),
77
- format: isESMFile(resolved) ? 'module' : 'commonjs',
78
- }
79
- }
80
-
81
- function getSource (url, { format }) {
82
- return {
83
- source: fs.readFileSync(fileURLToPath(url), 'utf8'),
84
- format,
85
- }
86
- }
87
-
88
78
  /**
89
- * @typedef {[typeof LOAD_OPERATION, URL, object] | [typeof RESOLVE_OPERATION, string, object]} GetExportsOperation
90
- */
91
-
92
- /**
93
- * @typedef {{ done: false, value: GetExportsOperation } | { done: true, value: Set<string> }} GetExportsResult
94
- */
95
-
96
- /**
97
- * @typedef {{
98
- * next: (value?: unknown) => GetExportsResult,
99
- * throw: (error?: unknown) => GetExportsResult,
100
- * }} GetExportsGenerator
79
+ * @param {string} modulePath
80
+ * @param {string} format
81
+ * @returns {string}
101
82
  */
83
+ function getModuleFormat (modulePath, format) {
84
+ if (modulePath.endsWith('.mts')) return 'module-typescript'
85
+ if (modulePath.endsWith('.cts')) return 'commonjs-typescript'
86
+ if (modulePath.endsWith('.ts')) return `${format}-typescript`
87
+ return format
88
+ }
102
89
 
103
90
  /**
104
- * Drives the generator returned by import-in-the-middle >=3.1.0 export discovery.
91
+ * Resolves a module with the import conditions used by ESM instrumentation.
105
92
  *
106
- * @param {GetExportsGenerator} exportsGenerator Generator returned by getExports
107
- * @param {(url: URL, context: object) => { source: string, format: string }} getSource
108
- * Function that loads module source
109
- * @returns {Set<string>}
93
+ * @param {string} specifier
94
+ * @param {{ parentURL: URL }} context
95
+ * @param {EsmResolver} resolver
96
+ * @returns {Promise<{ format: string, url: URL }>}
110
97
  */
111
- function driveGetExportsGenerator (exportsGenerator, getSource) {
112
- let next = exportsGenerator.next()
113
- while (next.done === false) {
114
- let result
115
- let error
116
- let threw = false
117
-
118
- try {
119
- const operation = next.value
120
- const operationType = operation[0]
121
-
122
- if (operationType === LOAD_OPERATION) {
123
- result = getSource(operation[1], operation[2])
124
- } else if (operationType === RESOLVE_OPERATION) {
125
- result = resolve(operation[1], operation[2])
126
- } else {
127
- throw new Error(`Unsupported import-in-the-middle getExports operation: ${operationType}`)
128
- }
129
- } catch (err) {
130
- threw = true
131
- error = err
132
- }
133
-
134
- next = threw ? exportsGenerator.throw(error) : exportsGenerator.next(result)
98
+ async function resolveModule (specifier, context, resolver) {
99
+ const url = new URL(await resolver.resolve(specifier, context.parentURL))
100
+ if (url.protocol === 'node:') return { format: 'builtin', url }
101
+ if (url.protocol !== 'file:') throw new Error(`Unsupported ESM resolution URL: ${url.href}`)
102
+
103
+ const resolved = fileURLToPath(url)
104
+ if (resolved.endsWith('.node')) {
105
+ throw new Error(`Unsupported ESM analysis target: ${resolved}`)
135
106
  }
136
- return next.value
107
+ const format = isESMFile(resolved) ? 'module' : 'commonjs'
108
+ return { format, url }
137
109
  }
138
110
 
139
111
  /**
@@ -144,16 +116,72 @@ function driveGetExportsGenerator (exportsGenerator, getSource) {
144
116
  * @param {boolean} [moduleData.internal]
145
117
  * @param {object} moduleData.context
146
118
  * @param {boolean} [moduleData.excludeDefault]
119
+ * @param {Map<string, string>} [moduleData.moduleSources]
120
+ * @param {EsmResolver} [moduleData.resolver]
121
+ * @param {(source: string, options: { loader: 'ts' }) => { code: string }} [moduleData.transform]
122
+ * @param {Set<string>} [activeModules]
147
123
  * @returns {Promise<Map>}
148
124
  */
149
- async function processModule ({ path, internal = false, context, excludeDefault = false }) {
150
- let exportNames, srcUrl
125
+ async function processModule (
126
+ { path, internal = false, context, excludeDefault = false, moduleSources = new Map(), resolver, transform },
127
+ activeModules
128
+ ) {
129
+ const ownsResolver = resolver === undefined
130
+ resolver ??= createEsmResolver()
131
+ try {
132
+ return await processModuleWithResolver(
133
+ { path, internal, context, excludeDefault, moduleSources, transform },
134
+ activeModules,
135
+ resolver
136
+ )
137
+ } finally {
138
+ if (ownsResolver) await resolver.close()
139
+ }
140
+ }
141
+
142
+ /**
143
+ * @param {object} moduleData
144
+ * @param {string} moduleData.path
145
+ * @param {boolean} moduleData.internal
146
+ * @param {object} moduleData.context
147
+ * @param {boolean} moduleData.excludeDefault
148
+ * @param {Map<string, string>} moduleData.moduleSources
149
+ * @param {(source: string, options: { loader: 'ts' }) => { code: string }} [moduleData.transform]
150
+ * @param {Set<string>} [activeModules]
151
+ * @param {EsmResolver} resolver
152
+ * @returns {Promise<Map>}
153
+ */
154
+ async function processModuleWithResolver (
155
+ { path, internal, context, excludeDefault, moduleSources, transform },
156
+ activeModules,
157
+ resolver
158
+ ) {
159
+ let moduleExports, srcUrl
151
160
  if (internal) {
152
161
  // we can not read and parse of internal modules
153
- exportNames = await getExportsImporting(path)
162
+ moduleExports = { exportNames: await getExportsImporting(path) }
154
163
  } else {
164
+ context = { ...context, format: getModuleFormat(path, context.format) }
155
165
  srcUrl = pathToFileURL(path)
156
- exportNames = await getExports(srcUrl, context, getSource)
166
+ const readSource = (url) => {
167
+ const modulePath = fileURLToPath(url)
168
+ let source = moduleSources.get(modulePath)
169
+ if (source === undefined) {
170
+ source = fs.readFileSync(modulePath, 'utf8')
171
+ moduleSources.set(modulePath, source)
172
+ }
173
+ return source
174
+ }
175
+ const loadSource = (url, { format }) => {
176
+ let source = readSource(url)
177
+ if (transform && (format === 'module-typescript' || format === 'commonjs-typescript')) {
178
+ source = transform(source, { loader: 'ts' }).code
179
+ format = format === 'module-typescript' ? 'module' : 'commonjs'
180
+ }
181
+ return { format, source }
182
+ }
183
+ readSource(srcUrl)
184
+ moduleExports = await getExports(srcUrl, context, loadSource, resolver)
157
185
  }
158
186
 
159
187
  const starExports = new Set()
@@ -186,43 +214,31 @@ async function processModule ({ path, internal = false, context, excludeDefault
186
214
  }
187
215
  }
188
216
 
189
- for (const n of exportNames) {
190
- if (n === 'default' && excludeDefault) continue
217
+ let starReexports = moduleExports.starReexports
218
+ for (const n of moduleExports.exportNames) {
219
+ const isDefault = n === 'default' || (
220
+ n === 'module.exports' &&
221
+ moduleExports.hasModuleExportsCJSDefault &&
222
+ (context.format === 'commonjs' || context.format === 'commonjs-typescript')
223
+ )
224
+ if (isDefault && excludeDefault) continue
191
225
 
192
226
  if (isStarExportLine(n)) {
193
- // export * from 'wherever'
194
- const [, modFile] = n.split('* from ')
195
-
196
- // Relative paths need to be resolved relative to the parent module
197
- const newSpecifier = isBareSpecifier(modFile) ? modFile : new URL(modFile, srcUrl).href
198
- // We need to call `parentResolve` to resolve bare specifiers to a full
199
- // URL. We also need to call `parentResolve` for all sub-modules to get
200
- // the `format`. We can't rely on the parents `format` to know if this
201
- // sub-module is ESM or CJS!
227
+ starReexports ??= []
228
+ starReexports.push({ parentURL: srcUrl.href, specifier: n.slice('* from '.length) })
229
+ continue
230
+ }
202
231
 
203
- const result = resolve(newSpecifier, { parentURL: srcUrl })
232
+ const variableName = `$dd${Buffer.from(n).toString('hex')}`
233
+ const objectKey = JSON.stringify(n)
234
+ const reExportedName = n === 'default' ? n : objectKey
204
235
 
205
- // eslint-disable-next-line no-await-in-loop
206
- const subSetters = await processModule({
207
- path: fileURLToPath(result.url),
208
- context: { ...context, format: result.format },
209
- excludeDefault: true,
210
- })
211
-
212
- for (const [name, setter] of subSetters.entries()) {
213
- addSetter(name, setter, true)
214
- }
215
- } else {
216
- const variableName = `$${n.replaceAll(/[^a-zA-Z0-9_$]/g, '_')}`
217
- const objectKey = JSON.stringify(n)
218
- const reExportedName = n === 'default' ? n : objectKey
219
-
220
- addSetter(n, `
236
+ addSetter(n, `
221
237
  let ${variableName}
222
238
  try {
223
239
  ${variableName} = _[${objectKey}] = namespace[${objectKey}]
224
- } catch (err) {
225
- if (!(err instanceof ReferenceError)) throw err
240
+ } catch (error) {
241
+ if (!(error instanceof ReferenceError)) throw error
226
242
  }
227
243
  export { ${variableName} as ${reExportedName} }
228
244
  set[${objectKey}] = (v) => {
@@ -231,6 +247,34 @@ async function processModule ({ path, internal = false, context, excludeDefault
231
247
  }
232
248
  get[${objectKey}] = () => ${variableName}
233
249
  `)
250
+ }
251
+
252
+ if (starReexports) {
253
+ for (const { parentURL, specifier } of starReexports) {
254
+ const baseUrl = new URL(parentURL)
255
+ const resolvedSpecifier = isBareSpecifier(specifier) ? specifier : new URL(specifier, baseUrl).href
256
+ // The runtime's import conditions and the declaring module's URL own star-export resolution.
257
+ // eslint-disable-next-line no-await-in-loop
258
+ const result = await resolveModule(resolvedSpecifier, { parentURL: baseUrl }, resolver)
259
+
260
+ activeModules ??= new Set([srcUrl.href])
261
+ if (activeModules.has(result.url.href)) continue
262
+ activeModules.add(result.url.href)
263
+
264
+ // eslint-disable-next-line no-await-in-loop
265
+ const subSetters = await processModuleWithResolver({
266
+ path: result.format === 'builtin' ? result.url.href : fileURLToPath(result.url),
267
+ internal: result.format === 'builtin',
268
+ context: { ...context, format: result.format },
269
+ excludeDefault: true,
270
+ moduleSources,
271
+ transform,
272
+ }, activeModules, resolver)
273
+ activeModules.delete(result.url.href)
274
+
275
+ for (const [name, setter] of subSetters.entries()) {
276
+ addSetter(name, setter, true)
277
+ }
234
278
  }
235
279
  }
236
280
 
@@ -246,8 +290,8 @@ async function processModule ({ path, internal = false, context, excludeDefault
246
290
  * @returns {boolean}
247
291
  */
248
292
  function isESMFile (fullPathToModule, modulePackageJsonPath, packageJson = {}) {
249
- if (fullPathToModule.endsWith('.mjs')) return true
250
- if (fullPathToModule.endsWith('.cjs')) return false
293
+ if (fullPathToModule.endsWith('.mjs') || fullPathToModule.endsWith('.mts')) return true
294
+ if (fullPathToModule.endsWith('.cjs') || fullPathToModule.endsWith('.cts')) return false
251
295
 
252
296
  const pathParts = fullPathToModule.split(path.sep)
253
297
  do {
@@ -271,6 +315,6 @@ function isESMFile (fullPathToModule, modulePackageJsonPath, packageJson = {}) {
271
315
  }
272
316
 
273
317
  module.exports = {
274
- processModule,
275
318
  isESMFile,
319
+ processModule,
276
320
  }
@@ -6,112 +6,48 @@ const { addHook, getHooks } = require('./helpers/instrument')
6
6
 
7
7
  const vercelAiTracingChannel = tracingChannel('dd-trace:vercel-ai')
8
8
  const vercelAiSpanSetAttributesChannel = channel('dd-trace:vercel-ai:span:setAttributes')
9
- const doGenerateBeforeChannel = channel('dd-trace:vercel-ai:doGenerate:before')
10
- const doGenerateAfterChannel = channel('dd-trace:vercel-ai:doGenerate:after')
11
- const doStreamBeforeChannel = channel('dd-trace:vercel-ai:doStream:before')
12
- const doStreamAfterChannel = channel('dd-trace:vercel-ai:doStream:after')
13
9
 
14
- const tracers = new WeakSet()
15
- const wrappedModels = new WeakSet()
10
+ // Published synchronously per model call with the native call data. A subscriber may set either
11
+ // callback and this instrumentation applies it, so the wrapping stays here and only the policy
12
+ // lives outside:
13
+ // beforeResult () => Promise<void>|undefined holds the result back until it settles
14
+ // onResult (result) => unknown|Promise<unknown> inspects or replaces the delivered result
15
+ const modelInterceptChannel = channel('dd-trace:vercel-ai:model:intercept')
16
16
 
17
- /**
18
- * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
19
- *
20
- * Subscribers push async work into `pending` synchronously during publication and
21
- * abort `abortController` with an error before the pushed promise resolves to block.
22
- *
23
- * @param {object} lifecycleChannel
24
- * @param {object} payload
25
- * @returns {Promise<void>}
26
- */
27
- function publishLifecycle (lifecycleChannel, payload) {
28
- const abortController = new AbortController()
29
- const ctx = { ...payload, abortController, pending: [] }
17
+ const INTERCEPTED_MODEL_METHODS = ['doGenerate', 'doStream']
30
18
 
31
- lifecycleChannel.publish(ctx)
32
-
33
- return Promise.all(ctx.pending).then(() => {
34
- if (abortController.signal.aborted) {
35
- throw abortController.signal.reason
36
- }
37
- })
38
- }
19
+ const tracers = new WeakSet()
20
+ const wrappedModels = new WeakSet()
39
21
 
40
22
  /**
41
- * Wraps a Vercel AI language model's doGenerate and doStream lifecycle methods.
23
+ * Wraps a language model's call methods so subscribers can interpose on each call.
42
24
  *
43
- * @param {object} model - A Vercel AI language model instance
25
+ * @param {object} model
44
26
  */
45
- function wrapModelWithLifecycle (model) {
27
+ function wrapModel (model) {
46
28
  if (!model || wrappedModels.has(model)) return
47
29
  wrappedModels.add(model)
48
30
 
49
- if (typeof model.doGenerate === 'function') {
50
- shimmer.wrap(model, 'doGenerate', function (original) {
51
- return function (options) {
52
- const originalResult = original.call(this, options)
31
+ for (const method of INTERCEPTED_MODEL_METHODS) {
32
+ if (typeof model[method] !== 'function') continue
53
33
 
54
- if (!doGenerateBeforeChannel.hasSubscribers && !doGenerateAfterChannel.hasSubscribers) return originalResult
55
- if (!options.prompt?.length) return originalResult
34
+ shimmer.wrap(model, method, original => function (...args) {
35
+ const result = original.apply(this, args)
36
+ if (!modelInterceptChannel.hasSubscribers) return result
56
37
 
57
- const beforeEvaluation = doGenerateBeforeChannel.hasSubscribers
58
- ? publishLifecycle(doGenerateBeforeChannel, { prompt: options.prompt, options })
59
- : Promise.resolve()
38
+ const interceptCtx = { method, arguments: args }
39
+ modelInterceptChannel.publish(interceptCtx)
60
40
 
61
- return Promise.all([beforeEvaluation, originalResult])
62
- .then(([, result]) => {
63
- if (!doGenerateAfterChannel.hasSubscribers || !result.content?.length) return result
64
- const payload = { prompt: options.prompt, options, result }
65
- return publishLifecycle(doGenerateAfterChannel, payload).then(() => result)
66
- })
67
- }
68
- })
69
- }
41
+ if (!interceptCtx.beforeResult && !interceptCtx.onResult) return result
70
42
 
71
- if (typeof model.doStream === 'function') {
72
- shimmer.wrap(model, 'doStream', function (original) {
73
- return function (options) {
74
- const originalResult = original.call(this, options)
75
-
76
- if (!doStreamBeforeChannel.hasSubscribers && !doStreamAfterChannel.hasSubscribers) return originalResult
77
- if (!options.prompt?.length) return originalResult
78
-
79
- const beforeEvaluation = doStreamBeforeChannel.hasSubscribers
80
- ? publishLifecycle(doStreamBeforeChannel, { prompt: options.prompt, options })
81
- : Promise.resolve()
82
-
83
- return Promise.all([beforeEvaluation, originalResult])
84
- .then(([, result]) => {
85
- if (!doStreamAfterChannel.hasSubscribers) return result
86
-
87
- const chunks = []
88
- const reader = result.stream.getReader()
89
-
90
- function readAll () {
91
- return reader.read().then(({ done, value }) => {
92
- if (done) return
93
- chunks.push(value)
94
- return readAll()
95
- })
96
- }
97
-
98
- return readAll().then(() => {
99
- return publishLifecycle(doStreamAfterChannel, { prompt: options.prompt, options, chunks })
100
- .then(() => {
101
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
102
- const stream = new ReadableStream({
103
- start (controller) {
104
- for (const chunk of chunks) {
105
- controller.enqueue(chunk)
106
- }
107
- controller.close()
108
- },
109
- })
110
- return { ...result, stream }
111
- })
112
- })
113
- })
114
- }
43
+ // A model may return any thenable, so normalize before attaching handlers.
44
+ const settled = Promise.resolve(result)
45
+ // The interceptor's rejection wins over an earlier SDK rejection.
46
+ settled.catch(() => {})
47
+
48
+ return Promise.resolve(interceptCtx.beforeResult?.())
49
+ .then(() => settled)
50
+ .then(value => (interceptCtx.onResult ? interceptCtx.onResult(value) : value))
115
51
  })
116
52
  }
117
53
  }
@@ -209,7 +145,9 @@ function wrapTracer (tracer) {
209
145
  })
210
146
  }
211
147
 
212
- for (const hook of getHooks('ai')) {
148
+ let orchestrionSubscribed = false
149
+
150
+ for (const hook of getHooks('ai').values()) {
213
151
  if (hook.file === 'dist/index.js') {
214
152
  // if not removed, the below hook will never match correctly
215
153
  // however, it is still needed in the orchestrion definition
@@ -217,6 +155,9 @@ for (const hook of getHooks('ai')) {
217
155
  }
218
156
 
219
157
  addHook(hook, exports => {
158
+ if (orchestrionSubscribed) return exports
159
+ orchestrionSubscribed = true
160
+
220
161
  const getTracerChannel = tracingChannel('orchestrion:ai:getTracer')
221
162
  getTracerChannel.subscribe({
222
163
  end (ctx) {
@@ -258,11 +199,14 @@ for (const hook of getHooks('ai')) {
258
199
  tracingChannel('orchestrion:ai:resolveLanguageModel').subscribe({
259
200
  end (ctx) {
260
201
  const model = ctx.arguments[0]
202
+
203
+ // The SDK builds a model from a string id, in which case only the resolved instance is
204
+ // worth wrapping; when the caller passed an instance, that is the one the SDK calls.
261
205
  if (typeof model !== 'string' && model !== ctx.result) {
262
- wrapModelWithLifecycle(model)
206
+ wrapModel(model)
263
207
  wrappedModels.add(ctx.result)
264
208
  } else {
265
- wrapModelWithLifecycle(ctx.result)
209
+ wrapModel(ctx.result)
266
210
  }
267
211
  },
268
212
  })
@@ -315,5 +259,3 @@ addHook({ name: 'ai', versions: ['>=7.0.0'] }, exports => {
315
259
 
316
260
  return exports
317
261
  })
318
-
319
- module.exports = { wrapModelWithLifecycle }