dd-trace 5.105.0 → 5.107.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 (108) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +5 -7
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -2
  7. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  8. package/packages/datadog-instrumentations/src/connect.js +3 -2
  9. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  10. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
  11. package/packages/datadog-instrumentations/src/cucumber.js +319 -152
  12. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  13. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  14. package/packages/datadog-instrumentations/src/express.js +24 -20
  15. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  16. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  18. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  19. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  20. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  21. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  22. package/packages/datadog-instrumentations/src/jest.js +143 -73
  23. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  25. package/packages/datadog-instrumentations/src/multer.js +3 -2
  26. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  27. package/packages/datadog-instrumentations/src/net.js +8 -6
  28. package/packages/datadog-instrumentations/src/openai.js +19 -7
  29. package/packages/datadog-instrumentations/src/pg.js +19 -0
  30. package/packages/datadog-instrumentations/src/router.js +12 -10
  31. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  32. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  33. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
  34. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
  35. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  36. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  37. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  38. package/packages/datadog-plugin-http/src/client.js +0 -3
  39. package/packages/datadog-plugin-http/src/server.js +11 -1
  40. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  41. package/packages/datadog-plugin-pg/src/index.js +10 -0
  42. package/packages/dd-trace/src/aiguard/index.js +34 -15
  43. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  44. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/id.js +15 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +91 -5
  68. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
  69. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  70. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  71. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  72. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  73. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  74. package/packages/dd-trace/src/llmobs/util.js +54 -0
  75. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  76. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  77. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  78. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  79. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  80. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  81. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  82. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +25 -5
  83. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -10
  84. package/packages/dd-trace/src/opentracing/span.js +23 -18
  85. package/packages/dd-trace/src/opentracing/span_context.js +1 -3
  86. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  87. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  88. package/packages/dd-trace/src/priority_sampler.js +6 -5
  89. package/packages/dd-trace/src/profiling/config.js +11 -25
  90. package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
  91. package/packages/dd-trace/src/profiling/profiler.js +19 -9
  92. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -3
  93. package/packages/dd-trace/src/proxy.js +13 -10
  94. package/packages/dd-trace/src/remote_config/index.js +1 -2
  95. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  96. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  97. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  98. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  99. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  100. package/packages/dd-trace/src/span_format.js +33 -25
  101. package/packages/dd-trace/src/span_stats.js +1 -1
  102. package/packages/dd-trace/src/startup-log.js +1 -2
  103. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +1 -1
  105. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  106. package/vendor/dist/shell-quote/index.js +1 -1
  107. package/packages/dd-trace/src/agent/url.js +0 -28
  108. package/scripts/preinstall.js +0 -34
@@ -206,6 +206,32 @@ function wrapConnection (Connection, version) {
206
206
  })
207
207
  }
208
208
  }
209
+ /**
210
+ * mysql2 defers a busy pool's `getConnection` callback and runs it in the releasing connection's
211
+ * async context; capture the caller's context and restore it around the callback so spans created
212
+ * by the queued query attach to the caller rather than the previous query that freed the connection.
213
+ *
214
+ * @param {Function} Pool
215
+ * @returns {Function}
216
+ */
217
+ function wrapGetConnection (Pool) {
218
+ const connectionStartCh = channel('apm:mysql2:connection:start')
219
+ const connectionFinishCh = channel('apm:mysql2:connection:finish')
220
+
221
+ shimmer.wrap(Pool.prototype, 'getConnection', getConnection => function (cb) {
222
+ const ctx = {}
223
+ arguments[0] = function (...args) {
224
+ return connectionFinishCh.runStores(ctx, cb, this, ...args)
225
+ }
226
+
227
+ connectionStartCh.publish(ctx)
228
+
229
+ return getConnection.apply(this, arguments)
230
+ })
231
+
232
+ return Pool
233
+ }
234
+
209
235
  /**
210
236
  * @param {Function} Pool
211
237
  * @param {string} version
@@ -215,6 +241,8 @@ function wrapPool (Pool, version) {
215
241
  const startOuterQueryCh = channel('datadog:mysql2:outerquery:start')
216
242
  const shouldEmitEndAfterQueryAbort = satisfies(version, '>=1.3.3')
217
243
 
244
+ wrapGetConnection(Pool)
245
+
218
246
  shimmer.wrap(Pool.prototype, 'query', query => function (sql, values, cb) {
219
247
  if (!startOuterQueryCh.hasSubscribers) return query.apply(this, arguments)
220
248
 
@@ -373,6 +401,12 @@ addHook(
373
401
  /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapPool)
374
402
  )
375
403
 
404
+ // mysql2 >=3.11.5 moved the pool onto BasePool in lib/base/pool.js.
405
+ addHook(
406
+ { name: 'mysql2', file: 'lib/base/pool.js', versions: ['>=3.11.5'] },
407
+ /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapGetConnection)
408
+ )
409
+
376
410
  // PoolNamespace.prototype.query does not exist in mysql2<2.3.0
377
411
  addHook(
378
412
  { name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['2.3.0 - 3.11.4'] },
@@ -49,20 +49,22 @@ addHook({ name: 'net' }, (net) => {
49
49
 
50
50
  const emit = this.emit
51
51
  let pendingReadyEvents = 2
52
- this.emit = shimmer.wrapFunction(emit, emit => function (eventName) {
52
+ // Named `emit`/arity-1 mirrors the socket method so the per-socket wrap
53
+ // skips its name/length rewrite.
54
+ this.emit = shimmer.wrapFunction(emit, originalEmit => function emit (eventName) {
53
55
  switch (eventName) {
54
56
  case 'ready':
55
57
  case 'connect':
56
- if (--pendingReadyEvents === 0) this.emit = emit
58
+ if (--pendingReadyEvents === 0) this.emit = originalEmit
57
59
  return readyCh.runStores(ctx, () => {
58
- return emit.apply(this, arguments)
60
+ return Reflect.apply(originalEmit, this, arguments)
59
61
  })
60
62
  case 'error':
61
63
  case 'close':
62
- this.emit = emit
63
- return emit.apply(this, arguments)
64
+ this.emit = originalEmit
65
+ return Reflect.apply(originalEmit, this, arguments)
64
66
  default:
65
- return emit.apply(this, arguments)
67
+ return Reflect.apply(originalEmit, this, arguments)
66
68
  }
67
69
  })
68
70
 
@@ -240,6 +240,10 @@ for (const extension of extensions) {
240
240
  }
241
241
 
242
242
  return ch.start.runStores(ctx, () => {
243
+ // Explicit childOf rather than async-context: the _thenUnwrap/parse path
244
+ // decouples the lazy evaluation from the active scope at call time.
245
+ if (guard) guard.parentSpan = ctx.currentStore?.span
246
+
243
247
  const apiProm = methodFn.apply(this, args)
244
248
 
245
249
  if (baseResource === 'chat.completions' && typeof apiProm._thenUnwrap === 'function') {
@@ -300,23 +304,31 @@ function handleUnwrappedAPIPromise (apiProm, ctx, stream, guard) {
300
304
  return body
301
305
  }
302
306
 
303
- finish(ctx, {
307
+ const responseData = {
304
308
  headers: response.headers,
305
309
  data: body,
306
310
  request: {
307
311
  path: response.url,
308
312
  method: options.method,
309
313
  },
310
- })
314
+ }
311
315
 
312
- if (!guard) return body
316
+ if (!guard) {
317
+ finish(ctx, responseData)
318
+ return body
319
+ }
313
320
 
314
- return aiGuard.evaluateOutput(guard, body).then(() => body)
321
+ // Finish after evaluation so a block propagates the error to openai.request
322
+ // and the span wraps its ai_guard child instead of closing before it.
323
+ return aiGuard.evaluateOutput(guard, body).then(() => {
324
+ finish(ctx, responseData)
325
+ return body
326
+ })
315
327
  })
316
328
  .catch(error => {
317
- // ctx.result is set inside finish(); if absent, finish never ran (sync throw in
318
- // success branch, before-model block, or openai error) — record the error now.
319
- // If finish already ran successfully (after-model block), don't double-publish.
329
+ // ctx.result is set inside finish(); if absent, finish never ran (sync throw in the success
330
+ // branch, Before Model block, After Model block, or openai error) — record the error now so
331
+ // the openai.request span is marked errored. If finish already ran, don't double-publish.
320
332
  if (!ctx.result) finish(ctx, undefined, error)
321
333
  throw error
322
334
  })
@@ -15,6 +15,9 @@ const errorCh = channel('apm:pg:query:error')
15
15
  const startPoolQueryCh = channel('datadog:pg:pool:query:start')
16
16
  const finishPoolQueryCh = channel('datadog:pg:pool:query:finish')
17
17
 
18
+ const poolConnectStartCh = channel('apm:pg:pool:connect:start')
19
+ const poolConnectFinishCh = channel('apm:pg:pool:connect:finish')
20
+
18
21
  // Drivers like pg-promise reuse the same prepared-statement query object across executions; cache
19
22
  // the un-injected `text` so the wrap doesn't capture a previous DBM injection as the new original.
20
23
  const originalTextCache = new WeakMap()
@@ -30,6 +33,22 @@ addHook({ name: 'pg', versions: ['>=8.0.3'], file: 'lib/client.js' }, Client =>
30
33
  })
31
34
 
32
35
  addHook({ name: 'pg', versions: ['>=8.0.3'] }, pg => {
36
+ // pg defers a busy pool's connect callback and runs it in the releasing query's async context;
37
+ // capture the caller's context and restore it around the callback so spans attach to the caller.
38
+ shimmer.wrap(pg.Pool.prototype, 'connect', connect => function (cb) {
39
+ if (typeof cb !== 'function' || !poolConnectStartCh.hasSubscribers) {
40
+ return connect.apply(this, arguments)
41
+ }
42
+
43
+ const ctx = {}
44
+ arguments[0] = function (...args) {
45
+ return poolConnectFinishCh.runStores(ctx, cb, this, ...args)
46
+ }
47
+
48
+ poolConnectStartCh.publish(ctx)
49
+
50
+ return connect.apply(this, arguments)
51
+ })
33
52
  shimmer.wrap(pg.Pool.prototype, 'query', query => wrapPoolQuery(query))
34
53
  return pg
35
54
  })
@@ -157,9 +157,9 @@ function createWrapRouterMethod (name, compile) {
157
157
  }
158
158
 
159
159
  function wrapNext (req, originalNext) {
160
- // Per layer dispatch, N per request. `shimmer.wrapCallback` preserves
161
- // only `name` + `length`; see its JSDoc for the full contract.
162
- return shimmer.wrapCallback(originalNext, next => function (error) {
160
+ // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
161
+ // router continuation so wrapCallback skips its name/length rewrite.
162
+ return shimmer.wrapCallback(originalNext, original => function next (error) {
163
163
  if (error && error !== 'route' && error !== 'router') {
164
164
  errorChannel.publish({ req, error })
165
165
  }
@@ -167,7 +167,7 @@ function createWrapRouterMethod (name, compile) {
167
167
  nextChannel.publish({ req })
168
168
  finishChannel.publish({ req })
169
169
 
170
- next.apply(this, arguments)
170
+ original.apply(this, arguments)
171
171
  })
172
172
  }
173
173
 
@@ -328,7 +328,8 @@ const routerParamStartCh = channel('datadog:router:param:start')
328
328
  const visitedParams = new WeakSet()
329
329
 
330
330
  function wrapHandleRequest (original) {
331
- return function wrappedHandleRequest (req, res, next) {
331
+ return function wrappedHandleRequest (...args) {
332
+ const req = args[0]
332
333
  if (routerParamStartCh.hasSubscribers && !visitedParams.has(req.params) && Object.keys(req.params).length) {
333
334
  visitedParams.add(req.params)
334
335
 
@@ -336,7 +337,7 @@ function wrapHandleRequest (original) {
336
337
 
337
338
  routerParamStartCh.publish({
338
339
  req,
339
- res,
340
+ res: args[1],
340
341
  params: req?.params,
341
342
  abortController,
342
343
  })
@@ -344,7 +345,7 @@ function wrapHandleRequest (original) {
344
345
  if (abortController.signal.aborted) return
345
346
  }
346
347
 
347
- return original.apply(this, arguments)
348
+ return Reflect.apply(original, this, args)
348
349
  }
349
350
  }
350
351
 
@@ -358,7 +359,8 @@ addHook({
358
359
  function wrapParam (original) {
359
360
  return function wrappedProcessParams (...args) {
360
361
  args[1] = shimmer.wrapFunction(args[1], (originalFn) => {
361
- return function wrappedFn (req, res) {
362
+ return function wrappedFn (...fnArgs) {
363
+ const req = fnArgs[0]
362
364
  if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) {
363
365
  visitedParams.add(req.params)
364
366
 
@@ -366,7 +368,7 @@ function wrapParam (original) {
366
368
 
367
369
  routerParamStartCh.publish({
368
370
  req,
369
- res,
371
+ res: fnArgs[1],
370
372
  params: req?.params,
371
373
  abortController,
372
374
  })
@@ -374,7 +376,7 @@ function wrapParam (original) {
374
376
  if (abortController.signal.aborted) return
375
377
  }
376
378
 
377
- return originalFn.apply(this, arguments)
379
+ return Reflect.apply(originalFn, this, fnArgs)
378
380
  }
379
381
  })
380
382
 
@@ -57,6 +57,7 @@ const codeCoverageReportCh = channel('ci:vitest:coverage-report')
57
57
 
58
58
  const taskToCtx = new WeakMap()
59
59
  const taskToStatuses = new WeakMap()
60
+ const taskToReportedErrorCount = new WeakMap()
60
61
  const attemptToFixTaskToStatuses = new WeakMap()
61
62
  const originalHookFns = new WeakMap()
62
63
  const newTasks = new WeakSet()
@@ -316,6 +317,27 @@ function recordFinalAttemptToFixExecution (task, status, providedContext) {
316
317
  })
317
318
  }
318
319
 
320
+ function disableFrameworkRetries (task) {
321
+ task.retry = 0
322
+ }
323
+
324
+ /**
325
+ * Vitest accumulates retry and repeat errors on one task result. The first error added since
326
+ * the last reported attempt is the primary error for the failed attempt currently being reported.
327
+ *
328
+ * @param {object} task
329
+ * @param {Array<object> | undefined} errors
330
+ * @returns {object | undefined}
331
+ */
332
+ function getCurrentAttemptTestError (task, errors) {
333
+ if (!errors?.length) return
334
+
335
+ const previousErrorCount = taskToReportedErrorCount.get(task) ?? 0
336
+ const testError = errors[previousErrorCount] ?? errors[0]
337
+ taskToReportedErrorCount.set(task, errors.length)
338
+ return testError
339
+ }
340
+
319
341
  /**
320
342
  * Wraps a function so it runs inside the current test span context.
321
343
  * @param {object} task
@@ -801,6 +823,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
801
823
  onDone: (isAttemptToFix) => {
802
824
  if (isAttemptToFix) {
803
825
  isRetryReasonAttemptToFix = task.repeats !== testManagementAttemptToFixRetries
826
+ disableFrameworkRetries(task)
804
827
  task.repeats = testManagementAttemptToFixRetries
805
828
  attemptToFixTasks.add(task)
806
829
  attemptToFixTaskToStatuses.set(task, [])
@@ -831,6 +854,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
831
854
  if (isImpacted) {
832
855
  if (isEarlyFlakeDetectionEnabled) {
833
856
  isRetryReasonEfd = true
857
+ disableFrameworkRetries(task)
834
858
  task.repeats = numRepeats
835
859
  }
836
860
  modifiedTasks.add(task)
@@ -849,6 +873,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
849
873
  if (isNew && !attemptToFixTasks.has(task)) {
850
874
  if (isEarlyFlakeDetectionEnabled && !modifiedTasks.has(task)) {
851
875
  isRetryReasonEfd = true
876
+ disableFrameworkRetries(task)
852
877
  task.repeats = numRepeats
853
878
  }
854
879
  newTasks.add(task)
@@ -986,7 +1011,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
986
1011
  const promises = {}
987
1012
  const shouldSetProbe = isDiEnabled && numAttempt === 1
988
1013
  const ctx = taskToCtx.get(task)
989
- const testError = task.result?.errors?.[0]
1014
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
990
1015
  if (ctx) {
991
1016
  testErrorCh.publish({
992
1017
  error: testError,
@@ -1016,7 +1041,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
1016
1041
  const ctx = taskToCtx.get(task)
1017
1042
  if (ctx) {
1018
1043
  if (lastExecutionStatus === 'fail') {
1019
- const testError = task.result?.errors?.[0]
1044
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
1020
1045
  testErrorCh.publish({ error: testError, ...ctx.currentStore })
1021
1046
  } else {
1022
1047
  testPassCh.publish({ task, ...ctx.currentStore })
@@ -1040,7 +1065,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
1040
1065
 
1041
1066
  const ctx = taskToCtx.get(task)
1042
1067
  if (lastExecutionStatus === 'fail') {
1043
- const testError = task.result?.errors?.[0]
1068
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
1044
1069
  testErrorCh.publish({ error: testError, ...ctx.currentStore })
1045
1070
  } else {
1046
1071
  testPassCh.publish({ task, ...ctx.currentStore })
@@ -1389,7 +1414,7 @@ addHook({
1389
1414
 
1390
1415
  if (result) {
1391
1416
  const { state, duration, errors } = result
1392
- const testError = errors?.[0]
1417
+ const testError = getCurrentAttemptTestError(task, errors)
1393
1418
  if (attemptToFixTasks.has(task)) {
1394
1419
  const status = getFinalAttemptToFixStatus(task, state, isSwitchedStatus, testCtx)
1395
1420
  recordFinalAttemptToFixExecution(task, status, providedContext)
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
4
3
  const ClientPlugin = require('../../dd-trace/src/plugins/client')
5
4
  const { storage } = require('../../datadog-core')
6
5
  const { tagsFromRequest, tagsFromResponse } = require('../../dd-trace/src/payload-tagging')
@@ -113,8 +112,6 @@ class BaseAwsSdkPlugin extends ClientPlugin {
113
112
  integrationName: 'aws-sdk',
114
113
  }, ctx)
115
114
 
116
- analyticsSampler.sample(span, this.config.measured)
117
-
118
115
  storage('legacy').run(ctx.currentStore, () => {
119
116
  this.requestInject(span, request)
120
117
  })
@@ -3,7 +3,7 @@
3
3
  const BaseAwsSdkPlugin = require('../../base')
4
4
  const { parseModelId } = require('./utils')
5
5
 
6
- const enabledOperations = new Set(['invokeModel', 'invokeModelWithResponseStream'])
6
+ const enabledOperations = new Set(['invokeModel', 'invokeModelWithResponseStream', 'converse', 'converseStream'])
7
7
 
8
8
  class BedrockRuntime extends BaseAwsSdkPlugin {
9
9
  static id = 'bedrockruntime'
@@ -131,6 +131,7 @@ class Generation {
131
131
  outputTokens,
132
132
  cacheReadTokens,
133
133
  cacheWriteTokens,
134
+ messages,
134
135
  } = {}) {
135
136
  // stringify message as it could be a single generated message as well as a list of embeddings
136
137
  this.message = typeof message === 'string' ? message : JSON.stringify(message) || ''
@@ -143,6 +144,7 @@ class Generation {
143
144
  cacheReadTokens,
144
145
  cacheWriteTokens,
145
146
  }
147
+ this.messages = messages ?? [{ content: this.message, role: this.role }]
146
148
  }
147
149
  }
148
150
 
@@ -401,10 +403,7 @@ function extractTextAndResponseReason (response, provider, modelName) {
401
403
  message: output.message?.content[0]?.text ?? 'Unsupported content type',
402
404
  finishReason: body.stopReason,
403
405
  role: output.message?.role,
404
- inputTokens: body.usage?.inputTokens,
405
- outputTokens: body.usage?.outputTokens,
406
- cacheReadInputTokenCount: body.usage?.cacheReadInputTokenCount,
407
- cacheWriteInputTokenCount: body.usage?.cacheWriteInputTokenCount,
406
+ ...buildUsage(body.usage),
408
407
  })
409
408
  }
410
409
  break
@@ -476,6 +475,216 @@ function extractTextAndResponseReason (response, provider, modelName) {
476
475
  return new Generation()
477
476
  }
478
477
 
478
+ /**
479
+ * Convert a Converse content-block array to an LLMObs message array.
480
+ *
481
+ * @param {string} role
482
+ * @param {Array<object>} contentBlocks
483
+ * @returns {{ content?: string, role: string, toolCalls?: Array, toolResults?: Array } | undefined}
484
+ */
485
+ function extractMessagesFromConverseContent (role, contentBlocks) {
486
+ let content = ''
487
+ const toolCalls = []
488
+ const toolResults = []
489
+
490
+ for (const block of contentBlocks || []) {
491
+ if (block == null || typeof block !== 'object') continue
492
+ if (typeof block.text === 'string') {
493
+ content += block.text
494
+ } else if (block.toolUse) {
495
+ toolCalls.push(buildToolCall(block.toolUse))
496
+ } else if (block.toolResult) {
497
+ toolResults.push(buildToolResult(block.toolResult))
498
+ } else {
499
+ content += `[Unsupported content type: ${getContentBlockType(block)}]`
500
+ }
501
+ }
502
+
503
+ if (!content && toolCalls.length === 0 && toolResults.length === 0) return
504
+
505
+ const message = { role }
506
+ if (content) message.content = content
507
+ if (toolCalls.length > 0) message.toolCalls = toolCalls
508
+ if (toolResults.length > 0) message.toolResults = toolResults
509
+ return message
510
+ }
511
+
512
+ /**
513
+ * Resolve a Converse `ContentBlock`'s member type. The block is a key-presence
514
+ * tagged union (no `type` discriminator), so the active member is its sole own
515
+ * key. For forward-compat `$unknown` members the real type is the first element
516
+ * of the `[name, value]` tuple.
517
+ *
518
+ * @param {object} block
519
+ * @returns {string}
520
+ */
521
+ function getContentBlockType (block) {
522
+ const key = Object.keys(block)[0]
523
+ if (key === '$unknown') return block.$unknown?.[0] ?? 'unknown'
524
+ return key ?? 'unknown'
525
+ }
526
+
527
+ // Always emit at least one output message so downstream tagging has a role to attach to.
528
+ function toOutputMessages (role, contentBlocks) {
529
+ const message = extractMessagesFromConverseContent(role, contentBlocks)
530
+ return message ? [message] : [{ role, content: '' }]
531
+ }
532
+
533
+ function buildToolCall ({ name, input, toolUseId }) {
534
+ return { name: name ?? '', arguments: input ?? {}, toolId: toolUseId ?? '', type: 'toolUse' }
535
+ }
536
+
537
+ function parseToolInput (inputStr) {
538
+ try {
539
+ return JSON.parse(inputStr)
540
+ } catch {
541
+ log.warn('Failed to parse Converse stream toolUse.input JSON; emitting empty arguments')
542
+ return {}
543
+ }
544
+ }
545
+
546
+ function buildToolResult ({ toolUseId, content }) {
547
+ const result = (content || []).map(resolveToolResultItem).join('')
548
+ return { name: '', result, toolId: toolUseId ?? '', type: 'tool_result' }
549
+ }
550
+
551
+ function resolveToolResultItem (item) {
552
+ if (typeof item.text === 'string') return item.text
553
+ if (item.json != null) return JSON.stringify(item.json)
554
+ return `[Unsupported content type(s): ${getContentBlockType(item)}]`
555
+ }
556
+
557
+ function buildUsage (usage = {}) {
558
+ return {
559
+ inputTokens: usage.inputTokens,
560
+ outputTokens: usage.outputTokens,
561
+ cacheReadTokens: usage.cacheReadInputTokens ?? usage.cacheReadInputTokenCount,
562
+ cacheWriteTokens: usage.cacheWriteInputTokens ?? usage.cacheWriteInputTokenCount,
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Extract tool definitions from a Converse request's `toolConfig.tools`,
568
+ * mapping Bedrock's `toolSpec` shape to LLMObs `ToolDefinition` shape.
569
+ *
570
+ * @param {object} params - Converse request params with optional `toolConfig.tools[].toolSpec`.
571
+ * @returns {Array<{ name: string, description: string, schema: object }>}
572
+ */
573
+ function extractConverseToolDefinitions (params) {
574
+ const toolDefinitions = []
575
+ for (const tool of params.toolConfig?.tools || []) {
576
+ const toolSpec = tool?.toolSpec
577
+ if (!toolSpec?.name) continue
578
+ toolDefinitions.push({
579
+ name: toolSpec.name,
580
+ description: toolSpec.description ?? '',
581
+ schema: toolSpec.inputSchema ?? {},
582
+ })
583
+ }
584
+ return toolDefinitions
585
+ }
586
+
587
+ /**
588
+ * Extract request metadata + rendered input messages from a Converse /
589
+ * ConverseStream request.
590
+ *
591
+ * @param {{ modelId?: string, messages?: Array, system?: Array, inferenceConfig?: object, toolConfig?: object }} params
592
+ * @returns {RequestParams}
593
+ */
594
+ function extractRequestParamsConverse (params) {
595
+ const prompt = []
596
+ for (const block of params.system || []) {
597
+ if (typeof block?.text === 'string') prompt.push({ content: block.text, role: 'system' })
598
+ }
599
+ for (const msg of params.messages || []) {
600
+ if (msg == null || typeof msg !== 'object') continue
601
+ const message = extractMessagesFromConverseContent(msg.role || 'user', msg.content)
602
+ if (message) prompt.push(message)
603
+ }
604
+
605
+ const { temperature, topP, maxTokens, stopSequences } = params.inferenceConfig || {}
606
+ return new RequestParams({ prompt, temperature, topP, maxTokens, stopSequences })
607
+ }
608
+
609
+ /**
610
+ * Extract output messages + usage from a non-stream Converse response.
611
+ *
612
+ * @param {{ output?: { message?: { role?: string, content?: Array } }, stopReason?: string, usage?: object }} response
613
+ * @returns {Generation}
614
+ */
615
+ function extractTextAndResponseReasonConverse (response) {
616
+ const outputMessage = response?.output?.message
617
+ const role = outputMessage?.role || 'assistant'
618
+
619
+ return new Generation({
620
+ role,
621
+ finishReason: response?.stopReason || '',
622
+ ...buildUsage(response?.usage),
623
+ messages: toOutputMessages(role, outputMessage?.content),
624
+ })
625
+ }
626
+
627
+ /**
628
+ * Aggregate Converse stream events into a single output message + usage.
629
+ * One messageStart / messageStop pair per response, so one message out.
630
+ *
631
+ * Stream events describe the same content-block structure as the non-stream
632
+ * response, spread across start/delta chunks. We reassemble those chunks
633
+ * into a normalized content-block array and reuse the non-stream extractor.
634
+ *
635
+ * @param {Array<object>} chunks - Ordered ConverseStreamOutput events.
636
+ * @returns {Generation}
637
+ */
638
+ function extractTextAndResponseReasonConverseFromStream (chunks) {
639
+ let role = 'assistant'
640
+ let stopReason = ''
641
+ let usage = {}
642
+ const blocksByIdx = new Map()
643
+
644
+ for (const chunk of chunks || []) {
645
+ if (chunk.messageStart?.role) {
646
+ role = chunk.messageStart.role
647
+ } else if (chunk.messageStop?.stopReason) {
648
+ stopReason = chunk.messageStop.stopReason
649
+ } else if (chunk.metadata?.usage) {
650
+ usage = chunk.metadata.usage
651
+ } else if (chunk.contentBlockStart?.start?.toolUse) {
652
+ const { contentBlockIndex, start: { toolUse } } = chunk.contentBlockStart
653
+ blocksByIdx.set(contentBlockIndex, {
654
+ toolUse: { toolUseId: toolUse.toolUseId, name: toolUse.name, inputStr: '' },
655
+ })
656
+ } else if (chunk.contentBlockDelta) {
657
+ const { contentBlockIndex, delta } = chunk.contentBlockDelta
658
+ if (typeof delta?.text === 'string') {
659
+ const block = blocksByIdx.get(contentBlockIndex) ?? {}
660
+ block.text = (block.text ?? '') + delta.text
661
+ blocksByIdx.set(contentBlockIndex, block)
662
+ } else if (typeof delta?.toolUse?.input === 'string') {
663
+ const block = blocksByIdx.get(contentBlockIndex) ?? { toolUse: { inputStr: '' } }
664
+ block.toolUse ??= { inputStr: '' }
665
+ block.toolUse.inputStr += delta.toolUse.input
666
+ blocksByIdx.set(contentBlockIndex, block)
667
+ }
668
+ }
669
+ }
670
+
671
+ const contentBlocks = [...blocksByIdx.keys()].sort((a, b) => a - b).map(i => {
672
+ const block = blocksByIdx.get(i)
673
+ if (block.toolUse) {
674
+ const { toolUseId, name, inputStr } = block.toolUse
675
+ block.toolUse = { toolUseId, name, input: parseToolInput(inputStr) }
676
+ }
677
+ return block
678
+ })
679
+
680
+ return new Generation({
681
+ role,
682
+ finishReason: stopReason,
683
+ ...buildUsage(usage),
684
+ messages: toOutputMessages(role, contentBlocks),
685
+ })
686
+ }
687
+
479
688
  module.exports = {
480
689
  Generation,
481
690
  RequestParams,
@@ -483,5 +692,10 @@ module.exports = {
483
692
  parseModelId,
484
693
  extractRequestParams,
485
694
  extractTextAndResponseReason,
695
+ extractMessagesFromConverseContent,
696
+ extractConverseToolDefinitions,
697
+ extractRequestParamsConverse,
698
+ extractTextAndResponseReasonConverse,
699
+ extractTextAndResponseReasonConverseFromStream,
486
700
  PROVIDER,
487
701
  }