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
@@ -1,8 +1,5 @@
1
1
  'use strict'
2
2
 
3
- // Capture real timers at module load time, before any test can install fake timers.
4
- const realSetTimeout = setTimeout
5
-
6
3
  const { performance } = require('node:perf_hooks')
7
4
 
8
5
  const shimmer = require('../../datadog-shimmer')
@@ -18,6 +15,7 @@ const {
18
15
  testFinishTimeCh,
19
16
  testPassCh,
20
17
  testErrorCh,
18
+ testDiWaitCh,
21
19
  testSkipCh,
22
20
  testFnCh,
23
21
  testSuiteStartCh,
@@ -32,39 +30,36 @@ const {
32
30
  getVitestTestProperties,
33
31
  } = require('./vitest-util')
34
32
 
35
- const BREAKPOINT_HIT_GRACE_PERIOD_MS = 400
36
-
37
33
  const taskToCtx = new WeakMap()
38
34
  const taskToTestProperties = new WeakMap()
39
35
  const taskToStatuses = new WeakMap()
40
36
  const taskToReportedErrorCount = new WeakMap()
41
37
  const attemptToFixTaskToStatuses = new WeakMap()
38
+ const fileToHasConcurrentTests = new WeakMap()
42
39
  const originalHookFns = new WeakMap()
43
40
  const newTasks = new WeakSet()
44
41
  const dynamicNameTasks = new WeakSet()
45
42
  const disabledTasks = new WeakSet()
46
43
  const quarantinedTasks = new WeakSet()
47
44
  const attemptToFixTasks = new WeakSet()
45
+ const attemptToFixRetryTasks = new WeakSet()
48
46
  const modifiedTasks = new WeakSet()
47
+ const efdRetryTasks = new WeakSet()
49
48
  const efdDeterminedRetries = new WeakMap()
50
49
  const efdSlowAbortedTasks = new WeakSet()
51
50
  const efdExecutionStartByTask = new WeakMap()
52
51
  const efdSkippedRetryResults = new WeakMap()
53
52
  const attemptToFixExecutions = new Map()
54
53
  const loggedAttemptToFixTests = new Set()
55
- let isRetryReasonEfd = false
56
- let isRetryReasonAttemptToFix = false
57
54
  const switchedStatuses = new WeakSet()
58
55
  let vitestGetFn = null
59
56
  let vitestSetFn = null
60
57
  let vitestGetHooks = null
61
58
 
62
59
  function waitForHitProbe () {
63
- return new Promise(resolve => {
64
- realSetTimeout(() => {
65
- resolve()
66
- }, BREAKPOINT_HIT_GRACE_PERIOD_MS)
67
- })
60
+ const promises = {}
61
+ testDiWaitCh.publish({ promises })
62
+ return promises.hitBreakpointPromise
68
63
  }
69
64
 
70
65
  function getVitestTestStatus (test, retryCount) {
@@ -138,6 +133,70 @@ function wrapBeforeEachCleanupResult (task, result) {
138
133
  return result
139
134
  }
140
135
 
136
+ /**
137
+ * Returns whether a Vitest task tree includes any concurrent task.
138
+ *
139
+ * @param {Array<{ type?: string, concurrent?: boolean, tasks?: object[] }>|undefined} tasks
140
+ * @returns {boolean}
141
+ */
142
+ function hasConcurrentTask (tasks) {
143
+ if (!tasks) return false
144
+
145
+ for (const task of tasks) {
146
+ if (task.concurrent === true) return true
147
+ if (hasConcurrentTask(task.tasks)) return true
148
+ }
149
+
150
+ return false
151
+ }
152
+
153
+ /**
154
+ * Returns whether a Vitest file includes any concurrent test.
155
+ *
156
+ * @param {{ tasks?: object[] }} file
157
+ * @returns {boolean}
158
+ */
159
+ function hasConcurrentTests (file) {
160
+ const cached = fileToHasConcurrentTests.get(file)
161
+ if (cached !== undefined) return cached
162
+
163
+ const hasConcurrent = hasConcurrentTask(file.tasks)
164
+ fileToHasConcurrentTests.set(file, hasConcurrent)
165
+ return hasConcurrent
166
+ }
167
+
168
+ /**
169
+ * Gets the task associated with a Vitest hook invocation.
170
+ *
171
+ * @param {unknown[]} args
172
+ * @param {object} fallbackTask
173
+ * @returns {object}
174
+ */
175
+ function getTaskFromHookArgs (args, fallbackTask) {
176
+ return args[0]?.task || fallbackTask
177
+ }
178
+
179
+ /**
180
+ * Wraps a Vitest hook so it runs inside the span context for the current test.
181
+ *
182
+ * @param {'beforeEach'|'afterEach'} hookType
183
+ * @param {Function} fn
184
+ * @param {object} fallbackTask
185
+ * @returns {Function}
186
+ */
187
+ function wrapSuiteHookFn (hookType, fn, fallbackTask) {
188
+ return shimmer.wrapFunction(fn, fn => function (...args) {
189
+ const task = getTaskFromHookArgs(args, fallbackTask)
190
+ const result = testFnCh.runStores(taskToCtx.get(task), () => fn.apply(this, args))
191
+
192
+ if (hookType === 'beforeEach') {
193
+ return wrapBeforeEachCleanupResult(task, result)
194
+ }
195
+
196
+ return result
197
+ })
198
+ }
199
+
141
200
  function wrapVitestTestRunner (VitestTestRunner) {
142
201
  // `onBeforeRunTask` is run before any repetition or attempt is run
143
202
  // `onBeforeRunTask` is an async function
@@ -163,7 +222,9 @@ function wrapVitestTestRunner (VitestTestRunner) {
163
222
  isQuarantined,
164
223
  } = testProperties
165
224
  if (isAttemptToFix) {
166
- isRetryReasonAttemptToFix = task.repeats !== testManagementAttemptToFixRetries
225
+ if (task.repeats !== testManagementAttemptToFixRetries) {
226
+ attemptToFixRetryTasks.add(task)
227
+ }
167
228
  disableFrameworkRetries(task)
168
229
  task.repeats = testManagementAttemptToFixRetries
169
230
  attemptToFixTasks.add(task)
@@ -183,7 +244,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
183
244
 
184
245
  if (isImpactedTestsEnabled && testProperties.isModified) {
185
246
  if (isEarlyFlakeDetectionEnabled) {
186
- isRetryReasonEfd = true
247
+ efdRetryTasks.add(task)
187
248
  disableFrameworkRetries(task)
188
249
  task.repeats = numRepeats
189
250
  }
@@ -193,7 +254,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
193
254
 
194
255
  if (isKnownTestsEnabled && testProperties.isNew && !attemptToFixTasks.has(task)) {
195
256
  if (isEarlyFlakeDetectionEnabled && !modifiedTasks.has(task)) {
196
- isRetryReasonEfd = true
257
+ efdRetryTasks.add(task)
197
258
  disableFrameworkRetries(task)
198
259
  task.repeats = numRepeats
199
260
  }
@@ -267,6 +328,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
267
328
  }
268
329
 
269
330
  const { retry: numAttempt, repeats: numRepetition } = retryInfo
331
+ const isFailedTestReplayAllowed = !hasConcurrentTests(task.file)
270
332
  const isEfdManagedTask = isEarlyFlakeDetectionEnabled && taskToStatuses.has(task) && !attemptToFixTasks.has(task)
271
333
 
272
334
  if (isEfdManagedTask && numRepetition > 0 && !efdDeterminedRetries.has(task)) {
@@ -303,19 +365,20 @@ function wrapVitestTestRunner (VitestTestRunner) {
303
365
 
304
366
  // We finish the previous test here because we know it has failed already
305
367
  if (numAttempt > 0) {
306
- const shouldWaitForHitProbe = isDiEnabled && numAttempt > 1
368
+ const shouldWaitForHitProbe = isDiEnabled && isFailedTestReplayAllowed && numAttempt > 1
307
369
  if (shouldWaitForHitProbe) {
308
370
  await waitForHitProbe()
309
371
  }
310
372
 
311
373
  const promises = {}
312
- const shouldSetProbe = isDiEnabled && numAttempt === 1
374
+ const shouldSetProbe = isDiEnabled && isFailedTestReplayAllowed && numAttempt === 1
313
375
  const ctx = taskToCtx.get(task)
314
376
  const testError = getCurrentAttemptTestError(task, task.result?.errors)
315
377
  if (ctx) {
316
378
  testErrorCh.publish({
317
379
  error: testError,
318
380
  shouldSetProbe,
381
+ shouldWaitForHitProbe,
319
382
  promises,
320
383
  ...ctx.currentStore,
321
384
  })
@@ -377,18 +440,18 @@ function wrapVitestTestRunner (VitestTestRunner) {
377
440
 
378
441
  const isRetryReasonAtr = numAttempt > 0 &&
379
442
  isFlakyTestRetriesEnabledForTask(providedContext, task) &&
380
- !isRetryReasonAttemptToFix &&
381
- !isRetryReasonEfd
443
+ !attemptToFixRetryTasks.has(task) &&
444
+ !efdRetryTasks.has(task)
382
445
 
383
446
  const ctx = {
384
447
  testName,
385
448
  testSuiteAbsolutePath: task.file.filepath,
386
449
  isRetry: numAttempt > 0 || numRepetition > 0,
387
- isRetryReasonEfd,
388
- isRetryReasonAttemptToFix: isRetryReasonAttemptToFix && numRepetition > 0,
450
+ isRetryReasonEfd: efdRetryTasks.has(task),
451
+ isRetryReasonAttemptToFix: attemptToFixRetryTasks.has(task) && numRepetition > 0,
389
452
  isNew,
390
453
  hasDynamicName: dynamicNameTasks.has(task),
391
- mightHitProbe: isDiEnabled && numAttempt > 0,
454
+ mightHitProbe: isDiEnabled && isFailedTestReplayAllowed && numAttempt > 0,
392
455
  isAttemptToFix: attemptToFixTasks.has(task),
393
456
  isDisabled: disabledTasks.has(task),
394
457
  isQuarantined: quarantinedTasks.has(task),
@@ -429,17 +492,9 @@ function wrapVitestTestRunner (VitestTestRunner) {
429
492
  if (!hookArray) continue
430
493
  for (let i = 0; i < hookArray.length; i++) {
431
494
  const currentFn = hookArray[i]
432
- const originalFn = originalHookFns.get(currentFn) || currentFn
433
- const wrappedFn = shimmer.wrapFunction(originalFn, fn => function (...args) {
434
- const result = testFnCh.runStores(taskToCtx.get(task), () => fn.apply(this, args))
435
-
436
- if (hookType === 'beforeEach') {
437
- return wrapBeforeEachCleanupResult(task, result)
438
- }
439
-
440
- return result
441
- })
442
- originalHookFns.set(wrappedFn, originalFn)
495
+ if (originalHookFns.has(currentFn)) continue
496
+ const wrappedFn = wrapSuiteHookFn(hookType, currentFn, task)
497
+ originalHookFns.set(wrappedFn, currentFn)
443
498
  hookArray[i] = wrappedFn
444
499
  }
445
500
  }
@@ -468,6 +523,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
468
523
  const ctx = taskToCtx.get(task)
469
524
 
470
525
  const { isDiEnabled } = getProvidedContext()
526
+ const isFailedTestReplayAllowed = !hasConcurrentTests(task.file)
471
527
 
472
528
  if (efdSkippedRetryResults.has(task)) {
473
529
  task.result = efdSkippedRetryResults.get(task)
@@ -475,7 +531,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
475
531
  return result
476
532
  }
477
533
 
478
- if (isDiEnabled && retryInfo.retry > 1) {
534
+ if (isDiEnabled && isFailedTestReplayAllowed && retryInfo.retry > 1) {
479
535
  await waitForHitProbe()
480
536
  }
481
537
 
@@ -601,6 +657,7 @@ addHook({
601
657
  testCommand: providedContext.testCommand,
602
658
  repositoryRoot: providedContext.repositoryRoot,
603
659
  codeOwnersEntries: providedContext.codeOwnersEntries,
660
+ testEnvironmentMetadata: providedContext.testEnvironmentMetadata,
604
661
  }
605
662
  testSuiteStartCh.runStores(testSuiteCtx, () => {})
606
663
  const startTestsResponse = await startTests.apply(this, arguments)
@@ -611,6 +668,7 @@ addHook({
611
668
  })
612
669
 
613
670
  const testTasks = getTypeTasks(startTestsResponse[0].tasks)
671
+ const testEventPromises = []
614
672
 
615
673
  // Only one test task per test, even if there are retries
616
674
  for (const task of testTasks) {
@@ -639,12 +697,17 @@ addHook({
639
697
  if (testCtx) {
640
698
  const isSkippedByTestManagement =
641
699
  !attemptToFixTasks.has(task) && (disabledTasks.has(task) || quarantinedTasks.has(task))
700
+ const promises = {}
642
701
  testPassCh.publish({
643
702
  task,
644
703
  finalStatus: isSkippedByTestManagement ? 'skip' : 'pass',
645
704
  earlyFlakeAbortReason: efdSlowAbortedTasks.has(task) ? 'slow' : undefined,
705
+ promises,
646
706
  ...testCtx.currentStore,
647
707
  })
708
+ if (promises.hitBreakpointPromise) {
709
+ testEventPromises.push(promises.hitBreakpointPromise)
710
+ }
648
711
  }
649
712
  } else if (state === 'fail' || isSwitchedStatus) {
650
713
  let hasFailedAllRetries = false
@@ -684,6 +747,7 @@ addHook({
684
747
 
685
748
  if (testCtx) {
686
749
  const isRetry = task.result?.retryCount > 0
750
+ const promises = {}
687
751
  // `duration` is the duration of all the retries, so it can't be used if there are retries
688
752
 
689
753
  let finalStatus
@@ -708,8 +772,12 @@ addHook({
708
772
  attemptToFixFailed,
709
773
  finalStatus,
710
774
  earlyFlakeAbortReason: efdSlowAbortedTasks.has(task) ? 'slow' : undefined,
775
+ promises,
711
776
  ...testCtx.currentStore,
712
777
  })
778
+ if (promises.hitBreakpointPromise) {
779
+ testEventPromises.push(promises.hitBreakpointPromise)
780
+ }
713
781
  }
714
782
  if (errors?.length) {
715
783
  testSuiteError = testError // we store the error to bubble it up to the suite
@@ -725,6 +793,8 @@ addHook({
725
793
  }
726
794
  }
727
795
 
796
+ await Promise.all(testEventPromises)
797
+
728
798
  const testSuiteResult = startTestsResponse[0].result
729
799
 
730
800
  if (testSuiteResult.errors?.length) { // Errors from root level hooks
@@ -5,7 +5,7 @@ const VercelAILLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/ai')
5
5
  const VercelAITracingPlugin = require('./tracing')
6
6
 
7
7
  class VercelAIPlugin extends CompositePlugin {
8
- static get id () { return 'ai' }
8
+ static id = 'ai'
9
9
  static get plugins () {
10
10
  return {
11
11
  llmobs: VercelAILLMObsPlugin,
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
3
4
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
- const { getModelProvider } = require('./utils')
5
+ const { getModelProvider, parseModelProvider } = require('./utils')
5
6
 
6
- class VercelAITracingPlugin extends TracingPlugin {
7
- static id = 'ai'
7
+ class DdTelemetryPlugin extends TracingPlugin {
8
+ static id = 'ai_tracing_dd_telemetry'
8
9
  static prefix = 'tracing:dd-trace:vercel-ai'
9
10
 
10
11
  bindStart (ctx) {
@@ -30,4 +31,66 @@ class VercelAITracingPlugin extends TracingPlugin {
30
31
  }
31
32
  }
32
33
 
34
+ class VercelAiTelemetryPlugin extends TracingPlugin {
35
+ static id = 'ai_tracing_vercel_telemetry'
36
+ static prefix = 'tracing:ai:telemetry'
37
+
38
+ #streamedCalls = new Set()
39
+
40
+ constructor () {
41
+ super(...arguments)
42
+
43
+ this.addSub('dd-trace:vercel-ai:chunk', ({ ctx, chunk, done }) => {
44
+ ctx.streamConsumed = done
45
+ })
46
+ }
47
+
48
+ bindStart (ctx) {
49
+ const { type: name, event } = ctx
50
+ const model = event.modelId
51
+ const modelProvider = parseModelProvider(event.provider, model)
52
+
53
+ let isStream = this.#streamedCalls.has(event.callId)
54
+ if (name.includes('stream')) {
55
+ this.#streamedCalls.add(event.callId)
56
+ isStream = true
57
+
58
+ ctx.streamConsumed = false
59
+ }
60
+
61
+ ctx.isStream = isStream
62
+
63
+ this.startSpan(name, {
64
+ meta: {
65
+ 'resource.name': event.functionId ?? name,
66
+ 'ai.request.model': model,
67
+ 'ai.request.model_provider': modelProvider,
68
+ },
69
+ }, ctx)
70
+
71
+ return ctx.currentStore
72
+ }
73
+
74
+ asyncEnd (ctx) {
75
+ // check if isStreamed and stream resolved
76
+ // this event will fire multiple times for the same channel
77
+ if (ctx.isStream && ctx.result?.stream && !ctx.streamConsumed) return
78
+
79
+ if (ctx.type?.includes('stream')) {
80
+ this.#streamedCalls.delete(ctx.event?.callId)
81
+ }
82
+
83
+ const span = ctx.currentStore?.span
84
+ span?.finish()
85
+ }
86
+ }
87
+
88
+ class VercelAITracingPlugin extends CompositePlugin {
89
+ static id = 'ai_tracing'
90
+ static plugins = {
91
+ dd: DdTelemetryPlugin,
92
+ ai: VercelAiTelemetryPlugin,
93
+ }
94
+ }
95
+
33
96
  module.exports = VercelAITracingPlugin
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { parseModelId } = require('../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
3
+ const { parseModelId: parseBedrockModelId } = require('../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
4
4
 
5
5
  /**
6
6
  * Get the model provider from the span tags or attributes.
@@ -11,12 +11,24 @@ const { parseModelId } = require('../../datadog-plugin-aws-sdk/src/services/bedr
11
11
  */
12
12
  function getModelProvider (tags) {
13
13
  const modelProviderTag = tags['ai.model.provider']
14
- const providerParts = modelProviderTag?.split('.')
14
+ const modelId = tags['ai.model.id']
15
+
16
+ return parseModelProvider(modelProviderTag, modelId)
17
+ }
18
+
19
+ /**
20
+ * Parse the model provider from the raw provider string.
21
+ *
22
+ * @param {string} rawProvider
23
+ * @param {string} modelId
24
+ * @returns {string}
25
+ */
26
+ function parseModelProvider (rawProvider, modelId) {
27
+ const providerParts = rawProvider?.split('.')
15
28
  const provider = providerParts?.[0]
16
29
 
17
30
  if (provider === 'amazon-bedrock') {
18
- const modelId = tags['ai.model.id']
19
- const model = modelId && parseModelId(modelId)
31
+ const model = modelId && parseBedrockModelId(modelId)
20
32
  return model?.modelProvider ?? provider
21
33
  }
22
34
 
@@ -25,4 +37,5 @@ function getModelProvider (tags) {
25
37
 
26
38
  module.exports = {
27
39
  getModelProvider,
40
+ parseModelProvider,
28
41
  }
@@ -50,6 +50,7 @@ class BaseContextPlugin extends TracingPlugin {
50
50
 
51
51
  this.startSpan(spanName, {
52
52
  resource,
53
+ type: this.constructor.type,
53
54
  kind: this.constructor.kind,
54
55
  meta,
55
56
  metrics,
@@ -52,6 +52,7 @@ class AwsDurableExecutionSdkJsHandlerPlugin extends TracingPlugin {
52
52
 
53
53
  this.startSpan(this.operationName(), {
54
54
  resource: handler?.name,
55
+ type: this.constructor.type,
55
56
  kind: this.constructor.kind,
56
57
  meta,
57
58
  }, ctx)
@@ -97,11 +97,22 @@ class ChildProcessPlugin extends TracingPlugin {
97
97
  }
98
98
 
99
99
  asyncEnd (ctx) {
100
- const { result } = ctx
100
+ const { result, error } = ctx
101
+ let exitCode
102
+
103
+ if (result !== null && typeof result === 'object') {
104
+ // util.promisify(execFile) resolves with a { stdout, stderr } object on
105
+ // success, where the exit code is 0.
106
+ exitCode = result.status ?? 0
107
+ } else if (result === undefined && error !== undefined) {
108
+ exitCode = error.status ?? error.code ?? 0
109
+ } else {
110
+ exitCode = result
111
+ }
101
112
 
102
113
  const span = ctx.currentStore?.span || this.activeSpan
103
114
 
104
- span?.setTag('cmd.exit_code', `${result}`)
115
+ span?.setTag('cmd.exit_code', `${exitCode}`)
105
116
  span?.finish()
106
117
 
107
118
  return ctx.parentStore
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ require('../../datadog-instrumentations/src/claude-agent-sdk')
4
+
5
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
6
+ const ClaudeAgentSdkLLMObsPlugins = require('../../dd-trace/src/llmobs/plugins/claude-agent-sdk')
7
+ const ClaudeAgentSdkTracingPlugins = require('./tracing')
8
+
9
+ class ClaudeAgentSdkPlugin extends CompositePlugin {
10
+ static id = 'claude-agent-sdk'
11
+
12
+ static get plugins () {
13
+ const plugins = {}
14
+
15
+ // LLM Obs plugins must be registered before tracing plugins so that
16
+ // annotations are added to the span before it finishes.
17
+ // The tracing plugin uses `bindStart` vs the LLM Obs plugin's `start`,
18
+ // so the span is created in the tracing plugin before the LLM Obs one runs.
19
+ for (const Plugin of ClaudeAgentSdkLLMObsPlugins) {
20
+ plugins[Plugin.id] = Plugin
21
+ }
22
+
23
+ for (const Plugin of ClaudeAgentSdkTracingPlugins) {
24
+ plugins[Plugin.id] = Plugin
25
+ }
26
+
27
+ return plugins
28
+ }
29
+ }
30
+
31
+ module.exports = ClaudeAgentSdkPlugin
@@ -0,0 +1,107 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
+ const { splitModel } = require('./util')
6
+
7
+ class QueryTracingPlugin extends TracingPlugin {
8
+ static id = 'claude_agent_sdk_query'
9
+ static operation = 'turn'
10
+ static system = 'claude-agent-sdk'
11
+ static prefix = 'tracing:orchestrion:@anthropic-ai/claude-agent-sdk:query'
12
+
13
+ bindStart (ctx) {
14
+ this.startSpan('claude_agent_sdk.query', {
15
+ meta: { 'resource.name': 'claude_agent_sdk.query' },
16
+ startTime: ctx.startTime,
17
+ }, ctx)
18
+ ctx.runInContext = fn => storage('legacy').run(ctx.currentStore, fn)
19
+ return ctx.currentStore
20
+ }
21
+
22
+ asyncEnd (ctx) {
23
+ if (!ctx.streamResolved) return
24
+
25
+ ctx.currentStore?.span?.finish(ctx.finishTime)
26
+ }
27
+ }
28
+
29
+ class StepTracingPlugin extends TracingPlugin {
30
+ static id = 'claude_agent_sdk_step'
31
+ static operation = 'step'
32
+ static system = 'claude-agent-sdk'
33
+ static prefix = 'tracing:apm:claude-agent-sdk:step'
34
+
35
+ bindStart (ctx) {
36
+ this.startSpan(`step-${ctx.stepIndex}`, {
37
+ meta: { 'resource.name': 'claude_agent_sdk.step' },
38
+ startTime: ctx.startTime,
39
+ }, ctx)
40
+
41
+ return ctx.currentStore
42
+ }
43
+
44
+ end (ctx) {
45
+ ctx.currentStore?.span?.finish(ctx.finishTime)
46
+ }
47
+ }
48
+
49
+ class ToolTracingPlugin extends TracingPlugin {
50
+ static id = 'claude_agent_sdk_tool'
51
+ static operation = 'tool'
52
+ static system = 'claude-agent-sdk'
53
+ static prefix = 'tracing:apm:claude-agent-sdk:tool'
54
+
55
+ bindStart (ctx) {
56
+ const toolName = ctx.name || 'claude_agent_sdk.tool'
57
+
58
+ this.startSpan(toolName, {
59
+ meta: { 'resource.name': 'claude_agent_sdk.tool' },
60
+ startTime: ctx.startTime,
61
+ }, ctx)
62
+
63
+ return ctx.currentStore
64
+ }
65
+
66
+ end (ctx) {
67
+ const span = ctx.currentStore?.span
68
+ if (ctx.error) this.addError(ctx.error, span)
69
+ span?.finish(ctx.finishTime)
70
+ }
71
+ }
72
+
73
+ class LlmTracingPlugin extends TracingPlugin {
74
+ static id = 'claude_agent_sdk_llm'
75
+ static operation = 'llm'
76
+ static system = 'claude-agent-sdk'
77
+ static prefix = 'tracing:apm:claude-agent-sdk:llm'
78
+
79
+ bindStart (ctx) {
80
+ const { model } = ctx
81
+
82
+ const { modelName, modelProvider } = splitModel(model)
83
+ const name = modelName || 'claude_agent_sdk.llm'
84
+
85
+ this.startSpan(name, {
86
+ meta: {
87
+ 'resource.name': 'claude_agent_sdk.llm',
88
+ 'claude-agent-sdk.request.model_name': modelName,
89
+ 'claude-agent-sdk.request.model_provider': modelProvider,
90
+ },
91
+ startTime: ctx.startTime,
92
+ }, ctx)
93
+
94
+ return ctx.currentStore
95
+ }
96
+
97
+ end (ctx) {
98
+ ctx.currentStore?.span?.finish(ctx.finishTime)
99
+ }
100
+ }
101
+
102
+ module.exports = [
103
+ QueryTracingPlugin,
104
+ StepTracingPlugin,
105
+ ToolTracingPlugin,
106
+ LlmTracingPlugin,
107
+ ]
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Parse a model ID string into the model name and provider for claude agent sdk provided model IDs
5
+ * @param {string} model model id
6
+ * @returns {{ modelName: string, modelProvider: string }}
7
+ */
8
+ function splitModel (model) {
9
+ if (!model) return { modelName: undefined, modelProvider: 'anthropic' }
10
+ const idx = model.indexOf('/')
11
+ if (idx === -1) return { modelName: model, modelProvider: 'anthropic' }
12
+ return { modelName: model.slice(idx + 1), modelProvider: model.slice(0, idx) }
13
+ }
14
+
15
+ module.exports = { splitModel }