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
@@ -11,7 +11,7 @@ const path = require('path')
11
11
  const satisfies = require('../../../vendor/dist/semifies')
12
12
  const { DD_MAJOR } = require('../../../version')
13
13
  const shimmer = require('../../datadog-shimmer')
14
- const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
14
+ const { getEnvironmentVariable, getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
15
15
  const log = require('../../dd-trace/src/log')
16
16
  const {
17
17
  EMPTY_EFD_RETRY_POLICY,
@@ -64,6 +64,7 @@ const { addHook, channel } = require('./helpers/instrument')
64
64
  const testSessionStartCh = channel('ci:jest:session:start')
65
65
  const testSessionFinishCh = channel('ci:jest:session:finish')
66
66
  const codeCoverageReportCh = channel('ci:jest:coverage-report')
67
+ const bundlerLoadCh = channel('dd-trace:bundler:load')
67
68
 
68
69
  const testSessionConfigurationCh = channel('ci:jest:session:configuration')
69
70
 
@@ -179,7 +180,9 @@ const wrappedJestEsmLoaders = new WeakSet()
179
180
  const wrappedJestObjects = new WeakSet()
180
181
  const wrappedWorkerInitializers = new WeakSet()
181
182
  const publishedRuntimeReferenceErrors = new WeakMap()
182
- const jestEsmBypassModulePathsByRuntime = new WeakMap()
183
+ const jestEsmLoggingModulePathsByRuntime = new WeakMap()
184
+ const jestLoggingPackagesByRuntime = new WeakMap()
185
+ const instrumentedJestLoggingModules = new WeakMap()
183
186
  const wrappedCoverageReporters = new WeakSet()
184
187
  const coverageReporterRequires = new WeakMap()
185
188
  const handledJestEvents = new WeakSet()
@@ -3733,13 +3736,15 @@ if (DD_MAJOR < 6) {
3733
3736
  }, jestConfigSyncWrapper)
3734
3737
  }
3735
3738
 
3736
- const LOGGING_LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = new Set([
3739
+ const JEST_LOGGING_LIBRARIES = new Set([
3737
3740
  'bunyan',
3738
3741
  'pino',
3739
3742
  'winston',
3740
3743
  ])
3744
+ const disabledJestInstrumentations = new Set(
3745
+ getValueFromEnvSources('DD_TRACE_DISABLED_INSTRUMENTATIONS')?.split(',')
3746
+ )
3741
3747
  const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = new Set([
3742
- ...LOGGING_LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE,
3743
3748
  'selenium-webdriver',
3744
3749
  'selenium-webdriver/chrome',
3745
3750
  'selenium-webdriver/edge',
@@ -3884,13 +3889,17 @@ function requireOutsideJestRequireEngine (runtime, moduleName) {
3884
3889
  * @param {string} moduleName
3885
3890
  * @returns {void}
3886
3891
  */
3887
- function recordJestEsmBypassModulePath (runtime, from, moduleName) {
3888
- if (typeof from !== 'string' || !LOGGING_LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) return
3892
+ function recordJestEsmLoggingModulePath (runtime, from, moduleName) {
3893
+ if (
3894
+ typeof from !== 'string' ||
3895
+ !JEST_LOGGING_LIBRARIES.has(moduleName) ||
3896
+ disabledJestInstrumentations.has(moduleName)
3897
+ ) return
3889
3898
 
3890
- let pathsByParent = jestEsmBypassModulePathsByRuntime.get(runtime)
3899
+ let pathsByParent = jestEsmLoggingModulePathsByRuntime.get(runtime)
3891
3900
  if (!pathsByParent) {
3892
3901
  pathsByParent = new Map()
3893
- jestEsmBypassModulePathsByRuntime.set(runtime, pathsByParent)
3902
+ jestEsmLoggingModulePathsByRuntime.set(runtime, pathsByParent)
3894
3903
  }
3895
3904
 
3896
3905
  let modulePaths = pathsByParent.get(from)
@@ -3911,16 +3920,15 @@ function recordJestEsmBypassModulePath (runtime, from, moduleName) {
3911
3920
  * @param {object} runtime
3912
3921
  * @param {string} from
3913
3922
  * @param {string} modulePath
3914
- * @returns {boolean}
3923
+ * @returns {string | undefined}
3915
3924
  */
3916
- function hasJestEsmBypassModulePath (runtime, from, modulePath) {
3917
- const modulePaths = jestEsmBypassModulePathsByRuntime.get(runtime)?.get(from)
3918
- if (!modulePaths) return false
3925
+ function getJestEsmLoggingModuleName (runtime, from, modulePath) {
3926
+ const modulePaths = jestEsmLoggingModulePathsByRuntime.get(runtime)?.get(from)
3927
+ if (!modulePaths) return
3919
3928
 
3920
- for (const resolvedPath of modulePaths.values()) {
3921
- if (resolvedPath === modulePath) return true
3929
+ for (const [moduleName, resolvedPath] of modulePaths) {
3930
+ if (resolvedPath === modulePath) return moduleName
3922
3931
  }
3923
- return false
3924
3932
  }
3925
3933
 
3926
3934
  /**
@@ -3934,7 +3942,7 @@ function wrapJestEsmLoader (runtime) {
3934
3942
  wrappedJestEsmLoaders.add(esmLoader)
3935
3943
  if (typeof esmLoader.resolveModule === 'function') {
3936
3944
  shimmer.wrap(esmLoader, 'resolveModule', resolveModule => function (moduleName, from) {
3937
- recordJestEsmBypassModulePath(runtime, from, moduleName)
3945
+ recordJestEsmLoggingModulePath(runtime, from, moduleName)
3938
3946
  return resolveModule.apply(this, arguments)
3939
3947
  })
3940
3948
  }
@@ -3943,7 +3951,7 @@ function wrapJestEsmLoader (runtime) {
3943
3951
  esmLoader,
3944
3952
  'resolveSpecifierForSyncGraph',
3945
3953
  resolveSpecifier => function (from, moduleName) {
3946
- recordJestEsmBypassModulePath(runtime, from, moduleName)
3954
+ recordJestEsmLoggingModulePath(runtime, from, moduleName)
3947
3955
  return resolveSpecifier.apply(this, arguments)
3948
3956
  }
3949
3957
  )
@@ -3960,11 +3968,10 @@ function getJestBypassModulePath (runtime, from, moduleName) {
3960
3968
  if (typeof from !== 'string' || typeof moduleName !== 'string') return
3961
3969
 
3962
3970
  if (!LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) {
3963
- // Jest passes the resolved path when a CommonJS package is imported from an ESM test.
3964
- if (path.isAbsolute(moduleName) && hasJestEsmBypassModulePath(runtime, from, moduleName)) {
3965
- return moduleName
3966
- }
3967
- return
3971
+ // Keep the native fallback for logging packages that cannot be instrumented in Jest's realm, such as a
3972
+ // package-name-preserving symlink whose target is a user wrapper rather than the package itself.
3973
+ if (path.isAbsolute(moduleName) && getJestEsmLoggingModuleName(runtime, from, moduleName)) return moduleName
3974
+ if (!JEST_LOGGING_LIBRARIES.has(moduleName) || disabledJestInstrumentations.has(moduleName)) return
3968
3975
  }
3969
3976
 
3970
3977
  try {
@@ -3983,6 +3990,113 @@ function getJestBypassModulePath (runtime, from, moduleName) {
3983
3990
  }
3984
3991
  }
3985
3992
 
3993
+ /**
3994
+ * @param {object} runtime
3995
+ * @param {string} from
3996
+ * @param {string} moduleName
3997
+ * @returns {string}
3998
+ */
3999
+ function resolveJestModulePath (runtime, from, moduleName) {
4000
+ if (path.isAbsolute(moduleName)) return moduleName
4001
+
4002
+ if (typeof runtime._resolveCjsModule === 'function') {
4003
+ return runtime._resolveCjsModule(from, moduleName)
4004
+ } else if (typeof runtime.cjsLoader?.resolution?.resolveCjs === 'function') {
4005
+ return runtime.cjsLoader.resolution.resolveCjs(from, moduleName)
4006
+ }
4007
+ return runtime._resolveModule(from, moduleName)
4008
+ }
4009
+
4010
+ /**
4011
+ * @param {object} runtime
4012
+ * @param {string} from
4013
+ * @param {string} moduleName
4014
+ * @returns {{ name: string, path: string, version: string } | undefined}
4015
+ */
4016
+ function getJestLoggingPackage (runtime, from, moduleName) {
4017
+ if (typeof from !== 'string' || typeof moduleName !== 'string') return
4018
+
4019
+ const directModuleName = JEST_LOGGING_LIBRARIES.has(moduleName) && !disabledJestInstrumentations.has(moduleName)
4020
+ ? moduleName
4021
+ : getJestEsmLoggingModuleName(runtime, from, moduleName)
4022
+ const packages = jestLoggingPackagesByRuntime.get(runtime)
4023
+ let containingPackage
4024
+ if (!directModuleName) {
4025
+ if (!packages) return
4026
+
4027
+ const normalizedFrom = from.replaceAll(path.sep, '/')
4028
+ for (const [packageRoot, loggingPackage] of packages) {
4029
+ if (normalizedFrom.startsWith(`${packageRoot}/`)) {
4030
+ containingPackage = { packageRoot, ...loggingPackage }
4031
+ break
4032
+ }
4033
+ }
4034
+ if (!containingPackage) return
4035
+ }
4036
+
4037
+ try {
4038
+ const modulePath = resolveJestModulePath(runtime, from, moduleName)
4039
+ const normalizedModulePath = modulePath.replaceAll(path.sep, '/')
4040
+
4041
+ if (directModuleName) {
4042
+ if (modulePath !== createRequire(from).resolve(directModuleName)) return
4043
+
4044
+ const nodeModulesPath = `/node_modules/${directModuleName}/`
4045
+ const packagePathIndex = normalizedModulePath.lastIndexOf(nodeModulesPath)
4046
+ if (packagePathIndex === -1) return
4047
+
4048
+ const packageRoot = normalizedModulePath.slice(0, packagePathIndex + nodeModulesPath.length - 1)
4049
+ const { version } = JSON.parse(readFileSync(`${packageRoot}/package.json`, 'utf8'))
4050
+ if (typeof version !== 'string') return
4051
+
4052
+ let runtimePackages = packages
4053
+ if (!runtimePackages) {
4054
+ runtimePackages = new Map()
4055
+ jestLoggingPackagesByRuntime.set(runtime, runtimePackages)
4056
+ }
4057
+ runtimePackages.set(packageRoot, { name: directModuleName, version })
4058
+ return { name: directModuleName, path: directModuleName, version }
4059
+ }
4060
+
4061
+ const { packageRoot, name, version } = containingPackage
4062
+ if (!normalizedModulePath.startsWith(`${packageRoot}/`)) return
4063
+
4064
+ const modulePathWithinPackage = normalizedModulePath.slice(packageRoot.length + 1)
4065
+ return { name, path: `${name}/${modulePathWithinPackage}`, version }
4066
+ } catch {
4067
+ // Let Jest load unresolved, resolver-only, or virtual modules without instrumentation.
4068
+ }
4069
+ }
4070
+
4071
+ /**
4072
+ * @param {unknown} moduleExports
4073
+ * @param {{ name: string, path: string, version: string } | undefined} loggingPackage
4074
+ * @returns {unknown}
4075
+ */
4076
+ function instrumentJestLoggingModule (moduleExports, loggingPackage) {
4077
+ if (!loggingPackage || (typeof moduleExports !== 'object' && typeof moduleExports !== 'function')) {
4078
+ return moduleExports
4079
+ }
4080
+ if (moduleExports === null) return moduleExports
4081
+
4082
+ const instrumentedModule = instrumentedJestLoggingModules.get(moduleExports)
4083
+ if (instrumentedModule) return instrumentedModule
4084
+
4085
+ const payload = {
4086
+ module: moduleExports,
4087
+ package: loggingPackage.name,
4088
+ path: loggingPackage.path,
4089
+ version: loggingPackage.version,
4090
+ }
4091
+ // Apply the regular instrumentation after Jest has evaluated the module so its built-ins stay in Jest's realm.
4092
+ bundlerLoadCh.publish(payload)
4093
+ instrumentedJestLoggingModules.set(moduleExports, payload.module)
4094
+ if (payload.module && (typeof payload.module === 'object' || typeof payload.module === 'function')) {
4095
+ instrumentedJestLoggingModules.set(payload.module, payload.module)
4096
+ }
4097
+ return payload.module
4098
+ }
4099
+
3986
4100
  function formatDefaultStackTrace (error, structuredStackTrace) {
3987
4101
  const errorString = Error.prototype.toString.call(error)
3988
4102
  if (structuredStackTrace.length === 0) return errorString
@@ -4009,7 +4123,7 @@ addHook({
4009
4123
  // Jest 28 through 30.3 keeps ESM dependency resolution on Runtime itself.
4010
4124
  if (typeof Runtime.prototype.resolveModule === 'function') {
4011
4125
  shimmer.wrap(Runtime.prototype, 'resolveModule', resolveModule => function (moduleName, from) {
4012
- recordJestEsmBypassModulePath(this, from, moduleName)
4126
+ recordJestEsmLoggingModulePath(this, from, moduleName)
4013
4127
  return resolveModule.apply(this, arguments)
4014
4128
  })
4015
4129
  }
@@ -4024,11 +4138,13 @@ addHook({
4024
4138
  shimmer.wrap(Runtime.prototype, 'requireModule', requireModule => function (from, moduleName) {
4025
4139
  wrapJestGlobalsForRuntime(this)
4026
4140
  try {
4141
+ const loggingPackage = getJestLoggingPackage(this, from, moduleName)
4027
4142
  // Jest calls requireModule only after deciding that the module should not be mocked.
4028
- const bypassModulePath = getJestBypassModulePath(this, from, moduleName)
4029
- const returnedValue = bypassModulePath
4143
+ const bypassModulePath = loggingPackage ? undefined : getJestBypassModulePath(this, from, moduleName)
4144
+ let returnedValue = bypassModulePath
4030
4145
  ? requireOutsideJestRequireEngine(this, bypassModulePath)
4031
4146
  : requireModule.apply(this, arguments)
4147
+ returnedValue = instrumentJestLoggingModule(returnedValue, loggingPackage)
4032
4148
  if (moduleName === '@jest/globals') {
4033
4149
  wrapConcurrentJestGlobalsForRuntime(this, returnedValue)
4034
4150
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  const { addHook, getHooks } = require('./helpers/instrument')
4
4
 
5
- for (const hook of getHooks('@langchain/core')) {
5
+ for (const hook of getHooks('@langchain/core').values()) {
6
6
  addHook(hook, exports => exports)
7
7
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  const { addHook, getHooks } = require('./helpers/instrument')
4
4
 
5
- for (const hook of getHooks('@langchain/langgraph')) {
5
+ for (const hook of getHooks('@langchain/langgraph').values()) {
6
6
  addHook(hook, exports => exports)
7
7
  }
@@ -6,6 +6,6 @@
6
6
 
7
7
  const { addHook, getHooks } = require('./helpers/instrument')
8
8
 
9
- for (const hook of getHooks('mercurius')) {
9
+ for (const hook of getHooks('mercurius').values()) {
10
10
  addHook(hook, exports => exports)
11
11
  }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { AsyncResource } = require('node:async_hooks')
4
+
3
5
  const { createCoverageMap } = require('../../../../vendor/dist/istanbul-lib-coverage')
4
6
  const satisfies = require('../../../../vendor/dist/semifies')
5
7
  const { DD_MAJOR } = require('../../../../version')
@@ -80,6 +82,8 @@ const runnerTestEndHandlers = new WeakMap()
80
82
  const runnerFailuresAdjusted = new WeakSet()
81
83
  const runnerFrameworkErrors = new WeakMap()
82
84
  const runnerStarted = new WeakSet()
85
+ const readyRunners = new WeakSet()
86
+ const pendingRunnerStarts = new WeakMap()
83
87
  const runnerRecoveryStates = new WeakMap()
84
88
  const runnersWithPendingCoverageReset = new WeakSet()
85
89
  const parallelRunners = new WeakSet()
@@ -1066,6 +1070,38 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
1066
1070
  runStoresWithCompletion(libraryConfigurationCh, ctx, onReceivedConfiguration)
1067
1071
  }
1068
1072
 
1073
+ /**
1074
+ * @param {import('mocha').Runner} runner
1075
+ * @returns {void}
1076
+ */
1077
+ function startMochaRunner (runner) {
1078
+ if (readyRunners.has(runner)) {
1079
+ runner.suite.run()
1080
+ } else {
1081
+ // Global setup can finish after configuration. Preserve the configuration
1082
+ // context until Runner#run has installed its delayed-start listener.
1083
+ pendingRunnerStarts.set(runner, AsyncResource.bind(() => runner.suite.run()))
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * @param {import('mocha').Runner['run']} run
1089
+ * @param {import('mocha').Runner} runner
1090
+ * @param {Parameters<import('mocha').Runner['run']>} args
1091
+ * @returns {import('mocha').Runner}
1092
+ */
1093
+ function runMochaRunner (run, runner, args) {
1094
+ const result = run.apply(runner, args)
1095
+ // Once delay mode is enabled, startup must complete even if the plugin is disabled during global setup.
1096
+ readyRunners.add(runner)
1097
+ const start = pendingRunnerStarts.get(runner)
1098
+ if (start) {
1099
+ pendingRunnerStarts.delete(runner)
1100
+ start()
1101
+ }
1102
+ return result
1103
+ }
1104
+
1069
1105
  // In this hook we delay the execution with options.delay to grab library configuration,
1070
1106
  // skippable and known tests.
1071
1107
  // It is called but skipped in parallel mode.
@@ -1118,11 +1154,11 @@ function wrapMochaRun (Mocha, frameworkVersion) {
1118
1154
  getCodeCoverageCh.publish({
1119
1155
  onDone: (receivedCodeCoverage) => {
1120
1156
  untestedCoverage = receivedCodeCoverage
1121
- global.run()
1157
+ startMochaRunner(runner)
1122
1158
  },
1123
1159
  })
1124
1160
  } else {
1125
- global.run()
1161
+ startMochaRunner(runner)
1126
1162
  }
1127
1163
  })
1128
1164
 
@@ -1146,7 +1182,7 @@ addHook({
1146
1182
  const mocha = args[0]
1147
1183
 
1148
1184
  /**
1149
- * This attaches `run` to the global context, which we'll call after
1185
+ * This enables the delayed root suite, which we'll release after
1150
1186
  * our configuration and skippable suites requests.
1151
1187
  * You need this both here and in Mocha#run hook: the programmatic API
1152
1188
  * does not call `runMocha`, so it needs to be in Mocha#run. When using
@@ -1178,7 +1214,7 @@ addHook({
1178
1214
 
1179
1215
  shimmer.wrap(Runner.prototype, 'run', run => function (...args) {
1180
1216
  if (!testFinishCh.hasSubscribers) {
1181
- return run.apply(this, args)
1217
+ return runMochaRunner(run, this, args)
1182
1218
  }
1183
1219
 
1184
1220
  const { onRunDone, onFlushDone } = getRunCompletionCallbacks(args[0])
@@ -1511,7 +1547,7 @@ addHook({
1511
1547
  }
1512
1548
  })
1513
1549
 
1514
- return run.apply(this, args)
1550
+ return runMochaRunner(run, this, args)
1515
1551
  })
1516
1552
 
1517
1553
  return Runner
@@ -4,7 +4,7 @@ const { tracingChannel } = require('dc-polyfill')
4
4
  const shimmer = require('../../datadog-shimmer')
5
5
  const { addHook, channel, getHooks } = require('./helpers/instrument')
6
6
 
7
- for (const hook of getHooks('@modelcontextprotocol/sdk')) {
7
+ for (const hook of getHooks('@modelcontextprotocol/sdk').values()) {
8
8
  addHook(hook, exports => exports)
9
9
  }
10
10
 
@@ -24,7 +24,7 @@ function captureChatCompletionsModelBaseURL (ctx) {
24
24
 
25
25
  chatCompletionsModelConstructorCh.end.subscribe(captureChatCompletionsModelBaseURL)
26
26
 
27
- for (const hook of getHooks('@openai/agents-openai')) {
27
+ for (const hook of getHooks('@openai/agents-openai').values()) {
28
28
  addHook(hook, moduleExports => moduleExports)
29
29
  }
30
30
 
@@ -8,46 +8,28 @@ const ch = dc.tracingChannel('apm:openai:request')
8
8
  const onStreamedChunkCh = dc.channel('apm:openai:request:chunk')
9
9
  const responsePromiseContexts = new WeakMap()
10
10
 
11
- // Provider lifecycle channels. Payloads stay OpenAI-native:
12
- // before { args, parentSpan, abortController, pending }
13
- // after { args, body, parentSpan, abortController, pending }
14
- const chatCompletionsBeforeChannel = dc.channel('dd-trace:openai:chat.completions:before')
15
- const chatCompletionsAfterChannel = dc.channel('dd-trace:openai:chat.completions:after')
16
- const responsesBeforeChannel = dc.channel('dd-trace:openai:responses:before')
17
- const responsesAfterChannel = dc.channel('dd-trace:openai:responses:after')
18
-
19
- const LIFECYCLE_CHANNELS = {
20
- 'chat.completions': {
21
- before: chatCompletionsBeforeChannel,
22
- after: chatCompletionsAfterChannel,
23
- },
24
- responses: {
25
- before: responsesBeforeChannel,
26
- after: responsesAfterChannel,
27
- },
28
- }
11
+ // Published synchronously with the native call data. A subscriber may set either callback and
12
+ // this instrumentation applies it, so the wrapping stays here and only the policy lives outside:
13
+ // beforeResult () => Promise<void>|undefined holds the result back until it settles
14
+ // onResult (body) => unknown|Promise<unknown> inspects or replaces the delivered body
15
+ const chatCompletionsInterceptChannel = dc.channel('dd-trace:openai:chat.completions:intercept')
16
+ const responsesInterceptChannel = dc.channel('dd-trace:openai:responses:intercept')
29
17
 
30
18
  /**
31
- * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
32
- *
33
- * Subscribers push async work into `pending` synchronously during publication and
34
- * abort `abortController` with an error before the pushed promise resolves to block.
19
+ * Returns `promise`, but not before `settled` settles, so a rejection from an interceptor
20
+ * reaches the caller instead of the SDK's value.
35
21
  *
36
- * @param {object} channel
37
- * @param {object} payload
38
- * @returns {Promise<void>}
22
+ * @template T
23
+ * @param {Promise<T>} promise
24
+ * @param {Promise<void>|undefined} settled
25
+ * @returns {Promise<T>}
39
26
  */
40
- function publishLifecycle (channel, payload) {
41
- const abortController = new AbortController()
42
- const ctx = { ...payload, abortController, pending: [] }
27
+ function heldUntil (promise, settled) {
28
+ if (!settled) return promise
43
29
 
44
- channel.publish(ctx)
45
-
46
- return Promise.all(ctx.pending).then(() => {
47
- if (abortController.signal.aborted) {
48
- throw abortController.signal.reason
49
- }
50
- })
30
+ // The interceptor's rejection wins over an earlier SDK rejection, so keep that one handled.
31
+ promise.catch(() => {})
32
+ return settled.then(() => promise)
51
33
  }
52
34
 
53
35
  const V4_PACKAGE_SHIMS = [
@@ -57,6 +39,7 @@ const V4_PACKAGE_SHIMS = [
57
39
  baseResource: 'chat.completions',
58
40
  methods: ['create'],
59
41
  streamedResponse: true,
42
+ interceptChannel: chatCompletionsInterceptChannel,
60
43
  },
61
44
  {
62
45
  file: 'resources/completions',
@@ -72,6 +55,7 @@ const V4_PACKAGE_SHIMS = [
72
55
  methods: ['create'], // implicitly covers `parse` as well which calls `create` under the hood
73
56
  streamedResponse: true,
74
57
  versions: ['>=4.87.0'],
58
+ interceptChannel: responsesInterceptChannel,
75
59
  },
76
60
  {
77
61
  file: 'resources/embeddings',
@@ -185,6 +169,7 @@ const V4_PACKAGE_SHIMS = [
185
169
  methods: ['create'],
186
170
  streamedResponse: true,
187
171
  versions: ['>=4.85.0'],
172
+ interceptChannel: chatCompletionsInterceptChannel,
188
173
  },
189
174
  ]
190
175
 
@@ -287,13 +272,11 @@ for (const extension of extensions) {
287
272
  return handleUnwrappedAPIPromise(parsedPromise, state)
288
273
  })
289
274
 
275
+ // Raw-response callers bypass `parse`, so run the interceptor here too.
290
276
  shimmer.wrap(prototype, 'asResponse', asResponse => function (...args) {
291
277
  const state = responsePromiseContexts.get(this.responsePromise)
292
- const responsePromise = asResponse.apply(this, args)
293
-
294
- if (!state?.getBeforeVerdict) return responsePromise
295
278
 
296
- return Promise.all([state.getBeforeVerdict(), responsePromise]).then(([, response]) => response)
279
+ return heldUntil(asResponse.apply(this, args), state?.interceptCtx?.beforeResult?.())
297
280
  })
298
281
 
299
282
  return exports
@@ -303,7 +286,7 @@ for (const extension of extensions) {
303
286
 
304
287
  for (const extension of extensions) {
305
288
  for (const shim of V4_PACKAGE_SHIMS) {
306
- const { file, targetClass, baseResource, methods, versions, streamedResponse } = shim
289
+ const { file, targetClass, baseResource, methods, versions, streamedResponse, interceptChannel } = shim
307
290
  addHook({ name: 'openai', file: file + extension, versions: versions || ['>=4'] }, exports => {
308
291
  const targetPrototype = exports[targetClass].prototype
309
292
 
@@ -314,10 +297,9 @@ for (const extension of extensions) {
314
297
  // chat.completions and completions
315
298
  const stream = streamedResponse && getOption(args, 'stream', false)
316
299
 
317
- const channels = stream ? null : LIFECYCLE_CHANNELS[baseResource]
318
- const hasLifecycle = !!channels && (channels.before.hasSubscribers || channels.after.hasSubscribers)
300
+ const intercepted = !stream && interceptChannel?.hasSubscribers
319
301
 
320
- if (!ch.start.hasSubscribers && !hasLifecycle) {
302
+ if (!ch.start.hasSubscribers && !intercepted) {
321
303
  return methodFn.apply(this, args)
322
304
  }
323
305
 
@@ -330,24 +312,21 @@ for (const extension of extensions) {
330
312
  }
331
313
 
332
314
  return ch.start.runStores(ctx, () => {
333
- // Capture the parent span explicitly: the _thenUnwrap/parse path decouples
334
- // the lazy evaluation from the active scope at call time.
335
- const parentSpan = hasLifecycle ? ctx.currentStore?.span : undefined
336
-
337
315
  const apiProm = methodFn.apply(this, args)
338
316
 
339
- const beforeChannel = hasLifecycle && channels.before.hasSubscribers ? channels.before : null
340
- const afterChannel = hasLifecycle && channels.after.hasSubscribers ? channels.after : null
341
- let beforeVerdict
342
- const getBeforeVerdict = beforeChannel
343
- ? function getBeforeVerdict () {
344
- beforeVerdict ??= publishLifecycle(beforeChannel, { args, parentSpan })
345
- return beforeVerdict
346
- }
347
- : null
317
+ let interceptCtx
318
+ if (intercepted) {
319
+ interceptCtx = { arguments: args, tracingContext: ctx }
320
+ interceptChannel.publish(interceptCtx)
321
+ }
348
322
 
349
- const state = { ctx, stream, getBeforeVerdict, afterChannel, parentSpan }
350
- responsePromiseContexts.set(apiProm.responsePromise, state)
323
+ // Keyed by the response promise rather than the APIPromise: `_thenUnwrap` builds a new
324
+ // APIPromise around the same response, which is how `responses.parse` and the
325
+ // pagination page types reach the prototype wrappers above. A method that resolves to
326
+ // a plain value has none, and a WeakMap rejects a non-object key.
327
+ if (apiProm?.responsePromise) {
328
+ responsePromiseContexts.set(apiProm.responsePromise, { ctx, stream, interceptCtx })
329
+ }
351
330
 
352
331
  ch.end.publish(ctx)
353
332
 
@@ -361,20 +340,17 @@ for (const extension of extensions) {
361
340
  }
362
341
 
363
342
  function handleUnwrappedAPIPromise (apiProm, state) {
364
- const { ctx, stream, getBeforeVerdict, afterChannel, parentSpan } = state
365
- const gatedApiProm = getBeforeVerdict
366
- ? Promise.all([getBeforeVerdict(), apiProm]).then(([, result]) => result)
367
- : apiProm
343
+ const { ctx, stream, interceptCtx } = state
368
344
 
369
- return gatedApiProm
345
+ return heldUntil(apiProm, interceptCtx?.beforeResult?.())
370
346
  .then(([{ response, options }, body]) => {
371
347
  if (stream) {
348
+ const wrapIterator = wrapStreamIterator(response, options, ctx)
349
+
372
350
  if (body.iterator) {
373
- shimmer.wrap(body, 'iterator', wrapStreamIterator(response, options, ctx))
351
+ shimmer.wrap(body, 'iterator', wrapIterator)
374
352
  } else {
375
- shimmer.wrap(
376
- body.response.body, Symbol.asyncIterator, wrapStreamIterator(response, options, ctx)
377
- )
353
+ shimmer.wrap(body.response.body, Symbol.asyncIterator, wrapIterator)
378
354
  }
379
355
  return body
380
356
  }
@@ -388,28 +364,29 @@ function handleUnwrappedAPIPromise (apiProm, state) {
388
364
  },
389
365
  }
390
366
 
391
- if (!afterChannel) {
367
+ if (!interceptCtx?.onResult) {
392
368
  finish(ctx, responseData)
393
369
  return body
394
370
  }
395
371
 
396
- // Finish after evaluation so a block propagates the error to openai.request
397
- // and the span wraps its child instead of closing before it.
398
- return publishLifecycle(afterChannel, { args: ctx.args, body, parentSpan }).then(() => {
372
+ // Finish after the callback settles so a rejection propagates to openai.request and the
373
+ // span wraps its child instead of closing before it.
374
+ return Promise.resolve(interceptCtx.onResult(body)).then(deliveredBody => {
399
375
  finish(ctx, responseData)
400
- return body
376
+ return deliveredBody
401
377
  })
402
378
  })
403
379
  .catch(error => {
404
- // ctx.result is set inside finish(); if absent, finish never ran (sync throw in the success
405
- // branch, Before Model block, After Model block, or openai error) — record the error now so
406
- // the openai.request span is marked errored. If finish already ran, don't double-publish.
407
- if (!ctx.result) finish(ctx, undefined, error)
380
+ finish(ctx, undefined, error)
408
381
  throw error
409
382
  })
410
383
  }
411
384
 
412
385
  function finish (ctx, response, error) {
386
+ // `parse` is re-entered on every await of the same APIPromise, and a stream may be iterated
387
+ // more than once, so the span must only close once.
388
+ if (ctx.finished) return
389
+
413
390
  if (error) {
414
391
  ctx.error = error
415
392
  ch.error.publish(ctx)
@@ -419,6 +396,7 @@ function finish (ctx, response, error) {
419
396
  // so we don't want to override it here
420
397
  ctx.result ??= {}
421
398
  Object.assign(ctx.result, response)
399
+ ctx.finished = true
422
400
 
423
401
  ch.asyncEnd.publish(ctx)
424
402
  }
@@ -81,6 +81,7 @@ const testsToTestStatuses = new Map()
81
81
  const activeRumPages = new Set()
82
82
 
83
83
  const RUM_FLUSH_WAIT_TIME = getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')
84
+ const isPlaywrightWorker = getValueFromEnvSources('DD_PLAYWRIGHT_WORKER') === '1'
84
85
  const DD_PROPERTIES_TIMEOUT = 5000
85
86
  const isFailureScreenshotUploadEnabled =
86
87
  getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
@@ -1712,6 +1713,9 @@ createRootSuiteCh.subscribe({
1712
1713
 
1713
1714
  pageGotoCh.subscribe({
1714
1715
  asyncEnd (ctx) {
1716
+ // Playwright library consumers such as Vitest have no Playwright test span and may navigate during page startup.
1717
+ if (!isPlaywrightWorker) return
1718
+
1715
1719
  // The Page.goto rewriter waits for this so tests closing immediately after navigation still get RUM tags.
1716
1720
  const rumDetectionPromise = handlePageGoto(ctx.self)
1717
1721
  ctx.resolveCallback = onDone => rumDetectionPromise.then(onDone, onDone)