dd-trace 6.8.0 → 6.10.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 (209) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +140 -42
  44. package/index.d.ts +140 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +8 -5
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  55. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  56. package/packages/datadog-instrumentations/src/dns.js +21 -2
  57. package/packages/datadog-instrumentations/src/electron.js +1 -0
  58. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  59. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  60. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  61. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  62. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
  68. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  69. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  70. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  71. package/packages/datadog-instrumentations/src/jest.js +980 -234
  72. package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
  73. package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
  74. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  75. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  76. package/packages/datadog-instrumentations/src/mysql.js +46 -16
  77. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  78. package/packages/datadog-instrumentations/src/next.js +155 -23
  79. package/packages/datadog-instrumentations/src/openai.js +14 -0
  80. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  81. package/packages/datadog-instrumentations/src/pg.js +209 -11
  82. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  83. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  84. package/packages/datadog-instrumentations/src/promise.js +3 -3
  85. package/packages/datadog-instrumentations/src/router.js +195 -19
  86. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
  87. package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
  88. package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
  89. package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
  90. package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
  91. package/packages/datadog-instrumentations/src/when.js +3 -3
  92. package/packages/datadog-instrumentations/src/ws.js +5 -1
  93. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  94. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  95. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  97. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  98. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  99. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
  100. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  101. package/packages/datadog-plugin-electron/src/net.js +10 -4
  102. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  103. package/packages/datadog-plugin-http/src/client.js +1 -1
  104. package/packages/datadog-plugin-http2/src/client.js +1 -1
  105. package/packages/datadog-plugin-jest/src/index.js +6 -22
  106. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  107. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  108. package/packages/datadog-plugin-kafkajs/src/producer.js +5 -3
  109. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  110. package/packages/datadog-plugin-mocha/src/index.js +832 -4
  111. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  112. package/packages/datadog-plugin-next/src/index.js +51 -21
  113. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  114. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  115. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  116. package/packages/datadog-plugin-pg/src/index.js +66 -1
  117. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  118. package/packages/datadog-plugin-vitest/src/index.js +103 -27
  119. package/packages/datadog-plugin-ws/src/util.js +2 -1
  120. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  121. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  123. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  124. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  127. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  128. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  129. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  130. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  131. package/packages/dd-trace/src/appsec/index.js +48 -476
  132. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  133. package/packages/dd-trace/src/carrier.js +356 -0
  134. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  135. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  136. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  137. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  138. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  139. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  140. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  141. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  142. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  143. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  144. package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
  145. package/packages/dd-trace/src/config/helper.js +2 -0
  146. package/packages/dd-trace/src/config/index.js +16 -5
  147. package/packages/dd-trace/src/config/remote_config.js +1 -1
  148. package/packages/dd-trace/src/config/supported-configurations.json +18 -0
  149. package/packages/dd-trace/src/datastreams/context.js +6 -1
  150. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  151. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  152. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  153. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  154. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  155. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  156. package/packages/dd-trace/src/guardrails/log.js +1 -10
  157. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  158. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  159. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  160. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  161. package/packages/dd-trace/src/llmobs/index.js +34 -2
  162. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  163. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  165. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  166. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  167. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  168. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  169. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  170. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  171. package/packages/dd-trace/src/llmobs/util.js +78 -0
  172. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  173. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  174. package/packages/dd-trace/src/log/channels.js +1 -9
  175. package/packages/dd-trace/src/log/levels.js +12 -0
  176. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  177. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  179. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  180. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  181. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  182. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  183. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  184. package/packages/dd-trace/src/plugin_manager.js +2 -0
  185. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  186. package/packages/dd-trace/src/plugins/index.js +1 -0
  187. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  188. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  189. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  190. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  191. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
  192. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  193. package/packages/dd-trace/src/plugins/util/test.js +397 -74
  194. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  195. package/packages/dd-trace/src/plugins/util/web.js +76 -3
  196. package/packages/dd-trace/src/profiler.js +1 -1
  197. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  198. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  199. package/packages/dd-trace/src/proxy.js +1 -0
  200. package/packages/dd-trace/src/ritm.js +5 -0
  201. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  202. package/packages/dd-trace/src/sampling_rule.js +4 -2
  203. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  204. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  205. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  206. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  207. package/vendor/dist/pprof-format/index.js +1 -1
  208. package/version.js +10 -8
  209. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -21,6 +21,38 @@ class MySQLPlugin extends DatabasePlugin {
21
21
  })
22
22
 
23
23
  this.addBind(`apm:${this.component}:connection:finish`, ctx => ctx.currentStore)
24
+
25
+ // Explicit getConnection calls get an acquire span. Pool queries report their internal acquire
26
+ // on the query span instead, so each acquire is counted once.
27
+ this.addSub(`apm:${this.component}:pool:acquire:start`, ctx => {
28
+ const operation = `${this.component}.pool.acquire`
29
+
30
+ this.startSpan(operation, {
31
+ service: this.serviceName({ pluginConfig: this.config, dbConfig: ctx.conf, system: this.system }),
32
+ resource: operation,
33
+ startTime: ctx.startTime,
34
+ type: 'sql',
35
+ kind: 'client',
36
+ meta: {
37
+ 'db.type': this.system,
38
+ 'db.user': ctx.conf.user,
39
+ 'db.name': ctx.conf.database,
40
+ 'out.host': ctx.conf.host,
41
+ [CLIENT_PORT_KEY]: ctx.conf.port,
42
+ },
43
+ }, ctx)
44
+ })
45
+
46
+ this.addSub(`apm:${this.component}:pool:acquire:finish`, ctx => {
47
+ const span = ctx.currentStore?.span
48
+ if (span === undefined) return
49
+
50
+ if (ctx.error) {
51
+ this.addError(ctx.error, span)
52
+ }
53
+ span.setTag(`${this.component}.pool.wait_time`, ctx.poolWaitTime)
54
+ this.finish(ctx)
55
+ })
24
56
  }
25
57
 
26
58
  bindStart (ctx) {
@@ -38,6 +70,11 @@ class MySQLPlugin extends DatabasePlugin {
38
70
  [CLIENT_PORT_KEY]: ctx.conf.port,
39
71
  },
40
72
  }, ctx)
73
+
74
+ if (ctx.poolWaitTime !== undefined) {
75
+ span.setTag(`${this.component}.pool.wait_time`, ctx.poolWaitTime)
76
+ }
77
+
41
78
  ctx.sql = this.injectDbmQuery(span, ctx.sql, service.name)
42
79
 
43
80
  return ctx.currentStore
@@ -5,8 +5,11 @@ const { storage } = require('../../datadog-core')
5
5
  const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
6
6
  const { COMPONENT, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
7
7
  const web = require('../../dd-trace/src/plugins/util/web')
8
+ const { HTTP_ROUTE, RESOURCE_NAME } = require('../../../ext/tags')
8
9
 
9
10
  const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-found/page'])
11
+ const reusedNextRequestStores = new WeakSet()
12
+ const nextParentRoutes = new WeakMap()
10
13
 
11
14
  class NextPlugin extends ServerPlugin {
12
15
  static id = 'next'
@@ -18,7 +21,14 @@ class NextPlugin extends ServerPlugin {
18
21
 
19
22
  bindStart ({ req, res }) {
20
23
  const store = storage('legacy').getStore()
21
- const childOf = store ? store.span : store
24
+ const parentSpan = store?.span
25
+ if (parentSpan?._integrationName === this.constructor.id) {
26
+ const reusedStore = { ...store, span: parentSpan, req }
27
+ reusedNextRequestStores.add(reusedStore)
28
+ return reusedStore
29
+ }
30
+
31
+ const childOf = parentSpan || web.extractIncomingServerContext(this.tracer, req.headers)
22
32
  const { name: schemaServiceName, source: schemaServiceSource } = this.serviceName()
23
33
  const serviceName = this.config.service || schemaServiceName
24
34
  let serviceSource = this.config.service ? 'opt.plugin' : schemaServiceSource
@@ -42,7 +52,10 @@ class NextPlugin extends ServerPlugin {
42
52
 
43
53
  analyticsSampler.sample(span, this.config.measured, true)
44
54
 
45
- return { ...store, span, req }
55
+ const isHttpParent = parentSpan?._integrationName === 'http'
56
+ const httpParentSpan = isHttpParent ? parentSpan : undefined
57
+ const httpParentReq = isHttpParent ? web.getRequest(parentSpan) : undefined
58
+ return { ...store, span, req, httpParentSpan, httpParentReq }
46
59
  }
47
60
 
48
61
  error ({ span, error }) {
@@ -60,6 +73,7 @@ class NextPlugin extends ServerPlugin {
60
73
  const store = storage('legacy').getStore()
61
74
 
62
75
  if (!store) return
76
+ if (reusedNextRequestStores.has(store)) return
63
77
 
64
78
  const span = store.span
65
79
  const error = span.context().getTag('error')
@@ -88,12 +102,12 @@ class NextPlugin extends ServerPlugin {
88
102
  span.finish()
89
103
  }
90
104
 
91
- pageLoad ({ page, isAppPath = false, isStatic = false }) {
105
+ pageLoad ({ page, isAppPath = false, isStatic = false, isFilesystemPath = isAppPath }) {
92
106
  const store = storage('legacy').getStore()
93
107
 
94
108
  if (!store) return
95
109
 
96
- const { span, req } = store
110
+ const { span, req, httpParentReq, httpParentSpan } = store
97
111
 
98
112
  // safeguard against missing req in complicated timeout scenarios
99
113
  if (!req) return
@@ -106,10 +120,11 @@ class NextPlugin extends ServerPlugin {
106
120
  return
107
121
  }
108
122
 
109
- // remove ending /route or /page for appDir projects
110
- // need to check if not an error page too, as those are marked as app directory
111
- // in newer versions
112
- if (isAppPath && !isErrorPage) page = page.slice(0, Math.max(0, page.lastIndexOf('/')))
123
+ // Compiled runtimes may publish a normalized pathname rather than a filesystem path.
124
+ if (isFilesystemPath && !isErrorPage) {
125
+ if (isAppPath) page = normalizeAppPath(page)
126
+ page = normalizeIndexPage(page)
127
+ }
113
128
 
114
129
  // handle static resource
115
130
  if (isStatic) {
@@ -123,29 +138,44 @@ class NextPlugin extends ServerPlugin {
123
138
  'resource.name': `${req.method} ${page}`.trim(),
124
139
  'next.page': page,
125
140
  })
126
- web.setRoute(req, page)
141
+ const routeRequest = httpParentReq || req
142
+ web.setRouteOrEndpointTag(routeRequest)
143
+ setHttpParentRoute(httpParentSpan, req.method, page, isStatic)
144
+ web.setRoute(routeRequest, page)
127
145
  }
128
146
 
129
147
  configure (config) {
130
- return super.configure(normalizeConfig(config))
148
+ return super.configure(web.normalizeConfig(config))
131
149
  }
132
150
  }
133
151
 
134
- function normalizeConfig (config) {
135
- const hooks = getHooks(config)
136
- const validateStatus = typeof config.validateStatus === 'function'
137
- ? config.validateStatus
138
- : code => code < 500
139
-
140
- return { ...config, hooks, validateStatus }
152
+ function normalizeIndexPage (page) {
153
+ if (typeof page !== 'string') return page
154
+ if (page === '/index') return '/'
155
+ if (page.endsWith('/index')) return page.slice(0, -'/index'.length) || '/'
156
+ return page
141
157
  }
142
158
 
143
- const noop = () => {}
159
+ function normalizeAppPath (page) {
160
+ if (typeof page !== 'string') return page
144
161
 
145
- function getHooks (config) {
146
- const request = config.hooks?.request ?? noop
162
+ for (const suffix of ['/page', '/route']) {
163
+ if (page === suffix) return '/'
164
+ if (page.endsWith(suffix)) return page.slice(0, -suffix.length) || '/'
165
+ }
147
166
 
148
- return { request }
167
+ return page
149
168
  }
150
169
 
170
+ function setHttpParentRoute (span, method, page, isStatic) {
171
+ if (!span) return
172
+ const currentRoute = span.context().getTag(HTTP_ROUTE)
173
+
174
+ // Only refine a route that Next itself set; static resources are fallbacks.
175
+ if (currentRoute && (nextParentRoutes.get(span) !== currentRoute || isStatic)) return
176
+
177
+ span.setTag(HTTP_ROUTE, page)
178
+ span.setTag(RESOURCE_NAME, `${method} ${page}`.trim())
179
+ nextParentRoutes.set(span, page)
180
+ }
151
181
  module.exports = NextPlugin
@@ -627,6 +627,10 @@ function lookupOperationEndpoint (operationId, methodName, url) {
627
627
  * This function essentially normalizes the OpenAI method interface. Many methods accept
628
628
  * a single object argument. The remaining ones take individual arguments. This function
629
629
  * turns the individual arguments into an object to make extracting properties consistent.
630
+ *
631
+ * @param {string} methodName SDK method name, e.g. `createChatCompletion` or `chat.completions.create`
632
+ * @param {unknown[]} args arguments the SDK method was called with
633
+ * @returns {Record<string, unknown>}
630
634
  */
631
635
  function normalizeRequestPayload (methodName, args) {
632
636
  switch (methodName) {
@@ -15,6 +15,9 @@ const { AGENTS_ERROR_TYPE, applyError, deriveSpanName } = require('./util')
15
15
 
16
16
  const COMPONENT = 'openai-agents'
17
17
  const DEFAULT_MODEL_PROVIDER = 'openai'
18
+ // Bounds the agents-core parent-chain walk so a cyclic chain from a future
19
+ // agents-core version can't spin on the span-start hot path.
20
+ const MAX_ANCESTOR_WALK = 32
18
21
  const MODEL_BASE_URL_STORE_KEY = Symbol('openai-agents.model-base-url')
19
22
  const legacyStorage = storage('legacy')
20
23
 
@@ -37,9 +40,20 @@ const KIND_TO_SPAN_KIND = {
37
40
  * metadata?: Record<string, unknown>,
38
41
  * groupId?: string,
39
42
  * llmobsParentStore?: object,
43
+ * activeSpanCount: number,
44
+ * completionRequested: boolean,
40
45
  * }} LLMObsTraceInfo
41
46
  */
42
47
 
48
+ /**
49
+ * @typedef {{
50
+ * parentId: string | null,
51
+ * traceId?: string,
52
+ * activeChildCount: number,
53
+ * ended: boolean,
54
+ * }} UntracedSpanInfo
55
+ */
56
+
43
57
  /**
44
58
  * Owns tracer/tagger refs, maps agents-core span ids → dd-trace spans, and
45
59
  * reconstructs workflow-level input/output from the first and last response
@@ -59,6 +73,12 @@ class OpenAIAgentsIntegration {
59
73
  #tagger
60
74
  /** @type {Map<string, import('../../dd-trace/src/opentracing/span')>} */
61
75
  #oaiToDdSpan = new Map()
76
+ /**
77
+ * agents-core spans we deliberately don't trace (`task` / `turn`). Ended
78
+ * nodes stay walkable only while an observed child callback is still active.
79
+ * @type {Map<string, UntracedSpanInfo>}
80
+ */
81
+ #untracedSpans = new Map()
62
82
  /** @type {Map<string, LLMObsTraceInfo>} */
63
83
  #traceInfo = new Map()
64
84
 
@@ -83,11 +103,58 @@ class OpenAIAgentsIntegration {
83
103
  }
84
104
 
85
105
  /**
86
- * @param {string} spanId agents-core spanId
106
+ * Resolve the dd-trace span for an agents-core spanId. When the id belongs to
107
+ * a span type we don't trace — agents-core >=0.14 runs model calls inside a
108
+ * `turn` span — the nearest traced ancestor's span is returned so callers
109
+ * still activate the enclosing agent span.
110
+ *
111
+ * @param {string | undefined | null} spanId agents-core spanId
87
112
  * @returns {import('../../dd-trace/src/opentracing/span') | undefined}
88
113
  */
89
114
  getDDSpan (spanId) {
90
- return this.#oaiToDdSpan.get(spanId)
115
+ return this.#oaiToDdSpan.get(this.#nearestTracedAncestorId(spanId))
116
+ }
117
+
118
+ /**
119
+ * Remember an untraced agents-core span's parent so the chain stays walkable.
120
+ *
121
+ * @param {object} oaiSpan
122
+ */
123
+ recordUntracedSpan (oaiSpan) {
124
+ const { spanId } = oaiSpan
125
+ if (!spanId || this.#untracedSpans.has(spanId)) return
126
+
127
+ const parentId = oaiSpan.parentId ?? null
128
+ this.#untracedSpans.set(spanId, {
129
+ parentId,
130
+ traceId: oaiSpan.traceId,
131
+ activeChildCount: 0,
132
+ ended: false,
133
+ })
134
+ this.#retainUntracedParent(parentId)
135
+ this.#spanStarted(oaiSpan.traceId)
136
+ }
137
+
138
+ /**
139
+ * Mark an untraced span callback complete. Its ancestry remains available
140
+ * until every observed child callback has completed.
141
+ *
142
+ * @param {object} oaiSpan
143
+ */
144
+ endUntracedSpan (oaiSpan) {
145
+ const info = this.#untracedSpans.get(oaiSpan.spanId)
146
+ if (!info || info.ended) return
147
+
148
+ // agents-core does not end its trace when the run throws. In 0.14 the
149
+ // failure can live on the root task before any agent span exists, making
150
+ // this structural callback the only opportunity to finish the workflow.
151
+ if (oaiSpan.error && this.#isTopLevelSpan(oaiSpan)) {
152
+ this.#requestWorkflowCompletion(oaiSpan.traceId, oaiSpan)
153
+ }
154
+
155
+ info.ended = true
156
+ this.#pruneUntracedSpan(oaiSpan.spanId, info)
157
+ this.#spanEnded(info.traceId)
91
158
  }
92
159
 
93
160
  /**
@@ -114,6 +181,7 @@ class OpenAIAgentsIntegration {
114
181
  ddSpan.finish()
115
182
  }
116
183
  this.#oaiToDdSpan.clear()
184
+ this.#untracedSpans.clear()
117
185
  this.#traceInfo.clear()
118
186
  }
119
187
 
@@ -138,6 +206,8 @@ class OpenAIAgentsIntegration {
138
206
  groupId: oaiTrace.groupId || undefined,
139
207
  metadata: oaiTrace.metadata,
140
208
  llmobsParentStore,
209
+ activeSpanCount: 0,
210
+ completionRequested: false,
141
211
  })
142
212
 
143
213
  this.#tagger.registerLLMObsSpan(ddSpan, {
@@ -153,38 +223,55 @@ class OpenAIAgentsIntegration {
153
223
  }
154
224
 
155
225
  endTrace (oaiTrace) {
156
- this.#completeWorkflowSpan(oaiTrace.traceId)
226
+ this.#requestWorkflowCompletion(oaiTrace.traceId)
157
227
  }
158
228
 
159
229
  /**
160
- * Finish the workflow dd-trace span and clear its bookkeeping. Used by both
161
- * agents-core's normal `Trace.end()` path and the orphan-recovery path
162
- * (when `withTrace` skips its end callback because the body threw). When
163
- * `rootAgentSpan` is provided, its `error` field is reflected onto the
164
- * workflow span before finishing.
230
+ * Request workflow completion after all observed span callbacks finish.
231
+ * Error tags are applied immediately so the agents-core span does not need
232
+ * to be retained while completion is pending.
165
233
  *
166
234
  * @param {string | undefined} traceId
167
- * @param {object} [rootAgentSpan] - parentless oai-span that ended in error.
235
+ * @param {object} [rootSpan] - top-level oai-span that ended in error.
168
236
  */
169
- #completeWorkflowSpan (traceId, rootAgentSpan) {
237
+ #requestWorkflowCompletion (traceId, rootSpan) {
170
238
  if (!traceId) return
171
239
  const ddSpan = this.#oaiToDdSpan.get(traceId)
172
240
  if (!ddSpan) return
173
241
  const info = this.#traceInfo.get(traceId)
174
242
 
175
- if (rootAgentSpan?.error) {
243
+ if (rootSpan?.error) {
176
244
  ddSpan.setTag('error', true)
177
245
  ddSpan.setTag('error.type', AGENTS_ERROR_TYPE)
178
- if (rootAgentSpan.error.message) {
179
- ddSpan.setTag('error.message', rootAgentSpan.error.message)
246
+ if (rootSpan.error.message) {
247
+ ddSpan.setTag('error.message', rootSpan.error.message)
248
+ }
249
+ }
250
+
251
+ if (info) {
252
+ if (!info.completionRequested) {
253
+ info.completionRequested = true
254
+ if (LLMObsTagger.tagMap.has(ddSpan)) {
255
+ llmobsStorage.enterWith(info.llmobsParentStore)
256
+ }
180
257
  }
258
+ if (info.activeSpanCount > 0) return
181
259
  }
182
260
 
261
+ this.#completeWorkflowSpan(traceId)
262
+ }
263
+
264
+ /**
265
+ * Finish the workflow dd-trace span and clear its bookkeeping.
266
+ *
267
+ * @param {string} traceId
268
+ */
269
+ #completeWorkflowSpan (traceId) {
270
+ const ddSpan = this.#oaiToDdSpan.get(traceId)
271
+ if (!ddSpan) return
272
+
183
273
  if (this.#isLLMObsEnabled()) this.#setTraceAttributes(ddSpan, traceId)
184
274
  ddSpan.finish()
185
- if (info && LLMObsTagger.tagMap.has(ddSpan)) {
186
- llmobsStorage.enterWith(info.llmobsParentStore)
187
- }
188
275
  this.#oaiToDdSpan.delete(traceId)
189
276
  this.#traceInfo.delete(traceId)
190
277
  }
@@ -204,6 +291,8 @@ class OpenAIAgentsIntegration {
204
291
  })
205
292
 
206
293
  this.#oaiToDdSpan.set(spanId, ddSpan)
294
+ this.#retainUntracedParent(oaiSpan.parentId)
295
+ this.#spanStarted(oaiSpan.traceId)
207
296
 
208
297
  if (this.#isLLMObsEnabled()) {
209
298
  const llmobsOptions = {
@@ -230,49 +319,57 @@ class OpenAIAgentsIntegration {
230
319
  const ddSpan = this.#oaiToDdSpan.get(spanId)
231
320
  if (!ddSpan) return
232
321
 
233
- applyError(ddSpan, oaiSpan)
322
+ try {
323
+ applyError(ddSpan, oaiSpan)
234
324
 
235
- if (oaiSpan.spanData?.type === 'handoff') {
236
- const spanName = deriveSpanName(oaiSpan)
237
- ddSpan.setOperationName(spanName)
238
- if (this.#isLLMObsEnabled()) this.#tagger.setName(ddSpan, spanName)
239
- }
325
+ if (oaiSpan.spanData?.type === 'handoff') {
326
+ const spanName = deriveSpanName(oaiSpan)
327
+ ddSpan.setOperationName(spanName)
328
+ if (this.#isLLMObsEnabled()) this.#tagger.setName(ddSpan, spanName)
329
+ }
240
330
 
241
- if (this.#isLLMObsEnabled()) {
242
- const spanData = oaiSpan.spanData
243
- switch (spanData?.type) {
244
- case 'response':
245
- this.#setResponseAttributes(ddSpan, oaiSpan)
246
- this.#updateTraceInfoOutput(oaiSpan)
247
- break
248
- case 'generation':
249
- this.#setGenerationAttributes(ddSpan, oaiSpan)
250
- this.#updateTraceInfoOutput(oaiSpan)
251
- break
252
- case 'function':
253
- this.#tagger.tagTextIO(ddSpan, spanData.input ?? '', spanData.output ?? '')
254
- break
255
- case 'handoff':
256
- this.#tagger.tagTextIO(ddSpan, spanData.from_agent ?? '', spanData.to_agent ?? '')
257
- break
258
- case 'agent':
259
- this.#setAgentAttributes(ddSpan, oaiSpan)
260
- break
261
- case 'custom':
262
- if (spanData.data && typeof spanData.data === 'object') {
263
- this.#tagger.tagMetadata(ddSpan, spanData.data)
264
- }
265
- break
331
+ if (this.#isLLMObsEnabled()) {
332
+ const spanData = oaiSpan.spanData
333
+ switch (spanData?.type) {
334
+ case 'response':
335
+ this.#setResponseAttributes(ddSpan, oaiSpan)
336
+ this.#updateTraceInfoOutput(oaiSpan)
337
+ break
338
+ case 'generation':
339
+ this.#setGenerationAttributes(ddSpan, oaiSpan)
340
+ this.#updateTraceInfoOutput(oaiSpan)
341
+ break
342
+ case 'function':
343
+ this.#tagger.tagTextIO(ddSpan, spanData.input ?? '', spanData.output ?? '')
344
+ break
345
+ case 'handoff':
346
+ this.#tagger.tagTextIO(ddSpan, spanData.from_agent ?? '', spanData.to_agent ?? '')
347
+ break
348
+ case 'agent':
349
+ this.#setAgentAttributes(ddSpan, oaiSpan)
350
+ break
351
+ case 'custom':
352
+ if (spanData.data && typeof spanData.data === 'object') {
353
+ this.#tagger.tagMetadata(ddSpan, spanData.data)
354
+ }
355
+ break
356
+ }
266
357
  }
267
- }
268
358
 
269
- ddSpan.finish()
270
- // agents-core's withTrace skips Trace.end() when its callback throws, so an
271
- // errored parentless span is our last chance to finalize the workflow.
272
- if (oaiSpan.parentId == null && oaiSpan.error) {
273
- this.#completeWorkflowSpan(oaiSpan.traceId, oaiSpan)
359
+ // agents-core's withTrace skips Trace.end() when its callback throws, so
360
+ // an errored top-level span is our last chance to finalize the workflow.
361
+ if (oaiSpan.error && this.#isTopLevelSpan(oaiSpan)) {
362
+ this.#requestWorkflowCompletion(oaiSpan.traceId, oaiSpan)
363
+ }
364
+ } finally {
365
+ try {
366
+ ddSpan.finish()
367
+ } finally {
368
+ this.#oaiToDdSpan.delete(spanId)
369
+ this.#releaseUntracedParent(oaiSpan.parentId)
370
+ this.#spanEnded(oaiSpan.traceId)
371
+ }
274
372
  }
275
- this.#oaiToDdSpan.delete(spanId)
276
373
  }
277
374
 
278
375
  // ── Per-type attribute setters ──────────────────────────────────────────────
@@ -360,7 +457,7 @@ class OpenAIAgentsIntegration {
360
457
  #llmSpanParentAgentName (oaiSpan) {
361
458
  const traceInfo = this.#traceInfo.get(oaiSpan.traceId)
362
459
  if (!traceInfo?.currentTopLevelAgentSpanId) return
363
- if (oaiSpan.parentId !== traceInfo.currentTopLevelAgentSpanId) return
460
+ if (!this.#hasUntracedPathToAncestor(oaiSpan.parentId, traceInfo.currentTopLevelAgentSpanId)) return
364
461
  return traceInfo.currentTopLevelAgentName
365
462
  }
366
463
 
@@ -412,13 +509,12 @@ class OpenAIAgentsIntegration {
412
509
  const info = this.#traceInfo.get(oaiSpan.traceId)
413
510
  if (!info) return
414
511
 
415
- const parentId = oaiSpan.parentId
416
512
  const type = oaiSpan.spanData?.type
417
513
 
418
514
  // Identify the first top-level agent span under the root trace and
419
515
  // stash its display name so `${agentName} (LLM)` doesn't have to read
420
516
  // the dd-trace span context's private fields later.
421
- if (type === 'agent' && parentId == null) {
517
+ if (type === 'agent' && this.#isTopLevelSpan(oaiSpan)) {
422
518
  info.currentTopLevelAgentSpanId = oaiSpan.spanId
423
519
  info.currentTopLevelAgentName = spanName
424
520
  }
@@ -427,9 +523,9 @@ class OpenAIAgentsIntegration {
427
523
  // as the workflow-level input source.
428
524
  if (
429
525
  (type === 'response' || type === 'generation') &&
430
- parentId &&
526
+ info.currentTopLevelAgentSpanId &&
431
527
  !info.inputOaiSpan &&
432
- parentId === info.currentTopLevelAgentSpanId
528
+ this.#hasUntracedPathToAncestor(oaiSpan.parentId, info.currentTopLevelAgentSpanId)
433
529
  ) {
434
530
  info.inputOaiSpan = oaiSpan
435
531
  }
@@ -440,9 +536,8 @@ class OpenAIAgentsIntegration {
440
536
  if (!info) return
441
537
 
442
538
  if (
443
- oaiSpan.parentId &&
444
539
  info.currentTopLevelAgentSpanId &&
445
- oaiSpan.parentId === info.currentTopLevelAgentSpanId
540
+ this.#hasUntracedPathToAncestor(oaiSpan.parentId, info.currentTopLevelAgentSpanId)
446
541
  ) {
447
542
  info.outputOaiSpan = oaiSpan
448
543
  }
@@ -451,18 +546,132 @@ class OpenAIAgentsIntegration {
451
546
  // ── Helpers ─────────────────────────────────────────────────────────────────
452
547
 
453
548
  #resolveParent (oaiSpan) {
454
- const parentId = oaiSpan.parentId
455
- const traceId = oaiSpan.traceId
456
- if (parentId) {
457
- const parent = this.#oaiToDdSpan.get(parentId)
458
- if (parent) return parent
549
+ return this.getDDSpan(oaiSpan.parentId) ?? this.#oaiToDdSpan.get(oaiSpan.traceId)
550
+ }
551
+
552
+ /**
553
+ * Walk the agents-core parent chain from `spanId`, skipping spans we don't
554
+ * trace, and return the first id that maps to a dd-trace span. Returns
555
+ * `undefined` when the walk reaches the trace root (or a span we never saw),
556
+ * which callers read as "no traced ancestor".
557
+ *
558
+ * @param {string | undefined | null} spanId agents-core spanId to start from
559
+ * @returns {string | undefined}
560
+ */
561
+ #nearestTracedAncestorId (spanId) {
562
+ let currentId = spanId
563
+ for (let depth = 0; currentId != null && depth < MAX_ANCESTOR_WALK; depth++) {
564
+ if (this.#oaiToDdSpan.has(currentId)) return currentId
565
+ currentId = this.#untracedSpans.get(currentId)?.parentId
459
566
  }
460
- if (traceId) {
461
- const root = this.#oaiToDdSpan.get(traceId)
462
- if (root) return root
567
+ }
568
+
569
+ /**
570
+ * True when `spanId` reaches `ancestorId` through only untraced structural
571
+ * spans. Unlike #nearestTracedAncestorId, this still recognizes an ancestor
572
+ * whose dd-trace span has already finished and been removed from the live map.
573
+ *
574
+ * @param {string | undefined | null} spanId agents-core spanId to start from
575
+ * @param {string} ancestorId agents-core ancestor spanId
576
+ * @returns {boolean}
577
+ */
578
+ #hasUntracedPathToAncestor (spanId, ancestorId) {
579
+ let currentId = spanId
580
+ for (let depth = 0; currentId != null && depth < MAX_ANCESTOR_WALK; depth++) {
581
+ if (currentId === ancestorId) return true
582
+ currentId = this.#untracedSpans.get(currentId)?.parentId
583
+ }
584
+ return false
585
+ }
586
+
587
+ /**
588
+ * True when nothing but untraced spans sits between this span and the trace
589
+ * root. Stricter than `#nearestTracedAncestorId(…) === undefined`, which also
590
+ * answers "undefined" for a parent we simply never saw — that must not be
591
+ * mistaken for top-level, or an error would finalize the workflow span early.
592
+ *
593
+ * @param {object} oaiSpan
594
+ * @returns {boolean}
595
+ */
596
+ #isTopLevelSpan (oaiSpan) {
597
+ let currentId = oaiSpan.parentId
598
+ for (let depth = 0; depth < MAX_ANCESTOR_WALK; depth++) {
599
+ if (currentId == null) return true
600
+ const info = this.#untracedSpans.get(currentId)
601
+ if (!info) return false
602
+ currentId = info.parentId
603
+ }
604
+ return false
605
+ }
606
+
607
+ /**
608
+ * Keep a structural parent alive until this child callback completes.
609
+ *
610
+ * @param {string | undefined | null} parentId
611
+ */
612
+ #retainUntracedParent (parentId) {
613
+ const info = this.#untracedSpans.get(parentId)
614
+ if (info) info.activeChildCount++
615
+ }
616
+
617
+ /**
618
+ * Release a structural parent and iteratively prune any ended, childless
619
+ * ancestors. Each iteration deletes one node, so even a cyclic parent chain
620
+ * terminates when it reaches the first node it already removed.
621
+ *
622
+ * @param {string | undefined | null} parentId
623
+ */
624
+ #releaseUntracedParent (parentId) {
625
+ let currentId = parentId
626
+ while (currentId != null) {
627
+ const info = this.#untracedSpans.get(currentId)
628
+ if (!info || info.activeChildCount === 0) return
629
+
630
+ info.activeChildCount--
631
+ if (!info.ended || info.activeChildCount > 0) return
632
+
633
+ this.#untracedSpans.delete(currentId)
634
+ currentId = info.parentId
463
635
  }
464
636
  }
465
637
 
638
+ /**
639
+ * Remove an ended structural span once no observed child still needs it.
640
+ *
641
+ * @param {string} spanId
642
+ * @param {UntracedSpanInfo} info
643
+ */
644
+ #pruneUntracedSpan (spanId, info) {
645
+ if (info.activeChildCount > 0) return
646
+ this.#untracedSpans.delete(spanId)
647
+ this.#releaseUntracedParent(info.parentId)
648
+ }
649
+
650
+ /**
651
+ * Count a span callback whose end may arrive after trace completion.
652
+ *
653
+ * @param {string | undefined} traceId
654
+ */
655
+ #spanStarted (traceId) {
656
+ const info = this.#traceInfo.get(traceId)
657
+ if (info) info.activeSpanCount++
658
+ }
659
+
660
+ /**
661
+ * Release one observed span callback and complete the trace when it becomes
662
+ * quiescent. Structural ancestry prunes independently with each subtree.
663
+ *
664
+ * @param {string | undefined} traceId
665
+ */
666
+ #spanEnded (traceId) {
667
+ const info = this.#traceInfo.get(traceId)
668
+ if (!info) return
669
+ if (info.activeSpanCount > 0) info.activeSpanCount--
670
+ if (info.activeSpanCount > 0) return
671
+
672
+ if (info.completionRequested) this.#completeWorkflowSpan(traceId)
673
+ }
674
+
466
675
  /**
467
676
  * Read the mutable tracer configuration so remote/runtime enablement is
468
677
  * reflected without reconstructing the plugin.