dd-trace 5.116.0 → 5.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +3 -3
  45. package/packages/datadog-instrumentations/src/cucumber.js +11 -12
  46. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  47. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  50. package/packages/datadog-instrumentations/src/jest.js +18 -17
  51. package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
  52. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  53. package/packages/datadog-instrumentations/src/playwright.js +3 -24
  54. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
  55. package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
  56. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  57. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  58. package/packages/datadog-plugin-cucumber/src/index.js +2 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  60. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  61. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  62. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  63. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  64. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  65. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  66. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  67. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  68. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  70. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  71. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  72. package/packages/dd-trace/src/exporter.js +16 -9
  73. package/packages/dd-trace/src/llmobs/index.js +9 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  75. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  76. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  77. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  78. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  79. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  80. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  81. package/packages/dd-trace/src/llmobs/util.js +56 -3
  82. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  83. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  84. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  85. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  86. package/packages/dd-trace/src/plugins/index.js +1 -0
  87. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  88. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  89. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  90. package/packages/dd-trace/src/span_processor.js +1 -1
  91. package/packages/dd-trace/src/span_stats.js +22 -4
  92. package/ci/test-optimization-validation/command-suitability.js +0 -471
  93. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  94. package/ci/test-optimization-validation/init-probe.js +0 -246
  95. package/ci/test-optimization-validation/late-initialization.js +0 -63
  96. package/ci/test-optimization-validation/local-command.js +0 -163
  97. package/ci/test-optimization-validation/setup-runner.js +0 -97
  98. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -39,12 +39,6 @@ function findExportByName (pkg, name) {
39
39
  }
40
40
  }
41
41
 
42
- function getChannelPromise (channelToPublishTo, frameworkVersion, payload) {
43
- return new Promise(resolve => {
44
- channelToPublishTo.publish({ ...payload, onDone: resolve, frameworkVersion })
45
- })
46
- }
47
-
48
42
  function getTestRunnerExport (testPackage) {
49
43
  return findExportByName(testPackage, 'VitestTestRunner') || findExportByName(testPackage, 'TestRunner')
50
44
  }
@@ -239,7 +233,6 @@ module.exports = {
239
233
  workerReportLogsCh,
240
234
  codeCoverageReportCh,
241
235
  findExportByName,
242
- getChannelPromise,
243
236
  getTestRunnerExport,
244
237
  getTypeTasks,
245
238
  getTestName,
@@ -9,6 +9,7 @@ const {
9
9
  recordAttemptToFixExecution,
10
10
  logAttemptToFixTestExecution,
11
11
  } = require('../../dd-trace/src/plugins/util/test')
12
+ const { getChannelPromise } = require('./helpers/channel')
12
13
  const { addHook } = require('./helpers/instrument')
13
14
  const {
14
15
  testStartCh,
@@ -661,11 +662,6 @@ addHook({
661
662
  testSuiteStartCh.runStores(testSuiteCtx, () => {})
662
663
  const startTestsResponse = await startTests.apply(this, arguments)
663
664
 
664
- let onFinish = null
665
- const onFinishPromise = new Promise(resolve => {
666
- onFinish = resolve
667
- })
668
-
669
665
  const testTasks = getTypeTasks(startTestsResponse[0].tasks)
670
666
  const testEventPromises = []
671
667
 
@@ -811,9 +807,10 @@ addHook({
811
807
  testSuiteErrorCh.runStores(testSuiteCtx, () => {})
812
808
  }
813
809
 
814
- testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish, ...testSuiteCtx.currentStore })
815
-
816
- await onFinishPromise
810
+ await getChannelPromise(testSuiteFinishCh, {
811
+ status: testSuiteResult.state,
812
+ ...testSuiteCtx.currentStore,
813
+ })
817
814
 
818
815
  return startTestsResponse
819
816
  })
@@ -72,6 +72,7 @@ class CucumberPlugin extends CiPlugin {
72
72
  isEarlyFlakeDetectionFaulty,
73
73
  isTestManagementTestsEnabled,
74
74
  isParallel,
75
+ onDone,
75
76
  }) => {
76
77
  this._exportPendingWorkerTraces()
77
78
  const {
@@ -127,7 +128,7 @@ class CucumberPlugin extends CiPlugin {
127
128
  })
128
129
 
129
130
  this.libraryConfig = null
130
- this.tracer._exporter.flush()
131
+ this.tracer._exporter.flush(onDone)
131
132
  })
132
133
 
133
134
  this.addSub('ci:cucumber:test-suite:start', ({
@@ -390,6 +390,7 @@ class MochaPlugin extends CiPlugin {
390
390
  isEarlyFlakeDetectionFaulty,
391
391
  isTestManagementEnabled,
392
392
  isParallel,
393
+ onDone,
393
394
  }) => {
394
395
  this._exportPendingWorkerTraces()
395
396
  if (this.testSessionSpan) {
@@ -456,7 +457,7 @@ class MochaPlugin extends CiPlugin {
456
457
  })
457
458
  }
458
459
  this.libraryConfig = null
459
- this.tracer._exporter.flush()
460
+ this.tracer._exporter.flush(onDone)
460
461
  })
461
462
 
462
463
  this.addBind('ci:mocha:global:run', (ctx) => {
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const Plugin = require('../../dd-trace/src/plugins/plugin')
5
+ const { MODEL_BASE_URL_STORE_KEY, OpenAIAgentsIntegration } = require('./integration')
6
+ const { DDOpenAIAgentsProcessor } = require('./processor')
7
+
8
+ const legacyStorage = storage('legacy')
9
+
10
+ /**
11
+ * Drives the openai-agents integration through agents-core's
12
+ * `TracingProcessor` interface. The instrumentation hook publishes the
13
+ * loaded `@openai/agents` module on a channel; this plugin subscribes
14
+ * during its constructor (which runs synchronously between `loadChannel`'s
15
+ * publish and the addHook callback) and registers the processor.
16
+ *
17
+ * The integration's `enabled` flag follows this plugin's configure()
18
+ * lifecycle. Each loaded version of the agents package replaces all processors
19
+ * via setTraceProcessors() on module load, so the plugin re-registers a
20
+ * fresh DDOpenAIAgentsProcessor for each module version that fires the channel.
21
+ */
22
+ class OpenaiAgentsPlugin extends Plugin {
23
+ static id = 'openai-agents'
24
+
25
+ #integration
26
+
27
+ constructor (tracer, tracerConfig) {
28
+ super(tracer, tracerConfig)
29
+ this.#integration = new OpenAIAgentsIntegration({
30
+ tracer: this.tracer,
31
+ config: tracerConfig,
32
+ })
33
+
34
+ // Register a new processor each time @openai/agents fires the channel.
35
+ // Each module version calls setTraceProcessors() on load (which replaces
36
+ // all processors), so we must re-register after every new version loads.
37
+ // The instrumentation's patchedMods WeakSet ensures each module instance
38
+ // fires the channel exactly once, so no duplicates accumulate.
39
+ this.addSub('apm:openai-agents:agents-core:loaded', ({ mod }) => {
40
+ const processor = new DDOpenAIAgentsProcessor(() => this.#integration)
41
+ if (typeof mod?.addTraceProcessor === 'function') {
42
+ mod.addTraceProcessor(processor)
43
+ } else {
44
+ mod.getGlobalTraceProvider().registerProcessor(processor)
45
+ }
46
+ })
47
+
48
+ // Activate the current agent's dd-trace span in legacyStorage for the
49
+ // duration of model response calls and stream iterator advancement. This
50
+ // makes the openai plugin's shimmer see the correct parent when it creates its
51
+ // openai.request span, so all spans land in the same trace.
52
+ this.addBind('apm:openai-agents:model:start', ({ agentsCoreSpanId, baseURL }) => {
53
+ const store = legacyStorage.getStore()
54
+ if (!this.#integration.enabled || !agentsCoreSpanId) return store
55
+ const ddSpan = this.#integration.getDDSpan(agentsCoreSpanId)
56
+ if (!ddSpan) return store
57
+ return { ...store, [MODEL_BASE_URL_STORE_KEY]: baseURL, span: ddSpan }
58
+ })
59
+
60
+ this.addBind('apm:openai-agents:tool:start', ({ agentsCoreSpan }) => {
61
+ const store = legacyStorage.getStore()
62
+ if (!this.#integration.enabled || !agentsCoreSpan) return store
63
+ const ddSpan = this.#integration.getOrStartToolSpan(agentsCoreSpan)
64
+ return ddSpan ? { ...store, span: ddSpan } : store
65
+ })
66
+ }
67
+
68
+ configure (config) {
69
+ super.configure(config)
70
+ this.#integration.configure(config)
71
+ }
72
+ }
73
+
74
+ module.exports = OpenaiAgentsPlugin
@@ -0,0 +1,503 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const { storage: llmobsStorage } = require('../../dd-trace/src/llmobs/storage')
5
+ const LLMObsTagger = require('../../dd-trace/src/llmobs/tagger')
6
+ const { getOpenAIModelProvider } = require('../../dd-trace/src/llmobs/plugins/openai/utils')
7
+ const {
8
+ extractInputMessages,
9
+ extractOutputMessages,
10
+ extractGenerationOutputMessages,
11
+ extractMetrics,
12
+ extractMetadata,
13
+ } = require('../../dd-trace/src/llmobs/plugins/openai-agents/utils')
14
+ const { AGENTS_ERROR_TYPE, applyError, deriveSpanName } = require('./util')
15
+
16
+ const COMPONENT = 'openai-agents'
17
+ const DEFAULT_MODEL_PROVIDER = 'openai'
18
+ const MODEL_BASE_URL_STORE_KEY = Symbol('openai-agents.model-base-url')
19
+ const legacyStorage = storage('legacy')
20
+
21
+ const KIND_TO_SPAN_KIND = {
22
+ agent: 'internal',
23
+ tool: 'internal',
24
+ task: 'internal',
25
+ llm: 'client',
26
+ }
27
+
28
+ /**
29
+ * @typedef {{
30
+ * spanId: string,
31
+ * traceId: string,
32
+ * currentTopLevelAgentSpanId?: string,
33
+ * currentTopLevelAgentName?: string,
34
+ * inputOaiSpan?: object,
35
+ * inputMessages?: Array<{ role: string, content: string }>,
36
+ * outputOaiSpan?: object,
37
+ * metadata?: Record<string, unknown>,
38
+ * groupId?: string,
39
+ * llmobsParentStore?: object,
40
+ * }} LLMObsTraceInfo
41
+ */
42
+
43
+ /**
44
+ * Owns tracer/tagger refs, maps agents-core span ids → dd-trace spans, and
45
+ * reconstructs workflow-level input/output from the first and last response
46
+ * spans of the top-level agent.
47
+ */
48
+ class OpenAIAgentsIntegration {
49
+ #tracer
50
+ #config
51
+ #enabled = false
52
+ #service
53
+ /**
54
+ * LLMObs is gated independently of APM tracing: when DD_LLMOBS_ENABLED is
55
+ * false we keep emitting APM spans for the agent workflow but skip all
56
+ * LLMObs tagging and the work that feeds it.
57
+ * @type {LLMObsTagger}
58
+ */
59
+ #tagger
60
+ /** @type {Map<string, import('../../dd-trace/src/opentracing/span')>} */
61
+ #oaiToDdSpan = new Map()
62
+ /** @type {Map<string, LLMObsTraceInfo>} */
63
+ #traceInfo = new Map()
64
+
65
+ constructor ({ tracer, config } = {}) {
66
+ this.#tracer = tracer
67
+ this.#config = config ?? { llmobs: {} }
68
+ this.#tagger = new LLMObsTagger(this.#config, true)
69
+ }
70
+
71
+ get enabled () {
72
+ return this.#enabled
73
+ }
74
+
75
+ /**
76
+ * Apply plugin lifecycle configuration.
77
+ *
78
+ * @param {{ enabled?: boolean, service?: string }} [config]
79
+ */
80
+ configure (config) {
81
+ this.#enabled = !!config?.enabled
82
+ this.#service = config?.service
83
+ }
84
+
85
+ /**
86
+ * @param {string} spanId agents-core spanId
87
+ * @returns {import('../../dd-trace/src/opentracing/span') | undefined}
88
+ */
89
+ getDDSpan (spanId) {
90
+ return this.#oaiToDdSpan.get(spanId)
91
+ }
92
+
93
+ /**
94
+ * Ensure a function span is available synchronously at Tool.invoke. Newer
95
+ * agents-core versions dispatch processor callbacks after an async exporter
96
+ * callback, which can otherwise happen after user tool code has started.
97
+ *
98
+ * @param {object} oaiSpan
99
+ * @returns {import('../../dd-trace/src/opentracing/span') | undefined}
100
+ */
101
+ getOrStartToolSpan (oaiSpan) {
102
+ if (!oaiSpan?.spanId) return
103
+ const existingSpan = this.#oaiToDdSpan.get(oaiSpan.spanId)
104
+ if (existingSpan) return existingSpan
105
+ this.startSpan(oaiSpan, 'tool')
106
+ return this.#oaiToDdSpan.get(oaiSpan.spanId)
107
+ }
108
+
109
+ clearState () {
110
+ // Finish any dd-trace spans still in-flight so we don't leak open traces
111
+ // when agents-core's TracingProcessor.shutdown() runs (e.g., process
112
+ // exiting mid-run).
113
+ for (const ddSpan of this.#oaiToDdSpan.values()) {
114
+ ddSpan.finish()
115
+ }
116
+ this.#oaiToDdSpan.clear()
117
+ this.#traceInfo.clear()
118
+ }
119
+
120
+ // ── Trace lifecycle ─────────────────────────────────────────────────────────
121
+
122
+ startTrace (oaiTrace) {
123
+ const traceId = oaiTrace.traceId
124
+ if (!traceId) return
125
+
126
+ const name = oaiTrace.name || 'Agent workflow'
127
+ const parentSpan = legacyStorage.getStore()?.span
128
+ const ddSpan = this.#tracer.startSpan(name, {
129
+ childOf: parentSpan,
130
+ tags: this.#getSpanTags('internal'),
131
+ })
132
+
133
+ const llmobsParentStore = this.#isLLMObsEnabled() ? llmobsStorage.getStore() : undefined
134
+ this.#oaiToDdSpan.set(traceId, ddSpan)
135
+ this.#traceInfo.set(traceId, {
136
+ spanId: ddSpan.context().toSpanId(),
137
+ traceId,
138
+ groupId: oaiTrace.groupId || undefined,
139
+ metadata: oaiTrace.metadata,
140
+ llmobsParentStore,
141
+ })
142
+
143
+ this.#tagger.registerLLMObsSpan(ddSpan, {
144
+ kind: 'workflow',
145
+ name,
146
+ integration: COMPONENT,
147
+ parent: llmobsParentStore?.span,
148
+ sessionId: oaiTrace.groupId || undefined,
149
+ })
150
+ if (LLMObsTagger.tagMap.has(ddSpan)) {
151
+ llmobsStorage.enterWith({ ...llmobsParentStore, span: ddSpan })
152
+ }
153
+ }
154
+
155
+ endTrace (oaiTrace) {
156
+ this.#completeWorkflowSpan(oaiTrace.traceId)
157
+ }
158
+
159
+ /**
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.
165
+ *
166
+ * @param {string | undefined} traceId
167
+ * @param {object} [rootAgentSpan] - parentless oai-span that ended in error.
168
+ */
169
+ #completeWorkflowSpan (traceId, rootAgentSpan) {
170
+ if (!traceId) return
171
+ const ddSpan = this.#oaiToDdSpan.get(traceId)
172
+ if (!ddSpan) return
173
+ const info = this.#traceInfo.get(traceId)
174
+
175
+ if (rootAgentSpan?.error) {
176
+ ddSpan.setTag('error', true)
177
+ ddSpan.setTag('error.type', AGENTS_ERROR_TYPE)
178
+ if (rootAgentSpan.error.message) {
179
+ ddSpan.setTag('error.message', rootAgentSpan.error.message)
180
+ }
181
+ }
182
+
183
+ if (this.#isLLMObsEnabled()) this.#setTraceAttributes(ddSpan, traceId)
184
+ ddSpan.finish()
185
+ if (info && LLMObsTagger.tagMap.has(ddSpan)) {
186
+ llmobsStorage.enterWith(info.llmobsParentStore)
187
+ }
188
+ this.#oaiToDdSpan.delete(traceId)
189
+ this.#traceInfo.delete(traceId)
190
+ }
191
+
192
+ // ── Span lifecycle ──────────────────────────────────────────────────────────
193
+
194
+ startSpan (oaiSpan, llmobsKind) {
195
+ const spanId = oaiSpan.spanId
196
+ if (!spanId || this.#oaiToDdSpan.has(spanId)) return
197
+
198
+ const parentSpan = this.#resolveParent(oaiSpan)
199
+ const spanName = deriveSpanName(oaiSpan)
200
+
201
+ const ddSpan = this.#tracer.startSpan(spanName, {
202
+ childOf: parentSpan,
203
+ tags: this.#getSpanTags(KIND_TO_SPAN_KIND[llmobsKind] ?? 'internal'),
204
+ })
205
+
206
+ this.#oaiToDdSpan.set(spanId, ddSpan)
207
+
208
+ if (this.#isLLMObsEnabled()) {
209
+ const llmobsOptions = {
210
+ kind: llmobsKind,
211
+ name: spanName,
212
+ integration: COMPONENT,
213
+ parent: parentSpan,
214
+ }
215
+
216
+ if (oaiSpan.spanData?.type === 'response' || oaiSpan.spanData?.type === 'generation') {
217
+ // Model name only arrives with the response; tagged in
218
+ // `#setResponseAttributes` once known. Model provider is resolved from
219
+ // the agents-openai client's baseURL captured at getResponse time.
220
+ llmobsOptions.modelProvider = this.#getCurrentModelProvider()
221
+ }
222
+
223
+ this.#tagger.registerLLMObsSpan(ddSpan, llmobsOptions)
224
+ this.#updateTraceInfoInput(oaiSpan, spanName)
225
+ }
226
+ }
227
+
228
+ endSpan (oaiSpan) {
229
+ const spanId = oaiSpan.spanId
230
+ const ddSpan = this.#oaiToDdSpan.get(spanId)
231
+ if (!ddSpan) return
232
+
233
+ applyError(ddSpan, oaiSpan)
234
+
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
+ }
240
+
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
266
+ }
267
+ }
268
+
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)
274
+ }
275
+ this.#oaiToDdSpan.delete(spanId)
276
+ }
277
+
278
+ // ── Per-type attribute setters ──────────────────────────────────────────────
279
+
280
+ #setResponseAttributes (ddSpan, oaiSpan) {
281
+ const response = oaiSpan.spanData?._response
282
+ const input = oaiSpan.spanData?._input
283
+ if (response?.model) {
284
+ this.#tagger.tagModelName(ddSpan, response.model)
285
+ }
286
+
287
+ // Override the LLMObs span name to `{parent_agent_name} (LLM)` only when
288
+ // the response is a direct child of the top-level agent (Python parity:
289
+ // see `_llmobs_set_response_attributes` in dd-trace-py). For bare
290
+ // `withResponseSpan` calls outside a `Runner.run()` flow the default
291
+ // name (`openai_agents.response`) stays.
292
+ const parentAgentName = this.#llmSpanParentAgentName(oaiSpan)
293
+ if (parentAgentName) {
294
+ this.#tagger.setName(ddSpan, `${parentAgentName} (LLM)`)
295
+ }
296
+
297
+ // Always tag LLM I/O so the LLMObs event shape is consistent across
298
+ // happy/error paths. The extract* helpers emit placeholder messages
299
+ // when their source is absent.
300
+ const inputMessages = extractInputMessages(input, response?.instructions)
301
+ this.#tagger.tagLLMIO(ddSpan, inputMessages, extractOutputMessages(response))
302
+
303
+ // Cache messages for the workflow span's trace-level input (Python
304
+ // parity: last message of the first response under the top-level agent).
305
+ // Avoids re-running extractInputMessages in #setTraceAttributes.
306
+ const info = this.#traceInfo.get(oaiSpan.traceId)
307
+ if (info && info.inputOaiSpan === oaiSpan) {
308
+ info.inputMessages = inputMessages
309
+ }
310
+
311
+ if (response) {
312
+ const metrics = extractMetrics(response)
313
+ if (metrics) this.#tagger.tagMetrics(ddSpan, metrics)
314
+
315
+ const metadata = extractMetadata(response)
316
+ if (metadata) this.#tagger.tagMetadata(ddSpan, metadata)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Tag a Chat Completions generation span from agents-core's public span data.
322
+ *
323
+ * @param {import('../../dd-trace/src/opentracing/span')} ddSpan
324
+ * @param {object} oaiSpan
325
+ */
326
+ #setGenerationAttributes (ddSpan, oaiSpan) {
327
+ const spanData = oaiSpan.spanData
328
+ if (spanData?.model) {
329
+ this.#tagger.tagModelName(ddSpan, spanData.model)
330
+ }
331
+
332
+ const parentAgentName = this.#llmSpanParentAgentName(oaiSpan)
333
+ if (parentAgentName) {
334
+ this.#tagger.setName(ddSpan, `${parentAgentName} (LLM)`)
335
+ }
336
+
337
+ const inputMessages = extractInputMessages(spanData?.input)
338
+ this.#tagger.tagLLMIO(ddSpan, inputMessages, extractGenerationOutputMessages(spanData?.output))
339
+
340
+ const info = this.#traceInfo.get(oaiSpan.traceId)
341
+ if (info && info.inputOaiSpan === oaiSpan) {
342
+ info.inputMessages = inputMessages
343
+ }
344
+
345
+ const metrics = extractMetrics({
346
+ usage: spanData?.usage ?? spanData?.output?.at(-1)?.usage,
347
+ })
348
+ if (metrics) this.#tagger.tagMetrics(ddSpan, metrics)
349
+ if (spanData?.model_config) this.#tagger.tagMetadata(ddSpan, spanData.model_config)
350
+ }
351
+
352
+ /**
353
+ * If this response span's parent is the top-level agent span of the trace,
354
+ * return that agent's dd-trace span name. Used to set the LLMObs span name
355
+ * to `${agentName} (LLM)` (Python parity).
356
+ *
357
+ * @param {object} oaiSpan
358
+ * @returns {string | undefined}
359
+ */
360
+ #llmSpanParentAgentName (oaiSpan) {
361
+ const traceInfo = this.#traceInfo.get(oaiSpan.traceId)
362
+ if (!traceInfo?.currentTopLevelAgentSpanId) return
363
+ if (oaiSpan.parentId !== traceInfo.currentTopLevelAgentSpanId) return
364
+ return traceInfo.currentTopLevelAgentName
365
+ }
366
+
367
+ #setAgentAttributes (ddSpan, oaiSpan) {
368
+ const spanData = oaiSpan.spanData
369
+ let metadata
370
+ if (Array.isArray(spanData?.handoffs) && spanData.handoffs.length > 0) {
371
+ metadata = { handoffs: spanData.handoffs }
372
+ }
373
+ if (Array.isArray(spanData?.tools) && spanData.tools.length > 0) {
374
+ metadata ??= {}
375
+ metadata.tools = spanData.tools
376
+ }
377
+ if (spanData?.output_type) {
378
+ metadata ??= {}
379
+ metadata.output_type = spanData.output_type
380
+ }
381
+ if (metadata) this.#tagger.tagMetadata(ddSpan, metadata)
382
+ }
383
+
384
+ #setTraceAttributes (ddSpan, traceId) {
385
+ const info = this.#traceInfo.get(traceId)
386
+ if (!info) return
387
+
388
+ // Workflow-level input is the last input message of the first response
389
+ // span under the top-level agent; output is `response.output_text` of
390
+ // the last response span. Matches dd-trace-py's
391
+ // `OaiSpanAdapter.llmobs_trace_input` / `response_output_text`. The
392
+ // input messages were cached during #setResponseAttributes.
393
+ const lastInputMessage = info.inputMessages?.at(-1)
394
+ const inputValue = typeof lastInputMessage?.content === 'string' ? lastInputMessage.content : ''
395
+ const outputSpanData = info.outputOaiSpan?.spanData
396
+ let outputValue = outputSpanData?._response?.output_text ?? ''
397
+ if (outputSpanData?.type === 'generation') {
398
+ const outputMessages = extractGenerationOutputMessages(outputSpanData.output)
399
+ outputValue = outputMessages.at(-1)?.content ?? ''
400
+ }
401
+
402
+ this.#tagger.tagTextIO(ddSpan, inputValue, outputValue)
403
+
404
+ if (info.metadata && Object.keys(info.metadata).length > 0) {
405
+ this.#tagger.tagMetadata(ddSpan, info.metadata)
406
+ }
407
+ }
408
+
409
+ // ── Trace-info reconstruction (Python parity) ───────────────────────────────
410
+
411
+ #updateTraceInfoInput (oaiSpan, spanName) {
412
+ const info = this.#traceInfo.get(oaiSpan.traceId)
413
+ if (!info) return
414
+
415
+ const parentId = oaiSpan.parentId
416
+ const type = oaiSpan.spanData?.type
417
+
418
+ // Identify the first top-level agent span under the root trace and
419
+ // stash its display name so `${agentName} (LLM)` doesn't have to read
420
+ // the dd-trace span context's private fields later.
421
+ if (type === 'agent' && parentId == null) {
422
+ info.currentTopLevelAgentSpanId = oaiSpan.spanId
423
+ info.currentTopLevelAgentName = spanName
424
+ }
425
+
426
+ // Capture the first response span whose parent is the top-level agent
427
+ // as the workflow-level input source.
428
+ if (
429
+ (type === 'response' || type === 'generation') &&
430
+ parentId &&
431
+ !info.inputOaiSpan &&
432
+ parentId === info.currentTopLevelAgentSpanId
433
+ ) {
434
+ info.inputOaiSpan = oaiSpan
435
+ }
436
+ }
437
+
438
+ #updateTraceInfoOutput (oaiSpan) {
439
+ const info = this.#traceInfo.get(oaiSpan.traceId)
440
+ if (!info) return
441
+
442
+ if (
443
+ oaiSpan.parentId &&
444
+ info.currentTopLevelAgentSpanId &&
445
+ oaiSpan.parentId === info.currentTopLevelAgentSpanId
446
+ ) {
447
+ info.outputOaiSpan = oaiSpan
448
+ }
449
+ }
450
+
451
+ // ── Helpers ─────────────────────────────────────────────────────────────────
452
+
453
+ #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
459
+ }
460
+ if (traceId) {
461
+ const root = this.#oaiToDdSpan.get(traceId)
462
+ if (root) return root
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Read the mutable tracer configuration so remote/runtime enablement is
468
+ * reflected without reconstructing the plugin.
469
+ *
470
+ * @returns {boolean}
471
+ */
472
+ #isLLMObsEnabled () {
473
+ return !!this.#config.llmobs?.DD_LLMOBS_ENABLED
474
+ }
475
+
476
+ /**
477
+ * Resolve model provider from the model invocation's async-local context.
478
+ *
479
+ * @returns {string}
480
+ */
481
+ #getCurrentModelProvider () {
482
+ const baseURL = legacyStorage.getStore()?.[MODEL_BASE_URL_STORE_KEY]
483
+ return baseURL ? getOpenAIModelProvider(baseURL) : DEFAULT_MODEL_PROVIDER
484
+ }
485
+
486
+ /**
487
+ * Build common APM tags without overriding the tracer's global service when
488
+ * the integration has no explicit service configuration.
489
+ *
490
+ * @param {string} spanKind
491
+ * @returns {Record<string, string>}
492
+ */
493
+ #getSpanTags (spanKind) {
494
+ const tags = {
495
+ component: COMPONENT,
496
+ 'span.kind': spanKind,
497
+ }
498
+ if (this.#service) tags.service = this.#service
499
+ return tags
500
+ }
501
+ }
502
+
503
+ module.exports = { MODEL_BASE_URL_STORE_KEY, OpenAIAgentsIntegration }