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
@@ -241,7 +241,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
241
241
  if (res?.errors?.length) {
242
242
  span.setTag('error', res.errors[0])
243
243
  for (const err of res.errors) {
244
- extractErrorIntoSpanEvent(this._tracerConfig, span, err)
244
+ extractErrorIntoSpanEvent(this.config, span, err)
245
245
  }
246
246
  }
247
247
 
@@ -271,6 +271,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
271
271
  const loc = this.config.source && document && fieldNode && fieldNode.loc
272
272
  const source = loc && document.slice(loc.start, loc.end)
273
273
 
274
+ // ctx form: startSpan sets field.currentStore = { ...activeStore, span }
275
+ // without entering it. Only the field's first resolver call runs in that
276
+ // store (isFirst check in wrapResolve); siblings use field.parentStore.
274
277
  const span = this.startSpan('graphql.resolve', {
275
278
  service: this.config.service,
276
279
  resource: `${fieldName}:${returnType}`,
@@ -283,7 +286,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
283
286
  'graphql.field.type': baseTypeName,
284
287
  'graphql.source': source,
285
288
  },
286
- }, false)
289
+ }, field)
290
+
291
+ field.span = span
287
292
 
288
293
  if (fieldNode && this.config.variables && fieldNode.arguments) {
289
294
  const variables = this.config.variables(variableValues)
@@ -388,15 +393,22 @@ function wrapResolve (resolve) {
388
393
  pathString,
389
394
  collapsedKey: collapsedKey ?? pathString,
390
395
  span: null,
396
+ // Set by startResolveSpan; currentStore is used by the first resolver
397
+ // call only, siblings use parentStore (see the isFirst check below).
398
+ parentStore: null,
399
+ currentStore: null,
391
400
  }
392
401
  rootCtx.fields.set(fieldKey, field)
393
402
  }
394
403
 
395
404
  // Collapsed siblings still publish updateField (master's contract: one
396
405
  // publish per resolver call, even when the span is collapsed) and route
397
- // through callInAsyncScope so the abort signal stops them mid-flight.
406
+ // through callInAsyncScope so the abort signal stops them mid-flight. They
407
+ // run in the parent store, not field.currentStore: the first sibling's
408
+ // synchronous resolver already finished the shared graphql.resolve span, so
409
+ // re-entering its store would parent user spans to a closed span.
398
410
  if (!isFirst) {
399
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err) => {
411
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => {
400
412
  if (updateFieldCh.hasSubscribers) {
401
413
  updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
402
414
  }
@@ -406,9 +418,8 @@ function wrapResolve (resolve) {
406
418
  const executeSpan = rootCtx.executeSpan
407
419
  const startTime = executeSpan._getTime()
408
420
  const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
409
- field.span = span
410
421
 
411
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err, res) => {
422
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.currentStore, (err, res) => {
412
423
  const endTime = executeSpan._getTime()
413
424
  rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
414
425
  if (updateFieldCh.hasSubscribers) {
@@ -446,16 +457,16 @@ function wrapFieldType (field) {
446
457
  wrapFields(unwrapped)
447
458
  }
448
459
 
449
- function callInAsyncScope (fn, thisArg, args, abortController, cb) {
450
- cb ??= () => {}
451
-
460
+ // Runs the resolver inside `store`, including any code after an internal
461
+ // `await`. A `.then()` the caller attaches afterward runs outside `store`.
462
+ function callInAsyncScope (fn, thisArg, args, abortController, store, cb) {
452
463
  if (abortController?.signal.aborted) {
453
464
  cb(null, null)
454
465
  throw new AbortError('Aborted')
455
466
  }
456
467
 
457
468
  try {
458
- const result = fn.apply(thisArg, args)
469
+ const result = legacyStorage.run(store, () => fn.apply(thisArg, args))
459
470
  if (typeof result?.then === 'function') {
460
471
  return result.then(
461
472
  res => { cb(null, res); return res },
@@ -30,6 +30,13 @@ class GraphQLPlugin extends CompositePlugin {
30
30
  }
31
31
 
32
32
  // config validator helpers
33
+ //
34
+ // `collapse`, `depth`, `variables`, and `errorExtensions` arrive pre-merged on
35
+ // `config`: plugin_manager seeds the `DD_TRACE_GRAPHQL_*` env values (already
36
+ // parsed to their declared type) as the base and a programmatic
37
+ // `tracer.use('graphql', …)` value overrides them. So these helpers only shape
38
+ // the merged value — coerce, validate, and turn `variables` into a filter — and
39
+ // never read the environment themselves.
33
40
 
34
41
  function validateConfig (config) {
35
42
  const collapse = config.collapse === undefined || !!config.collapse
@@ -41,6 +48,7 @@ function validateConfig (config) {
41
48
  // v5 counted collapsed list indices toward `depth`, so the same query reached a
42
49
  // different depth depending on `collapse`. v6 counts selection-set depth only.
43
50
  countListIndices: DD_MAJOR < 6 && collapse,
51
+ errorExtensions: getErrorExtensions(config),
44
52
  hooks: getHooks(config),
45
53
  }
46
54
  }
@@ -58,13 +66,21 @@ function getVariablesFilter (config) {
58
66
  if (typeof config.variables === 'function') {
59
67
  return config.variables
60
68
  } else if (Array.isArray(config.variables)) {
61
- return variables => pick(variables, config.variables)
69
+ return config.variables.length > 0 ? variables => pick(variables, config.variables) : null
62
70
  } else if (config.hasOwnProperty('variables')) {
63
71
  log.error('Expected `variables` to be an array or function.')
64
72
  }
65
73
  return null
66
74
  }
67
75
 
76
+ function getErrorExtensions (config) {
77
+ if (Array.isArray(config.errorExtensions)) {
78
+ return config.errorExtensions.length > 0 ? config.errorExtensions : undefined
79
+ } else if (config.hasOwnProperty('errorExtensions')) {
80
+ log.error('Expected `errorExtensions` to be an array.')
81
+ }
82
+ }
83
+
68
84
  const noop = () => {}
69
85
  const noopHooks = { execute: noop, parse: noop, validate: noop, resolve: undefined }
70
86
 
@@ -1,5 +1,12 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * @param {{ errorExtensions?: string[] }} config Resolved plugin config; `errorExtensions` lists the
5
+ * GraphQL error `extensions` keys to copy onto the span event.
6
+ * @param {import('../../dd-trace/src/opentracing/span')} span
7
+ * @param {{ name?: string, message?: string, stack?: string, locations?: Array<{ line: number, column: number }>,
8
+ * path?: Array<string|number>, extensions?: Record<string, unknown> }} exc
9
+ */
3
10
  function extractErrorIntoSpanEvent (config, span, exc) {
4
11
  const attributes = {}
5
12
 
@@ -29,8 +36,8 @@ function extractErrorIntoSpanEvent (config, span, exc) {
29
36
  attributes.message = exc.message
30
37
  }
31
38
 
32
- if (config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
33
- for (const ext of config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
39
+ if (config.errorExtensions) {
40
+ for (const ext of config.errorExtensions) {
34
41
  if (exc.extensions?.[ext]) {
35
42
  const value = exc.extensions[ext]
36
43
 
@@ -38,7 +38,7 @@ class GraphQLValidatePlugin extends TracingPlugin {
38
38
  if (errors?.length) {
39
39
  span.setTag('error', errors[0])
40
40
  for (const err of errors) {
41
- extractErrorIntoSpanEvent(this._tracerConfig, span, err)
41
+ extractErrorIntoSpanEvent(this.config, span, err)
42
42
  }
43
43
  }
44
44
 
@@ -61,15 +61,6 @@ const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
61
61
  // https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38
62
62
  const CHILD_MESSAGE_END = 2
63
63
 
64
- function withTimeout (promise, timeoutMs) {
65
- return new Promise(resolve => {
66
- realSetTimeout(resolve, timeoutMs)
67
-
68
- // Also resolve if the original promise resolves
69
- promise.then(resolve)
70
- })
71
- }
72
-
73
64
  class JestPlugin extends CiPlugin {
74
65
  static id = 'jest'
75
66
 
@@ -451,6 +442,7 @@ class JestPlugin extends CiPlugin {
451
442
  isAtrRetry,
452
443
  finalStatus,
453
444
  earlyFlakeAbortReason,
445
+ promises,
454
446
  }) => {
455
447
  span.setTag(TEST_STATUS, status)
456
448
  if (finalStatus) {
@@ -481,12 +473,33 @@ class JestPlugin extends CiPlugin {
481
473
  this.getTestTelemetryTags(span)
482
474
  )
483
475
 
484
- span.finish()
485
- finishAllTraceSpans(span)
486
- this.activeTestSpan = null
476
+ const finish = () => {
477
+ span.finish()
478
+ finishAllTraceSpans(span)
479
+ this.activeTestSpan = null
480
+ }
481
+
482
+ if (finalStatus) {
483
+ if (promises?.hitBreakpointPromise) {
484
+ finish()
485
+ this.cancelDiBreakpointHitWait()
486
+ return
487
+ }
488
+ if (promises && this.diBreakpointHitPromise) {
489
+ promises.hitBreakpointPromise = this.waitForPreparedDiBreakpointHit().then(finish)
490
+ return
491
+ }
492
+ finish()
493
+ this.cancelDiBreakpointHitWait()
494
+ return
495
+ }
496
+ finish()
497
+ if (status === 'fail' && promises?.hitBreakpointPromise) {
498
+ this.prepareDiBreakpointHitWait()
499
+ }
487
500
  })
488
501
 
489
- this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, promises }) => {
502
+ this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, shouldWaitForHitProbe, promises }) => {
490
503
  if (error && span) {
491
504
  span.setTag(TEST_STATUS, 'fail')
492
505
  span.setTag('error', getFormattedError(error, this.repositoryRoot))
@@ -494,8 +507,11 @@ class JestPlugin extends CiPlugin {
494
507
  const probeInformation = this.addDiProbe(error)
495
508
  if (probeInformation) {
496
509
  const { setProbePromise } = probeInformation
497
- promises.isProbeReady = withTimeout(setProbePromise, 2000)
510
+ this.prepareDiBreakpointHitWait()
511
+ promises.isProbeReady = this.waitForDiOperation(setProbePromise)
498
512
  }
513
+ } else if (shouldWaitForHitProbe && this.di) {
514
+ promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
499
515
  }
500
516
  }
501
517
  })
@@ -1,8 +1,5 @@
1
1
  'use strict'
2
2
 
3
- // Capture real Date.now at module load time, before any test can install fake timers.
4
- const realDateNow = Date.now.bind(Date)
5
-
6
3
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
7
4
  const { storage } = require('../../datadog-core')
8
5
 
@@ -52,8 +49,6 @@ const {
52
49
  TELEMETRY_TEST_SESSION,
53
50
  } = require('../../dd-trace/src/ci-visibility/telemetry')
54
51
 
55
- const BREAKPOINT_SET_GRACE_PERIOD_MS = 200
56
-
57
52
  class MochaPlugin extends CiPlugin {
58
53
  static id = 'mocha'
59
54
 
@@ -263,6 +258,7 @@ class MochaPlugin extends CiPlugin {
263
258
  span.finish()
264
259
  finishAllTraceSpans(span)
265
260
  this.activeTestSpan = null
261
+ this.cancelDiBreakpointHitWait()
266
262
  if (this.di && this.libraryConfig?.isDiEnabled && this.runningTestProbe && isLastRetry) {
267
263
  this.removeDiProbe(this.runningTestProbe)
268
264
  this.runningTestProbe = null
@@ -307,8 +303,24 @@ class MochaPlugin extends CiPlugin {
307
303
  return ctx.currentStore
308
304
  })
309
305
 
310
- this.addSub('ci:mocha:test:retry', ({ span, isFirstAttempt, willBeRetried, err, test, isAtrRetry }) => {
306
+ this.addSub('ci:mocha:test:retry', ({
307
+ span,
308
+ isFirstAttempt,
309
+ willBeRetried,
310
+ err,
311
+ test,
312
+ isAtrRetry,
313
+ promises,
314
+ }) => {
311
315
  if (span) {
316
+ const finishSpan = () => {
317
+ span.finish()
318
+ finishAllTraceSpans(span)
319
+ if (this.activeTestSpan === span) {
320
+ this.activeTestSpan = null
321
+ }
322
+ }
323
+
312
324
  span.setTag(TEST_STATUS, 'fail')
313
325
  if (!isFirstAttempt) {
314
326
  span.setTag(TEST_IS_RETRY, 'true')
@@ -330,21 +342,34 @@ class MochaPlugin extends CiPlugin {
330
342
  if (isFirstAttempt && willBeRetried && this.di && this.libraryConfig?.isDiEnabled) {
331
343
  const probeInformation = this.addDiProbe(err)
332
344
  if (probeInformation) {
333
- const { file, line, stackIndex } = probeInformation
345
+ const { file, line, stackIndex, setProbePromise } = probeInformation
334
346
  this.runningTestProbe = { file, line }
335
347
  this.testErrorStackIndex = stackIndex
336
348
  test._ddShouldWaitForHitProbe = true
337
- const waitUntil = realDateNow() + BREAKPOINT_SET_GRACE_PERIOD_MS
338
- while (realDateNow() < waitUntil) {
339
- // TODO: To avoid a race condition, we should wait until `probeInformation.setProbePromise` has resolved.
340
- // However, Mocha doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to
341
- // fall back to a fixed syncronous delay.
349
+ this.prepareDiBreakpointHitWait()
350
+ if (promises) {
351
+ promises.setProbePromise = this.waitForDiOperation(setProbePromise)
342
352
  }
343
353
  }
344
354
  }
345
355
 
346
- span.finish()
347
- finishAllTraceSpans(span)
356
+ if (!isFirstAttempt &&
357
+ willBeRetried &&
358
+ this.di &&
359
+ this.libraryConfig?.isDiEnabled &&
360
+ this.runningTestProbe &&
361
+ promises) {
362
+ promises.finishTestPromise = this.waitForInFlightDiBreakpointHits().then(finishSpan, finishSpan)
363
+ return
364
+ }
365
+
366
+ finishSpan()
367
+ }
368
+ })
369
+
370
+ this.addSub('ci:mocha:test:di:wait', ({ promises }) => {
371
+ if (this.di) {
372
+ promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
348
373
  }
349
374
  })
350
375
 
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const MongodbCoreQueryPlugin = require('./query')
4
+
5
+ const MAX_RESOURCE_LENGTH = 10_000
6
+
7
+ class MongodbCoreBulkWritePlugin extends MongodbCoreQueryPlugin {
8
+ // bulkWrite is higher-level than the wire commands `query` traces, so it has its own operation
9
+ // and thus its own `apm:mongodb:bulkwrite:*` channels. It reuses the query plugin's `id`,
10
+ // `component`, and collection-stripping `getPeerService`, and inherits `finish`, `error`, and the
11
+ // finish-time parent-store restore from the tracing/outbound base classes.
12
+ static operation = 'bulkwrite'
13
+
14
+ /**
15
+ * Open the parent span for a `Collection#bulkWrite`. The per-type wire commands nest as
16
+ * children and carry the statements, host, and DBM comment, so this span only records
17
+ * the namespace and resource.
18
+ *
19
+ * @param {{ ns: string }} ctx
20
+ */
21
+ bindStart (ctx) {
22
+ const { ns } = ctx
23
+ const serviceResult = this.serviceName({ pluginConfig: this.config })
24
+
25
+ this.startSpan(this.operationName(), {
26
+ service: serviceResult,
27
+ resource: truncate(`bulkWrite ${ns}`),
28
+ type: 'mongodb',
29
+ kind: 'client',
30
+ meta: {
31
+ 'db.name': ns,
32
+ },
33
+ }, ctx)
34
+
35
+ return ctx.currentStore
36
+ }
37
+ }
38
+
39
+ function truncate (input) {
40
+ return input.length > MAX_RESOURCE_LENGTH ? input.slice(0, MAX_RESOURCE_LENGTH) : input
41
+ }
42
+
43
+ module.exports = MongodbCoreBulkWritePlugin