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.
- package/ci/diagnose.js +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +3 -26
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +75 -13
- package/ci/test-optimization-validation/generated-test-contract.js +283 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +7 -0
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/cucumber.js +11 -12
- package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/jest.js +18 -17
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-instrumentations/src/playwright.js +3 -24
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +2 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
- package/packages/dd-trace/src/llmobs/tagger.js +23 -3
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- 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
|
|
815
|
-
|
|
816
|
-
|
|
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 }
|