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