dd-trace 6.9.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 (59) hide show
  1. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  2. package/index.d.ts +42 -0
  3. package/package.json +4 -3
  4. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  5. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  6. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  10. package/packages/datadog-instrumentations/src/jest.js +1 -1
  11. package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
  12. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -8
  13. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  14. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  15. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  16. package/packages/datadog-instrumentations/src/pg.js +116 -92
  17. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  18. package/packages/datadog-instrumentations/src/vitest-main.js +169 -17
  19. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  21. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  23. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  24. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  25. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -1
  26. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  27. package/packages/datadog-plugin-jest/src/index.js +5 -4
  28. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  29. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  30. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  31. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  32. package/packages/datadog-plugin-mocha/src/index.js +550 -39
  33. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  34. package/packages/datadog-plugin-next/src/index.js +1 -19
  35. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  36. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  37. package/packages/datadog-plugin-pg/src/index.js +1 -0
  38. package/packages/datadog-plugin-vitest/src/index.js +8 -20
  39. package/packages/datadog-plugin-ws/src/util.js +2 -1
  40. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  41. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  42. package/packages/dd-trace/src/carrier.js +356 -0
  43. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  45. package/packages/dd-trace/src/datastreams/context.js +6 -1
  46. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  47. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  48. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  49. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  50. package/packages/dd-trace/src/llmobs/index.js +34 -2
  51. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  52. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  53. package/packages/dd-trace/src/llmobs/util.js +78 -0
  54. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  55. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  56. package/packages/dd-trace/src/plugin_manager.js +2 -0
  57. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +1 -62
  58. package/packages/dd-trace/src/plugins/util/test.js +84 -5
  59. package/packages/dd-trace/src/plugins/util/web.js +53 -2
@@ -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
@@ -145,7 +145,7 @@ class NextPlugin extends ServerPlugin {
145
145
  }
146
146
 
147
147
  configure (config) {
148
- return super.configure(normalizeConfig(config))
148
+ return super.configure(web.normalizeConfig(config))
149
149
  }
150
150
  }
151
151
 
@@ -178,22 +178,4 @@ function setHttpParentRoute (span, method, page, isStatic) {
178
178
  span.setTag(RESOURCE_NAME, `${method} ${page}`.trim())
179
179
  nextParentRoutes.set(span, page)
180
180
  }
181
-
182
- function normalizeConfig (config) {
183
- const hooks = getHooks(config)
184
- const validateStatus = typeof config.validateStatus === 'function'
185
- ? config.validateStatus
186
- : code => code < 500
187
-
188
- return { ...config, hooks, validateStatus }
189
- }
190
-
191
- const noop = () => {}
192
-
193
- function getHooks (config) {
194
- const request = config.hooks?.request ?? noop
195
-
196
- return { request }
197
- }
198
-
199
181
  module.exports = NextPlugin
@@ -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.
@@ -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: {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
4
4
  const { storage } = require('../../datadog-core')
5
+ const { writeDatadogParentId, writeDatadogTraceId } = require('../../dd-trace/src/carrier')
5
6
 
6
7
  const {
7
8
  TEST_STATUS,
@@ -13,7 +14,6 @@ const {
13
14
  getTestSuiteCommonTags,
14
15
  getTestLevelsMetadataTags,
15
16
  getTestSessionName,
16
- getIsFaultyEarlyFlakeDetection,
17
17
  TEST_SOURCE_FILE,
18
18
  TEST_IS_RETRY,
19
19
  TEST_CODE_OWNERS,
@@ -107,19 +107,6 @@ class VitestPlugin extends CiPlugin {
107
107
  })
108
108
  })
109
109
 
110
- this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
111
- knownTests,
112
- testFilepaths,
113
- onDone,
114
- }) => {
115
- const isFaulty = getIsFaultyEarlyFlakeDetection(
116
- testFilepaths.map(testFilepath => getTestSuitePath(testFilepath, this.repositoryRoot)),
117
- knownTests,
118
- this.libraryConfig.earlyFlakeDetectionFaultyThreshold
119
- )
120
- onDone(isFaulty)
121
- })
122
-
123
110
  this.addBind('ci:vitest:test:start', (ctx) => {
124
111
  const {
125
112
  testName,
@@ -424,12 +411,13 @@ class VitestPlugin extends CiPlugin {
424
411
  this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)
425
412
  this.command = testCommand
426
413
  this.frameworkVersion = frameworkVersion
427
- const testSessionSpanContext = testSessionId && testModuleId
428
- ? this.tracer.extract('text_map', {
429
- 'x-datadog-trace-id': testSessionId,
430
- 'x-datadog-parent-id': testModuleId,
431
- })
432
- : undefined
414
+ let testSessionSpanContext
415
+ if (testSessionId && testModuleId) {
416
+ const carrier = /** @type {Record<string, unknown>} */ ({})
417
+ writeDatadogTraceId(carrier, testSessionId)
418
+ writeDatadogParentId(carrier, testModuleId)
419
+ testSessionSpanContext = this.tracer.extract('text_map', carrier)
420
+ }
433
421
 
434
422
  const trimmedCommand = DD_MAJOR < 6 ? this.command : 'vitest run'
435
423
  // test suites run in a different process, so they also need to init the metadata dictionary
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { readDatadogTraceId, readTraceparent } = require('../../dd-trace/src/carrier')
3
4
  const DatadogSpanContext = require('../../dd-trace/src/opentracing/span_context')
4
5
 
5
6
  const TRACE_ID_UPPER_TAG = '_dd.p.tid'
@@ -38,7 +39,7 @@ function createWebSocketSpanContext (spanContext) {
38
39
  * @returns {boolean}
39
40
  */
40
41
  function hasTraceHeaders (headers) {
41
- return !!(headers && (headers['x-datadog-trace-id'] || headers.traceparent))
42
+ return !!(headers && (readDatadogTraceId(headers) || readTraceparent(headers)))
42
43
  }
43
44
 
44
45
  /**