dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -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
 
@@ -401,6 +498,7 @@ class OpenAIAgentsIntegration {
401
498
 
402
499
  this.#tagger.tagTextIO(ddSpan, inputValue, outputValue)
403
500
 
501
+ // eslint-disable-next-line no-restricted-syntax -- agents-core builds metadata before the plugin receives it
404
502
  if (info.metadata && Object.keys(info.metadata).length > 0) {
405
503
  this.#tagger.tagMetadata(ddSpan, info.metadata)
406
504
  }
@@ -412,13 +510,12 @@ class OpenAIAgentsIntegration {
412
510
  const info = this.#traceInfo.get(oaiSpan.traceId)
413
511
  if (!info) return
414
512
 
415
- const parentId = oaiSpan.parentId
416
513
  const type = oaiSpan.spanData?.type
417
514
 
418
515
  // Identify the first top-level agent span under the root trace and
419
516
  // stash its display name so `${agentName} (LLM)` doesn't have to read
420
517
  // the dd-trace span context's private fields later.
421
- if (type === 'agent' && parentId == null) {
518
+ if (type === 'agent' && this.#isTopLevelSpan(oaiSpan)) {
422
519
  info.currentTopLevelAgentSpanId = oaiSpan.spanId
423
520
  info.currentTopLevelAgentName = spanName
424
521
  }
@@ -427,9 +524,9 @@ class OpenAIAgentsIntegration {
427
524
  // as the workflow-level input source.
428
525
  if (
429
526
  (type === 'response' || type === 'generation') &&
430
- parentId &&
527
+ info.currentTopLevelAgentSpanId &&
431
528
  !info.inputOaiSpan &&
432
- parentId === info.currentTopLevelAgentSpanId
529
+ this.#hasUntracedPathToAncestor(oaiSpan.parentId, info.currentTopLevelAgentSpanId)
433
530
  ) {
434
531
  info.inputOaiSpan = oaiSpan
435
532
  }
@@ -440,9 +537,8 @@ class OpenAIAgentsIntegration {
440
537
  if (!info) return
441
538
 
442
539
  if (
443
- oaiSpan.parentId &&
444
540
  info.currentTopLevelAgentSpanId &&
445
- oaiSpan.parentId === info.currentTopLevelAgentSpanId
541
+ this.#hasUntracedPathToAncestor(oaiSpan.parentId, info.currentTopLevelAgentSpanId)
446
542
  ) {
447
543
  info.outputOaiSpan = oaiSpan
448
544
  }
@@ -451,18 +547,132 @@ class OpenAIAgentsIntegration {
451
547
  // ── Helpers ─────────────────────────────────────────────────────────────────
452
548
 
453
549
  #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
550
+ return this.getDDSpan(oaiSpan.parentId) ?? this.#oaiToDdSpan.get(oaiSpan.traceId)
551
+ }
552
+
553
+ /**
554
+ * Walk the agents-core parent chain from `spanId`, skipping spans we don't
555
+ * trace, and return the first id that maps to a dd-trace span. Returns
556
+ * `undefined` when the walk reaches the trace root (or a span we never saw),
557
+ * which callers read as "no traced ancestor".
558
+ *
559
+ * @param {string | undefined | null} spanId agents-core spanId to start from
560
+ * @returns {string | undefined}
561
+ */
562
+ #nearestTracedAncestorId (spanId) {
563
+ let currentId = spanId
564
+ for (let depth = 0; currentId != null && depth < MAX_ANCESTOR_WALK; depth++) {
565
+ if (this.#oaiToDdSpan.has(currentId)) return currentId
566
+ currentId = this.#untracedSpans.get(currentId)?.parentId
459
567
  }
460
- if (traceId) {
461
- const root = this.#oaiToDdSpan.get(traceId)
462
- if (root) return root
568
+ }
569
+
570
+ /**
571
+ * True when `spanId` reaches `ancestorId` through only untraced structural
572
+ * spans. Unlike #nearestTracedAncestorId, this still recognizes an ancestor
573
+ * whose dd-trace span has already finished and been removed from the live map.
574
+ *
575
+ * @param {string | undefined | null} spanId agents-core spanId to start from
576
+ * @param {string} ancestorId agents-core ancestor spanId
577
+ * @returns {boolean}
578
+ */
579
+ #hasUntracedPathToAncestor (spanId, ancestorId) {
580
+ let currentId = spanId
581
+ for (let depth = 0; currentId != null && depth < MAX_ANCESTOR_WALK; depth++) {
582
+ if (currentId === ancestorId) return true
583
+ currentId = this.#untracedSpans.get(currentId)?.parentId
584
+ }
585
+ return false
586
+ }
587
+
588
+ /**
589
+ * True when nothing but untraced spans sits between this span and the trace
590
+ * root. Stricter than `#nearestTracedAncestorId(…) === undefined`, which also
591
+ * answers "undefined" for a parent we simply never saw — that must not be
592
+ * mistaken for top-level, or an error would finalize the workflow span early.
593
+ *
594
+ * @param {object} oaiSpan
595
+ * @returns {boolean}
596
+ */
597
+ #isTopLevelSpan (oaiSpan) {
598
+ let currentId = oaiSpan.parentId
599
+ for (let depth = 0; depth < MAX_ANCESTOR_WALK; depth++) {
600
+ if (currentId == null) return true
601
+ const info = this.#untracedSpans.get(currentId)
602
+ if (!info) return false
603
+ currentId = info.parentId
604
+ }
605
+ return false
606
+ }
607
+
608
+ /**
609
+ * Keep a structural parent alive until this child callback completes.
610
+ *
611
+ * @param {string | undefined | null} parentId
612
+ */
613
+ #retainUntracedParent (parentId) {
614
+ const info = this.#untracedSpans.get(parentId)
615
+ if (info) info.activeChildCount++
616
+ }
617
+
618
+ /**
619
+ * Release a structural parent and iteratively prune any ended, childless
620
+ * ancestors. Each iteration deletes one node, so even a cyclic parent chain
621
+ * terminates when it reaches the first node it already removed.
622
+ *
623
+ * @param {string | undefined | null} parentId
624
+ */
625
+ #releaseUntracedParent (parentId) {
626
+ let currentId = parentId
627
+ while (currentId != null) {
628
+ const info = this.#untracedSpans.get(currentId)
629
+ if (!info || info.activeChildCount === 0) return
630
+
631
+ info.activeChildCount--
632
+ if (!info.ended || info.activeChildCount > 0) return
633
+
634
+ this.#untracedSpans.delete(currentId)
635
+ currentId = info.parentId
463
636
  }
464
637
  }
465
638
 
639
+ /**
640
+ * Remove an ended structural span once no observed child still needs it.
641
+ *
642
+ * @param {string} spanId
643
+ * @param {UntracedSpanInfo} info
644
+ */
645
+ #pruneUntracedSpan (spanId, info) {
646
+ if (info.activeChildCount > 0) return
647
+ this.#untracedSpans.delete(spanId)
648
+ this.#releaseUntracedParent(info.parentId)
649
+ }
650
+
651
+ /**
652
+ * Count a span callback whose end may arrive after trace completion.
653
+ *
654
+ * @param {string | undefined} traceId
655
+ */
656
+ #spanStarted (traceId) {
657
+ const info = this.#traceInfo.get(traceId)
658
+ if (info) info.activeSpanCount++
659
+ }
660
+
661
+ /**
662
+ * Release one observed span callback and complete the trace when it becomes
663
+ * quiescent. Structural ancestry prunes independently with each subtree.
664
+ *
665
+ * @param {string | undefined} traceId
666
+ */
667
+ #spanEnded (traceId) {
668
+ const info = this.#traceInfo.get(traceId)
669
+ if (!info) return
670
+ if (info.activeSpanCount > 0) info.activeSpanCount--
671
+ if (info.activeSpanCount > 0) return
672
+
673
+ if (info.completionRequested) this.#completeWorkflowSpan(traceId)
674
+ }
675
+
466
676
  /**
467
677
  * Read the mutable tracer configuration so remote/runtime enablement is
468
678
  * reflected without reconstructing the plugin.
@@ -66,10 +66,16 @@ class DDOpenAIAgentsProcessor {
66
66
  const integration = this._getIntegration()
67
67
  if (!integration?.enabled) return RESOLVED
68
68
  if (!oaiSpan?.spanData) return RESOLVED // guard NoopSpan
69
- const kind = SPAN_KIND_BY_TYPE[oaiSpan.spanData.type]
70
- if (!kind) return RESOLVED // span types without an LLMObs kind are not traced
69
+ const type = oaiSpan.spanData.type
70
+ const kind = SPAN_KIND_BY_TYPE[type]
71
71
  try {
72
- integration.startSpan(oaiSpan, kind)
72
+ if (kind) {
73
+ integration.startSpan(oaiSpan, kind)
74
+ } else if (type === 'task' || type === 'turn') {
75
+ // agents-core >=0.14 nests real work under these structural spans, so
76
+ // their ancestry still has to be walkable for parent resolution.
77
+ integration.recordUntracedSpan(oaiSpan)
78
+ }
73
79
  } catch (err) {
74
80
  log.warn('[openai-agents] onSpanStart failed: %s', err)
75
81
  }
@@ -80,8 +86,14 @@ class DDOpenAIAgentsProcessor {
80
86
  const integration = this._getIntegration()
81
87
  if (!integration?.enabled) return RESOLVED
82
88
  if (!oaiSpan?.spanData) return RESOLVED
89
+ const type = oaiSpan.spanData.type
90
+ const kind = SPAN_KIND_BY_TYPE[type]
83
91
  try {
84
- integration.endSpan(oaiSpan)
92
+ if (kind) {
93
+ integration.endSpan(oaiSpan)
94
+ } else if (type === 'task' || type === 'turn') {
95
+ integration.endUntracedSpan(oaiSpan)
96
+ }
85
97
  } catch (err) {
86
98
  log.warn('[openai-agents] onSpanEnd failed: %s', err)
87
99
  }
@@ -29,6 +29,7 @@ class PGPlugin extends DatabasePlugin {
29
29
  this.startSpan(operationName, {
30
30
  service,
31
31
  resource: operationName,
32
+ startTime: ctx.startTime,
32
33
  type: 'sql',
33
34
  kind: 'client',
34
35
  meta: {
@@ -79,6 +79,9 @@ function isPlaywrightFailureScreenshot (attachment) {
79
79
  class PlaywrightPlugin extends CiPlugin {
80
80
  static id = 'playwright'
81
81
 
82
+ #isFinalizingAfterError = false
83
+ #pendingTestFinishCallbacks = new Map()
84
+
82
85
  constructor (...args) {
83
86
  super(...args)
84
87
 
@@ -99,6 +102,7 @@ class PlaywrightPlugin extends CiPlugin {
99
102
  })
100
103
 
101
104
  this.addSub('ci:playwright:session:start', ({ isFailureScreenshotEnabled }) => {
105
+ this.#isFinalizingAfterError = false
102
106
  if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED) return
103
107
 
104
108
  if (!isFailureScreenshotEnabled) {
@@ -127,8 +131,21 @@ class PlaywrightPlugin extends CiPlugin {
127
131
  isEarlyFlakeDetectionEnabled,
128
132
  isEarlyFlakeDetectionFaulty,
129
133
  isTestManagementTestsEnabled,
134
+ error,
130
135
  onDone,
131
136
  }) => {
137
+ if (error) {
138
+ this.#isFinalizingAfterError = true
139
+ for (const testSuiteSpan of this._testSuiteSpansByTestSuiteAbsolutePath.values()) {
140
+ testSuiteSpan.setTag(TEST_STATUS, 'fail')
141
+ testSuiteSpan.setTag('error', error)
142
+ }
143
+ for (const [finishTest, abortController] of this.#pendingTestFinishCallbacks) {
144
+ finishTest(SCREENSHOT_UPLOAD_RESULT_ERROR)
145
+ abortController.abort()
146
+ }
147
+ }
148
+
132
149
  const finishSession = () => {
133
150
  this.testModuleSpan.setTag(TEST_STATUS, status)
134
151
  this.testSessionSpan.setTag(TEST_STATUS, status)
@@ -139,20 +156,24 @@ class PlaywrightPlugin extends CiPlugin {
139
156
  if (isEarlyFlakeDetectionFaulty) {
140
157
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
141
158
  }
142
- if (status === 'fail' && this.numFailedSuites > 0) {
159
+ let sessionError = error
160
+ if (!sessionError && status === 'fail' && this.numFailedSuites > 0) {
143
161
  let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
144
162
  if (this.numFailedTests > 0) {
145
163
  errorMessage += ` Tests failed: ${this.numFailedTests}`
146
164
  }
147
- const error = new Error(errorMessage)
148
- this.testModuleSpan.setTag('error', error)
149
- this.testSessionSpan.setTag('error', error)
165
+ sessionError = new Error(errorMessage)
166
+ }
167
+ if (sessionError) {
168
+ this.testModuleSpan.setTag('error', sessionError)
169
+ this.testSessionSpan.setTag('error', sessionError)
150
170
  }
151
171
 
152
172
  if (isTestManagementTestsEnabled) {
153
173
  this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
154
174
  }
155
175
 
176
+ this.tracer._exporter.exportDeferredTestSuiteSpans?.()
156
177
  this.testModuleSpan.finish()
157
178
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
158
179
  this.testSessionSpan.finish()
@@ -234,6 +255,7 @@ class PlaywrightPlugin extends CiPlugin {
234
255
  this.numFailedSuites++
235
256
  }
236
257
 
258
+ this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
237
259
  testSuiteSpan.finish()
238
260
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
239
261
  })
@@ -304,16 +326,16 @@ class PlaywrightPlugin extends CiPlugin {
304
326
  }
305
327
  }
306
328
 
307
- if (!formattedTestSpan || !screenshots) {
329
+ if (!formattedTestSpan || !screenshots || this.#isFinalizingAfterError) {
308
330
  exportTraces()
309
331
  return
310
332
  }
311
333
 
312
334
  this.pendingTestFinishes++
313
- const uploadStarted = this.uploadTestScreenshots({
314
- screenshots,
315
- traceId: formattedTestSpan.trace_id.toString(10),
316
- }, (screenshotUploadResult) => {
335
+ const abortController = new AbortController()
336
+ const finishTest = (screenshotUploadResult) => {
337
+ if (!this.#pendingTestFinishCallbacks.delete(finishTest)) return
338
+
317
339
  const screenshotUploadTag = getScreenshotUploadTag(screenshotUploadResult)
318
340
  if (screenshotUploadTag) {
319
341
  formattedTestSpan.meta[screenshotUploadTag] = 'true'
@@ -323,11 +345,16 @@ class PlaywrightPlugin extends CiPlugin {
323
345
  if (this.pendingTestFinishes === 0 && this.finishSession) {
324
346
  this.finishSession()
325
347
  }
326
- })
348
+ }
349
+ this.#pendingTestFinishCallbacks.set(finishTest, abortController)
350
+ const uploadStarted = this.uploadTestScreenshots({
351
+ screenshots,
352
+ traceId: formattedTestSpan.trace_id.toString(10),
353
+ signal: abortController.signal,
354
+ }, finishTest)
327
355
  if (uploadStarted) return
328
356
 
329
- this.pendingTestFinishes--
330
- exportTraces()
357
+ finishTest()
331
358
  })
332
359
 
333
360
  this.addBind('ci:playwright:test:start', (ctx) => {
@@ -552,10 +579,11 @@ class PlaywrightPlugin extends CiPlugin {
552
579
  * @param {object} options - Upload options
553
580
  * @param {Array<object>} options.screenshots - Playwright test attachments
554
581
  * @param {string} options.traceId - Test trace id used as the screenshot key
582
+ * @param {AbortSignal} options.signal - Signal used to cancel uploads during error finalization
555
583
  * @param {(result: string|undefined) => void} onDone - Completion callback
556
584
  * @returns {boolean} Whether at least one upload was started
557
585
  */
558
- uploadTestScreenshots ({ screenshots, traceId }, onDone) {
586
+ uploadTestScreenshots ({ screenshots, traceId, signal }, onDone) {
559
587
  const exporter = this.tracer?._exporter
560
588
  if (!Array.isArray(screenshots) || !screenshots.length ||
561
589
  !exporter?.canUploadTestScreenshots?.() ||
@@ -581,6 +609,7 @@ class PlaywrightPlugin extends CiPlugin {
581
609
  traceId,
582
610
  idempotencyKey: `${traceId}:${basename(filePath)}`,
583
611
  capturedAtMs: getScreenshotCapturedAtMs(filePath, filePath),
612
+ signal,
584
613
  }, (error, uploaded = true) => {
585
614
  if (uploaded) {
586
615
  uploadResults[resultIndex] = error