dd-trace 6.17.0 → 6.19.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/LICENSE-3rdparty.csv +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +55 -11
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +50 -9
- package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
- package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +30 -13
- package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
- package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
- package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
- package/packages/datadog-instrumentations/src/playwright.js +73 -6
- package/packages/datadog-instrumentations/src/undici.js +7 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
- package/packages/datadog-instrumentations/src/ws.js +41 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +22 -8
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
- package/packages/datadog-plugin-cypress/src/index.js +3 -1
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-jest/src/index.js +3 -4
- package/packages/datadog-plugin-mocha/src/index.js +3 -4
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-playwright/src/index.js +11 -6
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/datadog-plugin-vitest/src/index.js +3 -4
- package/packages/datadog-plugin-ws/src/producer.js +1 -1
- package/packages/datadog-shimmer/src/shimmer.js +14 -10
- package/packages/datadog-turbopack/index.js +4 -1
- package/packages/datadog-turbopack/src/loader.js +6 -2
- package/packages/dd-trace/src/agent/info.js +5 -4
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
- package/packages/dd-trace/src/config/index.js +7 -6
- package/packages/dd-trace/src/config/supported-configurations.json +16 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/debugger/index.js +1 -1
- package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
- package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
- package/packages/dd-trace/src/evp_proxy/path.js +21 -7
- package/packages/dd-trace/src/exporters/common/url.js +33 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +11 -40
- package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
- package/packages/dd-trace/src/openfeature/index.js +6 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
- package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/opentracing/tracer.js +3 -3
- package/packages/dd-trace/src/plugin_manager.js +27 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/test.js +13 -4
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/proxy.js +2 -2
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/packages/dd-trace/src/telemetry/send-data.js +2 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/packages/dd-trace/src/tracer.js +5 -4
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
- package/packages/datadog-instrumentations/src/bullmq.js +0 -11
- package/packages/datadog-instrumentations/src/langchain.js +0 -7
- package/packages/datadog-instrumentations/src/langgraph.js +0 -7
- package/packages/datadog-instrumentations/src/mercurius.js +0 -11
|
@@ -4,6 +4,7 @@ const fs = require('node:fs')
|
|
|
4
4
|
const path = require('node:path')
|
|
5
5
|
const { performance } = require('node:perf_hooks')
|
|
6
6
|
const { fileURLToPath } = require('node:url')
|
|
7
|
+
const { deserialize } = require('node:v8')
|
|
7
8
|
const { isMainThread, parentPort } = require('node:worker_threads')
|
|
8
9
|
|
|
9
10
|
const { channel } = require('dc-polyfill')
|
|
@@ -47,6 +48,7 @@ const {
|
|
|
47
48
|
|
|
48
49
|
const EFD_SUITE_ADMISSION_TIMEOUT_MS = 5000
|
|
49
50
|
const logSubmissionFlushCh = channel('ci:log-submission:flush')
|
|
51
|
+
const wrappedTestRunners = new WeakSet()
|
|
50
52
|
const taskToCtx = new WeakMap()
|
|
51
53
|
const taskToTestProperties = new WeakMap()
|
|
52
54
|
const taskToStatuses = new WeakMap()
|
|
@@ -412,6 +414,14 @@ function finishEfdSuiteAdmissionRequest (requestId, allowed) {
|
|
|
412
414
|
* @param {unknown} message
|
|
413
415
|
*/
|
|
414
416
|
function handleEfdSuiteAdmissionResponse (message) {
|
|
417
|
+
// Older Vitest 4 forks serialize parent messages explicitly; newer forks and threads send plain values.
|
|
418
|
+
if (Buffer.isBuffer(message) || (message?.type === 'Buffer' && Array.isArray(message.data))) {
|
|
419
|
+
try {
|
|
420
|
+
message = deserialize(Buffer.from(message))
|
|
421
|
+
} catch {
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
}
|
|
415
425
|
if (!Array.isArray(message) || message[0] !== VITEST_WORKER_EFD_SUITE_ADMISSION_RESPONSE_CODE) return
|
|
416
426
|
|
|
417
427
|
const { allowed, requestId } = message[1] || {}
|
|
@@ -520,6 +530,11 @@ function wrapSuiteHookFn (hookType, fn, fallbackTask) {
|
|
|
520
530
|
}
|
|
521
531
|
|
|
522
532
|
function wrapVitestTestRunner (VitestTestRunner) {
|
|
533
|
+
// ESM hooks can expose the same runner through multiple module identities.
|
|
534
|
+
// Wrapping its lifecycle methods twice would report each retry twice.
|
|
535
|
+
if (wrappedTestRunners.has(VitestTestRunner)) return
|
|
536
|
+
wrappedTestRunners.add(VitestTestRunner)
|
|
537
|
+
|
|
523
538
|
// `onBeforeRunTask` is run before any repetition or attempt is run
|
|
524
539
|
// `onBeforeRunTask` is an async function
|
|
525
540
|
shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => async function (task) {
|
|
@@ -623,6 +638,13 @@ function wrapVitestTestRunner (VitestTestRunner) {
|
|
|
623
638
|
shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) {
|
|
624
639
|
const { isTestManagementTestsEnabled } = getProvidedContext()
|
|
625
640
|
|
|
641
|
+
// Vitest 5 aggregates failed repetitions into the final state. Preserve the last attempt's actual result.
|
|
642
|
+
if ((attemptToFixTasks.has(task) || efdRetryTasks.has(task)) &&
|
|
643
|
+
task.result.state === 'fail' && taskToCtx.get(task)?.status === 'pass' &&
|
|
644
|
+
(task.result.errors?.length ?? 0) === (taskToReportedErrorCount.get(task) ?? 0)) {
|
|
645
|
+
task.result.state = 'pass'
|
|
646
|
+
}
|
|
647
|
+
|
|
626
648
|
if (isTestManagementTestsEnabled) {
|
|
627
649
|
const isAttemptingToFix = attemptToFixTasks.has(task)
|
|
628
650
|
const isQuarantined = quarantinedTasks.has(task)
|
|
@@ -745,7 +767,12 @@ function wrapVitestTestRunner (VitestTestRunner) {
|
|
|
745
767
|
}
|
|
746
768
|
}
|
|
747
769
|
|
|
748
|
-
|
|
770
|
+
let lastExecutionStatus = task.result.state
|
|
771
|
+
if (lastExecutionStatus === 'run' && numRepetition > 0) {
|
|
772
|
+
// Vitest 5 resets the state before each repetition but retains errors from earlier attempts.
|
|
773
|
+
const reportedErrorCount = taskToReportedErrorCount.get(task) ?? 0
|
|
774
|
+
lastExecutionStatus = (task.result.errors?.length ?? 0) > reportedErrorCount ? 'fail' : 'pass'
|
|
775
|
+
}
|
|
749
776
|
const isAtf = attemptToFixTasks.has(task)
|
|
750
777
|
const isEfd = efdRetryTasks.has(task)
|
|
751
778
|
const shouldTrackStatuses = isEfd || isAtf
|
|
@@ -1101,14 +1128,18 @@ function getStartTestsWrapper (frameworkVersion) {
|
|
|
1101
1128
|
isDisabled: disabledTasks.has(task),
|
|
1102
1129
|
isQuarantined: quarantinedTasks.has(task),
|
|
1103
1130
|
})
|
|
1104
|
-
} else if (state === 'pass' && !isSwitchedStatus) {
|
|
1131
|
+
} else if ((state === 'pass' && !isSwitchedStatus) || switchedStatus === 'pass') {
|
|
1132
|
+
// Attempt-to-fix can pass its last attempt while an earlier failure still fails the suite.
|
|
1133
|
+
if (switchedStatus === 'pass' && errors?.length) {
|
|
1134
|
+
testSuiteError = testError
|
|
1135
|
+
}
|
|
1105
1136
|
if (testCtx) {
|
|
1106
1137
|
const isSkippedByTestManagement =
|
|
1107
1138
|
!attemptToFixTasks.has(task) && (disabledTasks.has(task) || quarantinedTasks.has(task))
|
|
1108
1139
|
const promises = {}
|
|
1109
1140
|
testPassCh.publish({
|
|
1110
1141
|
task,
|
|
1111
|
-
finalStatus: isSkippedByTestManagement ? 'skip' :
|
|
1142
|
+
finalStatus: isSkippedByTestManagement ? 'skip' : state,
|
|
1112
1143
|
earlyFlakeAbortReason: efdSlowAbortedTasks.has(task) ? 'slow' : undefined,
|
|
1113
1144
|
promises,
|
|
1114
1145
|
...testCtx.currentStore,
|
|
@@ -63,6 +63,7 @@ const baseReporterWaitForSyncCh = tracingChannel('orchestrion:@wdio/runner:BaseR
|
|
|
63
63
|
const runnerRunCh = tracingChannel('orchestrion:@wdio/runner:Runner_run')
|
|
64
64
|
const executeAsyncCh = tracingChannel('orchestrion:@wdio/utils:executeAsync')
|
|
65
65
|
const launcherStartInstanceCh = tracingChannel('orchestrion:@wdio/cli:Launcher_startInstance')
|
|
66
|
+
const configParserShardCh = tracingChannel('orchestrion:@wdio/config:ConfigParser_shard')
|
|
66
67
|
const localRunnerRunCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_run')
|
|
67
68
|
const localRunnerShutdownCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_shutdown')
|
|
68
69
|
const testFrameworkFnWrapperCh = tracingChannel('orchestrion:@wdio/utils:testFrameworkFnWrapper')
|
|
@@ -87,6 +88,7 @@ if (loadCh.hasSubscribers) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const coordinatorStates = new WeakMap()
|
|
91
|
+
const emptyShardConfigurations = new WeakSet()
|
|
90
92
|
const localRunnerVersions = new WeakMap()
|
|
91
93
|
const rumBrowsers = new Set()
|
|
92
94
|
const rumCorrelationBrowsers = new Set()
|
|
@@ -936,6 +938,7 @@ function waitForRumTestStart (context) {
|
|
|
936
938
|
* @property {string} framework
|
|
937
939
|
* @property {string|undefined} rootDir
|
|
938
940
|
* @property {typeof process.env|undefined} runnerEnv
|
|
941
|
+
* @property {{total: number}|undefined} shard
|
|
939
942
|
*/
|
|
940
943
|
|
|
941
944
|
/**
|
|
@@ -1806,17 +1809,18 @@ function getSessionStatus (state) {
|
|
|
1806
1809
|
}
|
|
1807
1810
|
|
|
1808
1811
|
/**
|
|
1809
|
-
*
|
|
1812
|
+
* Explains a skipped session using the workers' discovery results.
|
|
1810
1813
|
*
|
|
1811
1814
|
* @param {CoordinatorState} state
|
|
1812
1815
|
*/
|
|
1813
|
-
function
|
|
1814
|
-
if (state.workers.size === 0)
|
|
1815
|
-
|
|
1816
|
+
function getTestSessionEmptyReason (state) {
|
|
1817
|
+
if (state.workers.size === 0 && emptyShardConfigurations.has(getRunnerConfiguration(state.localRunner))) {
|
|
1818
|
+
return 'zero_test_shard'
|
|
1819
|
+
}
|
|
1816
1820
|
for (const workerRecord of state.workers) {
|
|
1817
|
-
if (workerRecord.hasTests !== false) return
|
|
1821
|
+
if (workerRecord.hasTests !== false) return 'all_tests_skipped'
|
|
1818
1822
|
}
|
|
1819
|
-
return
|
|
1823
|
+
return 'zero_tests'
|
|
1820
1824
|
}
|
|
1821
1825
|
|
|
1822
1826
|
/**
|
|
@@ -1832,9 +1836,9 @@ function finishCoordinator (state, error, onDone) {
|
|
|
1832
1836
|
return
|
|
1833
1837
|
}
|
|
1834
1838
|
if (!state.sessionStarted) {
|
|
1835
|
-
if (!error &&
|
|
1836
|
-
|
|
1837
|
-
|
|
1839
|
+
if (!error && state.workers.size === 0 &&
|
|
1840
|
+
!emptyShardConfigurations.has(getRunnerConfiguration(state.localRunner))) {
|
|
1841
|
+
error = new Error('No test files were found.')
|
|
1838
1842
|
}
|
|
1839
1843
|
initializeCoordinator(state, () => finishCoordinator(state, error, onDone))
|
|
1840
1844
|
return
|
|
@@ -1861,7 +1865,7 @@ function finishCoordinator (state, error, onDone) {
|
|
|
1861
1865
|
const status = error ? 'fail' : getSessionStatus(state)
|
|
1862
1866
|
testSessionFinishCh.publish({
|
|
1863
1867
|
status,
|
|
1864
|
-
|
|
1868
|
+
testSessionEmptyReason: status === 'skip' ? getTestSessionEmptyReason(state) : undefined,
|
|
1865
1869
|
error,
|
|
1866
1870
|
isEarlyFlakeDetectionEnabled: state.configuration.isEarlyFlakeDetectionEnabled,
|
|
1867
1871
|
isEarlyFlakeDetectionFaulty: state.configuration.isEarlyFlakeDetectionFaulty,
|
|
@@ -1976,6 +1980,17 @@ jasmineAdapterInitCh.subscribe({
|
|
|
1976
1980
|
|
|
1977
1981
|
// dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
|
|
1978
1982
|
// @ts-expect-error
|
|
1983
|
+
configParserShardCh.subscribe({
|
|
1984
|
+
end (context) {
|
|
1985
|
+
const config = context.self._config
|
|
1986
|
+
if (testFinishCh.hasSubscribers && config?.shard?.total > 1 &&
|
|
1987
|
+
context.arguments[0].length > 0 && context.result?.length === 0) {
|
|
1988
|
+
emptyShardConfigurations.add(config)
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
// @ts-expect-error See the partial tracing-channel subscriber above.
|
|
1979
1994
|
launcherStartInstanceCh.subscribe({
|
|
1980
1995
|
start (context) {
|
|
1981
1996
|
const localRunner = context.self?.runner
|
|
@@ -2050,10 +2065,11 @@ localRunnerRunCh.subscribe({
|
|
|
2050
2065
|
// @ts-expect-error See the partial tracing-channel subscriber above.
|
|
2051
2066
|
localRunnerShutdownCh.subscribe({
|
|
2052
2067
|
asyncEnd (context) {
|
|
2053
|
-
const
|
|
2054
|
-
if (!
|
|
2068
|
+
const runnerConfiguration = getRunnerConfiguration(context.self)
|
|
2069
|
+
if (!testFinishCh.hasSubscribers || !SUPPORTED_FRAMEWORK_ADAPTERS.has(runnerConfiguration?.framework)) {
|
|
2055
2070
|
return
|
|
2056
2071
|
}
|
|
2072
|
+
const state = getCoordinatorState(context.self)
|
|
2057
2073
|
|
|
2058
2074
|
// Orchestrion uses the callback for the matching settlement path to delay LocalRunner.shutdown.
|
|
2059
2075
|
const waitForCoordinator = onDone => {
|
|
@@ -94,9 +94,22 @@ function wrapSend (send) {
|
|
|
94
94
|
return send.apply(this, arguments)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const socket = this._sender?._socket
|
|
98
|
+
// No socket is attached before the handshake completes or after it is torn down.
|
|
99
|
+
if (!socket) {
|
|
100
|
+
return send.apply(this, arguments)
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
const [data, options, cb] = arguments
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
// `ws` stringifies numbers, then spreads `options` over its own inferred default, so an
|
|
106
|
+
// explicitly passed `binary` wins even when it is nullish and the frame goes out as text.
|
|
107
|
+
const payload = typeof data === 'number' ? data.toString() : data
|
|
108
|
+
const binary = options != null && Object.hasOwn(options, 'binary')
|
|
109
|
+
? Boolean(options.binary)
|
|
110
|
+
: typeof payload !== 'string'
|
|
111
|
+
const byteLength = sentDataLength(payload)
|
|
112
|
+
const ctx = { data, binary, socket, byteLength }
|
|
100
113
|
|
|
101
114
|
return typeof cb === 'function'
|
|
102
115
|
? producerCh.traceCallback(send, undefined, ctx, this, data, options, cb)
|
|
@@ -241,6 +254,33 @@ addHook({
|
|
|
241
254
|
return Receiver
|
|
242
255
|
})
|
|
243
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Byte length of an outgoing payload, following the coercion `ws` applies in `Sender.send`:
|
|
259
|
+
* strings and blobs are measured directly and everything else goes through `toBuffer()`, which
|
|
260
|
+
* yields one byte per element for plain arrays rather than concatenating them. Blob-likes carry
|
|
261
|
+
* neither a `byteLength` nor an array shape, so they are matched last to keep the common
|
|
262
|
+
* buffer path first.
|
|
263
|
+
*
|
|
264
|
+
* @param {WebSocketMessageData | number | undefined} data
|
|
265
|
+
*/
|
|
266
|
+
function sentDataLength (data) {
|
|
267
|
+
if (typeof data === 'string') {
|
|
268
|
+
return Buffer.byteLength(data)
|
|
269
|
+
}
|
|
270
|
+
// An array is one byte per element even when it carries an unrelated `byteLength`.
|
|
271
|
+
if (Array.isArray(data)) {
|
|
272
|
+
return data.length
|
|
273
|
+
}
|
|
274
|
+
// Covers views as well as `ArrayBuffer` and `SharedArrayBuffer`.
|
|
275
|
+
if (typeof (/** @type {{ byteLength?: unknown }} */ (data)?.byteLength) === 'number') {
|
|
276
|
+
return /** @type {{ byteLength: number }} */ (data).byteLength
|
|
277
|
+
}
|
|
278
|
+
// Blobs and files, detected the way `ws` does: structurally, so shimmed and cross-realm
|
|
279
|
+
// implementations it accepts are measured instead of silently reported as empty.
|
|
280
|
+
const { size } = /** @type {{ size?: unknown }} */ (data) ?? {}
|
|
281
|
+
return typeof size === 'number' ? size : 0
|
|
282
|
+
}
|
|
283
|
+
|
|
244
284
|
/**
|
|
245
285
|
* @param {WebSocketMessageData} data
|
|
246
286
|
*/
|
|
@@ -23,6 +23,15 @@ const PROVIDER = {
|
|
|
23
23
|
MISTRAL: 'MISTRAL',
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{
|
|
28
|
+
* inputTokens?: number,
|
|
29
|
+
* outputTokens?: number,
|
|
30
|
+
* cacheReadTokens?: number,
|
|
31
|
+
* cacheWriteTokens?: number,
|
|
32
|
+
* }} StreamedUsage
|
|
33
|
+
*/
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Coerce the chunks into a single response body.
|
|
28
37
|
*
|
|
@@ -44,10 +53,7 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
|
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
let message = ''
|
|
47
|
-
let
|
|
48
|
-
let outputTokens = 0
|
|
49
|
-
let cacheReadTokens = 0
|
|
50
|
-
let cacheWriteTokens = 0
|
|
56
|
+
let usage
|
|
51
57
|
|
|
52
58
|
for (const { chunk: { bytes } } of chunks) {
|
|
53
59
|
const body = JSON.parse(Buffer.from(bytes).toString('utf8'))
|
|
@@ -56,8 +62,6 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
|
|
|
56
62
|
case PROVIDER.AMAZON: {
|
|
57
63
|
if (body?.outputText) {
|
|
58
64
|
message += body?.outputText
|
|
59
|
-
inputTokens = body?.inputTextTokenCount
|
|
60
|
-
outputTokens = body?.totalOutputTextTokenCount
|
|
61
65
|
} else if (body?.contentBlockDelta?.delta?.text) {
|
|
62
66
|
message += body.contentBlockDelta.delta.text
|
|
63
67
|
}
|
|
@@ -79,9 +83,6 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
|
|
|
79
83
|
message += body.delta.text
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
if (body.message?.usage?.input_tokens) inputTokens = body.message.usage.input_tokens
|
|
83
|
-
if (body.message?.usage?.output_tokens) outputTokens = body.message.usage.output_tokens
|
|
84
|
-
|
|
85
86
|
break
|
|
86
87
|
}
|
|
87
88
|
case PROVIDER.COHERE: {
|
|
@@ -101,24 +102,61 @@ function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelNam
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
usage = mergeStreamedUsage(usage, body, modelProviderUpper)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Generation({ message, role: 'assistant', ...usage })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Merge the token counts one streamed `invokeModel` frame reports into the running totals. Each
|
|
113
|
+
* provider spells them differently, and most report none until the trailing invocation metrics,
|
|
114
|
+
* so a frame carrying no counts leaves the totals as they were.
|
|
115
|
+
*
|
|
116
|
+
* @param {StreamedUsage} [usage]
|
|
117
|
+
* @param {object} body parsed chunk body
|
|
118
|
+
* @param {string} modelProviderUpper
|
|
119
|
+
* @returns {StreamedUsage | undefined}
|
|
120
|
+
*/
|
|
121
|
+
function mergeStreamedUsage (usage, body, modelProviderUpper) {
|
|
122
|
+
// by default, it seems newer versions of the AWS SDK include the input/output token counts in
|
|
123
|
+
// the response body; any provider can send them, and they supersede what the frames reported
|
|
124
|
+
const invocationMetrics = body['amazon-bedrock-invocationMetrics']
|
|
125
|
+
if (invocationMetrics) {
|
|
126
|
+
return {
|
|
127
|
+
inputTokens: invocationMetrics.inputTokenCount,
|
|
128
|
+
outputTokens: invocationMetrics.outputTokenCount,
|
|
129
|
+
cacheReadTokens: invocationMetrics.cacheReadInputTokenCount,
|
|
130
|
+
cacheWriteTokens: invocationMetrics.cacheWriteInputTokenCount,
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
switch (modelProviderUpper) {
|
|
135
|
+
case PROVIDER.AMAZON: {
|
|
136
|
+
const { inputTextTokenCount, totalOutputTextTokenCount } = body
|
|
137
|
+
if (inputTextTokenCount === undefined && totalOutputTextTokenCount === undefined) return usage
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...usage,
|
|
141
|
+
inputTokens: inputTextTokenCount ?? usage?.inputTokens,
|
|
142
|
+
outputTokens: totalOutputTextTokenCount ?? usage?.outputTokens,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
case PROVIDER.ANTHROPIC: {
|
|
146
|
+
// `message_start` reports the initial counts under `message.usage`; the closing
|
|
147
|
+
// `message_delta` reports the final output count at the top level
|
|
148
|
+
const chunkUsage = body.message?.usage ?? body.usage
|
|
149
|
+
if (!chunkUsage) return usage
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...usage,
|
|
153
|
+
inputTokens: chunkUsage.input_tokens ?? usage?.inputTokens,
|
|
154
|
+
outputTokens: chunkUsage.output_tokens ?? usage?.outputTokens,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return usage
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
class Generation {
|
|
@@ -568,13 +606,27 @@ function resolveToolResultItem (item) {
|
|
|
568
606
|
return `[Unsupported content type(s): ${getContentBlockType(item)}]`
|
|
569
607
|
}
|
|
570
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Normalize a Converse usage object onto the LLMObs metric names. Returns undefined when the
|
|
611
|
+
* response reported no counts at all, so a caller cannot mistake an empty record for a measurement.
|
|
612
|
+
*
|
|
613
|
+
* @param {object} [usage]
|
|
614
|
+
* @returns {{
|
|
615
|
+
* inputTokens?: number, outputTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number
|
|
616
|
+
* } | undefined}
|
|
617
|
+
*/
|
|
571
618
|
function buildUsage (usage = {}) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
619
|
+
const inputTokens = usage.inputTokens
|
|
620
|
+
const outputTokens = usage.outputTokens
|
|
621
|
+
const cacheReadTokens = usage.cacheReadInputTokens ?? usage.cacheReadInputTokenCount
|
|
622
|
+
const cacheWriteTokens = usage.cacheWriteInputTokens ?? usage.cacheWriteInputTokenCount
|
|
623
|
+
|
|
624
|
+
if (inputTokens === undefined && outputTokens === undefined &&
|
|
625
|
+
cacheReadTokens === undefined && cacheWriteTokens === undefined) {
|
|
626
|
+
return
|
|
577
627
|
}
|
|
628
|
+
|
|
629
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
|
|
578
630
|
}
|
|
579
631
|
|
|
580
632
|
/**
|
|
@@ -714,6 +766,8 @@ function extractTextAndResponseReasonConverseFromStream (chunks) {
|
|
|
714
766
|
|
|
715
767
|
module.exports = {
|
|
716
768
|
Generation,
|
|
769
|
+
buildUsage,
|
|
770
|
+
mergeStreamedUsage,
|
|
717
771
|
RequestParams,
|
|
718
772
|
extractTextAndResponseReasonFromStream,
|
|
719
773
|
parseModelId,
|
|
@@ -73,7 +73,7 @@ class CucumberPlugin extends CiPlugin {
|
|
|
73
73
|
isEarlyFlakeDetectionFaulty,
|
|
74
74
|
isTestManagementTestsEnabled,
|
|
75
75
|
isParallel,
|
|
76
|
-
|
|
76
|
+
testSessionEmptyReason,
|
|
77
77
|
error,
|
|
78
78
|
onDone,
|
|
79
79
|
}) => {
|
|
@@ -118,12 +118,11 @@ class CucumberPlugin extends CiPlugin {
|
|
|
118
118
|
|
|
119
119
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
120
120
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
121
|
-
if (
|
|
121
|
+
if (testSessionEmptyReason) {
|
|
122
122
|
setExpectedEmptyTestSessionTags(
|
|
123
123
|
this.testSessionSpan,
|
|
124
124
|
this.testModuleSpan,
|
|
125
|
-
|
|
126
|
-
'zero_tests'
|
|
125
|
+
testSessionEmptyReason
|
|
127
126
|
)
|
|
128
127
|
}
|
|
129
128
|
if (error) {
|
|
@@ -450,7 +449,8 @@ class CucumberPlugin extends CiPlugin {
|
|
|
450
449
|
})
|
|
451
450
|
|
|
452
451
|
this.addSub('ci:cucumber:is-modified-test', ({
|
|
453
|
-
|
|
452
|
+
gherkinNodes,
|
|
453
|
+
gherkinScopeRanges,
|
|
454
454
|
testFileAbsolutePath,
|
|
455
455
|
modifiedFiles,
|
|
456
456
|
stepIds,
|
|
@@ -458,11 +458,25 @@ class CucumberPlugin extends CiPlugin {
|
|
|
458
458
|
setIsModified,
|
|
459
459
|
}) => {
|
|
460
460
|
const testScenarioPath = getTestSuitePath(testFileAbsolutePath, this.repositoryRoot || process.cwd())
|
|
461
|
-
for (const
|
|
461
|
+
for (const [startLine, endLine] of gherkinScopeRanges) {
|
|
462
|
+
if (isModifiedTest(testScenarioPath, startLine, endLine, modifiedFiles, 'cucumber')) {
|
|
463
|
+
setIsModified(true)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const gherkinNode of gherkinNodes) {
|
|
468
|
+
const lastStep = gherkinNode.steps.at(-1)
|
|
469
|
+
let endLine = lastStep?.location.line ?? gherkinNode.location.line
|
|
470
|
+
if (lastStep?.dataTable?.rows.length) {
|
|
471
|
+
endLine = lastStep.dataTable.rows.at(-1).location.line
|
|
472
|
+
} else if (lastStep?.docString) {
|
|
473
|
+
const { content, location } = lastStep.docString
|
|
474
|
+
endLine = location.line + (content ? content.split('\n').length + 1 : 1)
|
|
475
|
+
}
|
|
462
476
|
const isModified = isModifiedTest(
|
|
463
477
|
testScenarioPath,
|
|
464
|
-
|
|
465
|
-
|
|
478
|
+
gherkinNode.location.line,
|
|
479
|
+
endLine,
|
|
466
480
|
modifiedFiles,
|
|
467
481
|
'cucumber'
|
|
468
482
|
)
|
|
@@ -94,6 +94,7 @@ const {
|
|
|
94
94
|
const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
|
|
95
95
|
const { RESOURCE_NAME } = require('../../../ext/tags')
|
|
96
96
|
const getConfig = require('../../dd-trace/src/config')
|
|
97
|
+
const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
|
|
97
98
|
const {
|
|
98
99
|
SCREENSHOT_UPLOAD_RESULT_ERROR,
|
|
99
100
|
SCREENSHOT_UPLOAD_RESULT_UPLOADED,
|
|
@@ -242,7 +243,7 @@ function getSessionStatus (summary, testSuiteStatuses) {
|
|
|
242
243
|
if (summary.status === 'failed' || summary.failures > 0 || summary.totalFailed > 0) {
|
|
243
244
|
return 'fail'
|
|
244
245
|
}
|
|
245
|
-
if (summary.totalSkipped
|
|
246
|
+
if ((summary.totalSkipped ?? 0) + (summary.totalPending ?? 0) === summary.totalTests) {
|
|
246
247
|
return 'skip'
|
|
247
248
|
}
|
|
248
249
|
return 'pass'
|
|
@@ -381,7 +382,7 @@ function getSuiteStatus (suiteStats) {
|
|
|
381
382
|
return 'fail'
|
|
382
383
|
}
|
|
383
384
|
if (suiteStats.tests !== undefined &&
|
|
384
|
-
|
|
385
|
+
suiteStats.tests === (suiteStats.pending ?? 0) + (suiteStats.skipped ?? 0)) {
|
|
385
386
|
return 'skip'
|
|
386
387
|
}
|
|
387
388
|
return 'pass'
|
|
@@ -451,6 +452,8 @@ function getFinalStatus ({
|
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
class CypressPlugin {
|
|
455
|
+
#pendingTestSuiteSpans = new Map()
|
|
456
|
+
|
|
454
457
|
_isInit = false
|
|
455
458
|
testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
|
|
456
459
|
|
|
@@ -548,6 +551,7 @@ class CypressPlugin {
|
|
|
548
551
|
*
|
|
549
552
|
*/
|
|
550
553
|
resetRunState () {
|
|
554
|
+
this.#pendingTestSuiteSpans.clear()
|
|
551
555
|
this._isInit = false
|
|
552
556
|
this.finishedTestsByFile = {}
|
|
553
557
|
this.hasTestsReported = false
|
|
@@ -854,6 +858,12 @@ class CypressPlugin {
|
|
|
854
858
|
|
|
855
859
|
const testOptimizationConfig = getConfig().testOptimization
|
|
856
860
|
this.rumFlushWaitMillis = testOptimizationConfig.DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS
|
|
861
|
+
if (getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED', true) === true) {
|
|
862
|
+
cypressConfig.screenshotOnRunFailure = true
|
|
863
|
+
}
|
|
864
|
+
if (getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED', true) === true) {
|
|
865
|
+
cypressConfig.video = true
|
|
866
|
+
}
|
|
857
867
|
this.warnIfMisconfiguredTestFailureScreenshots(cypressConfig, tracer, testOptimizationConfig)
|
|
858
868
|
this.warnIfMisconfiguredTestFailureVideos(cypressConfig, tracer, testOptimizationConfig)
|
|
859
869
|
|
|
@@ -1048,7 +1058,7 @@ class CypressPlugin {
|
|
|
1048
1058
|
}
|
|
1049
1059
|
}
|
|
1050
1060
|
|
|
1051
|
-
|
|
1061
|
+
const testSuiteSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_suite`, {
|
|
1052
1062
|
childOf: this.testModuleSpan,
|
|
1053
1063
|
tags: {
|
|
1054
1064
|
[COMPONENT]: TEST_FRAMEWORK_NAME,
|
|
@@ -1057,9 +1067,13 @@ class CypressPlugin {
|
|
|
1057
1067
|
},
|
|
1058
1068
|
integrationName: TEST_FRAMEWORK_NAME,
|
|
1059
1069
|
})
|
|
1070
|
+
this.#pendingTestSuiteSpans.set(testSuite, testSuiteSpan)
|
|
1071
|
+
return testSuiteSpan
|
|
1060
1072
|
}
|
|
1061
1073
|
|
|
1062
|
-
getTestSpan ({
|
|
1074
|
+
getTestSpan ({
|
|
1075
|
+
testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined, startTime,
|
|
1076
|
+
}) {
|
|
1063
1077
|
const testSuiteTags = {
|
|
1064
1078
|
[TEST_MODULE]: TEST_FRAMEWORK_NAME,
|
|
1065
1079
|
}
|
|
@@ -1116,6 +1130,7 @@ class CypressPlugin {
|
|
|
1116
1130
|
|
|
1117
1131
|
return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
|
|
1118
1132
|
childOf,
|
|
1133
|
+
startTime,
|
|
1119
1134
|
tags: {
|
|
1120
1135
|
[COMPONENT]: TEST_FRAMEWORK_NAME,
|
|
1121
1136
|
[ORIGIN_KEY]: CI_APP_ORIGIN,
|
|
@@ -1348,6 +1363,20 @@ class CypressPlugin {
|
|
|
1348
1363
|
}
|
|
1349
1364
|
|
|
1350
1365
|
afterRun (suiteStats, error) {
|
|
1366
|
+
const pendingFinalizations = []
|
|
1367
|
+
// Late manual initialization cannot intercept a later replacement of after:spec.
|
|
1368
|
+
// Run results let us finalize the suites whose browser hooks already created spans.
|
|
1369
|
+
if (this._isInit && suiteStats?.runs) {
|
|
1370
|
+
for (const result of suiteStats.runs) {
|
|
1371
|
+
const testSuiteSpan = this.#pendingTestSuiteSpans.get(result.spec?.relative)
|
|
1372
|
+
if (!testSuiteSpan) continue
|
|
1373
|
+
this.testSuiteSpan = testSuiteSpan
|
|
1374
|
+
const endedAt = Date.parse(result.stats?.endedAt)
|
|
1375
|
+
const finalization = this.afterSpec(result.spec, result, undefined,
|
|
1376
|
+
Number.isFinite(endedAt) ? endedAt : undefined)
|
|
1377
|
+
if (finalization) pendingFinalizations.push(finalization)
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1351
1380
|
const hasPendingVideoSpans = this.pendingVideoUploads.length > 0
|
|
1352
1381
|
const videoUploadsPromise = this.uploadPendingTestSuiteVideos()
|
|
1353
1382
|
if (!this._isInit) {
|
|
@@ -1358,7 +1387,11 @@ class CypressPlugin {
|
|
|
1358
1387
|
}
|
|
1359
1388
|
return
|
|
1360
1389
|
}
|
|
1361
|
-
|
|
1390
|
+
// Recovered screenshot-only tests must finish before the run closes any remaining trace spans.
|
|
1391
|
+
const finalizeRun = () => this.#finalizeRun(suiteStats, error, hasPendingVideoSpans)
|
|
1392
|
+
const finalizationPromise = pendingFinalizations.length > 0
|
|
1393
|
+
? Promise.all(pendingFinalizations).then(finalizeRun)
|
|
1394
|
+
: finalizeRun()
|
|
1362
1395
|
if (videoUploadsPromise) {
|
|
1363
1396
|
return Promise.all([videoUploadsPromise, finalizationPromise])
|
|
1364
1397
|
.then(() => this.#flushExporter(false))
|
|
@@ -1384,12 +1417,11 @@ class CypressPlugin {
|
|
|
1384
1417
|
|
|
1385
1418
|
this.testModuleSpan.setTag(TEST_STATUS, testStatus)
|
|
1386
1419
|
this.testSessionSpan.setTag(TEST_STATUS, testStatus)
|
|
1387
|
-
if (testStatus !== 'fail' && hasNoTests) {
|
|
1420
|
+
if (testStatus !== 'fail' && (hasNoTests || testStatus === 'skip')) {
|
|
1388
1421
|
setExpectedEmptyTestSessionTags(
|
|
1389
1422
|
this.testSessionSpan,
|
|
1390
1423
|
this.testModuleSpan,
|
|
1391
|
-
'
|
|
1392
|
-
'zero_tests'
|
|
1424
|
+
hasNoTests ? 'zero_tests' : 'all_tests_skipped'
|
|
1393
1425
|
)
|
|
1394
1426
|
}
|
|
1395
1427
|
if (error) {
|
|
@@ -1504,10 +1536,10 @@ class CypressPlugin {
|
|
|
1504
1536
|
}
|
|
1505
1537
|
}
|
|
1506
1538
|
|
|
1507
|
-
afterSpec (spec, results, error) {
|
|
1539
|
+
afterSpec (spec, results, error, finishTime) {
|
|
1508
1540
|
const { tests, stats, screenshots, video, error: resultError } = results || {}
|
|
1509
1541
|
const cypressTests = tests || []
|
|
1510
|
-
if (cypressTests.length > 0) this.hasTestsReported = true
|
|
1542
|
+
if (cypressTests.length > 0 || stats?.tests > 0) this.hasTestsReported = true
|
|
1511
1543
|
const specScreenshots = screenshots || []
|
|
1512
1544
|
const finishedTests = this.finishedTestsByFile[spec.relative] || []
|
|
1513
1545
|
const screenshotUploadPromises = []
|
|
@@ -1540,7 +1572,10 @@ class CypressPlugin {
|
|
|
1540
1572
|
? getTestSuitePath(spec.absolute, this.repositoryRoot)
|
|
1541
1573
|
: spec.relative
|
|
1542
1574
|
|
|
1543
|
-
|
|
1575
|
+
// Recovered tests that never ran have no execution time; keep them at the recorded suite end.
|
|
1576
|
+
const skippedTestSpan = this.getTestSpan({
|
|
1577
|
+
testName: cypressTestName, testSuite: spec.relative, testSourceFile, startTime: finishTime,
|
|
1578
|
+
})
|
|
1544
1579
|
skippedTestSpan.setTag(TEST_FINAL_STATUS, 'skip')
|
|
1545
1580
|
|
|
1546
1581
|
skippedTestSpan.setTag(TEST_STATUS, 'skip')
|
|
@@ -1578,7 +1613,7 @@ class CypressPlugin {
|
|
|
1578
1613
|
})
|
|
1579
1614
|
}
|
|
1580
1615
|
|
|
1581
|
-
testSpanFinishes.push({ testSpan: skippedTestSpan, finishTime: this._now() })
|
|
1616
|
+
testSpanFinishes.push({ testSpan: skippedTestSpan, finishTime: finishTime ?? this._now() })
|
|
1582
1617
|
}
|
|
1583
1618
|
|
|
1584
1619
|
// Make sure that reported test statuses are the same as Cypress reports.
|
|
@@ -1749,7 +1784,7 @@ class CypressPlugin {
|
|
|
1749
1784
|
}
|
|
1750
1785
|
}
|
|
1751
1786
|
|
|
1752
|
-
const testSuiteFinishTime = this._now()
|
|
1787
|
+
const testSuiteFinishTime = finishTime ?? this._now()
|
|
1753
1788
|
const suiteError = error || resultError || latestError
|
|
1754
1789
|
const suiteStatus = suiteError ||
|
|
1755
1790
|
cypressTests.some(test => CYPRESS_STATUS_TO_TEST_STATUS[test.state] === 'fail')
|
|
@@ -1771,6 +1806,7 @@ class CypressPlugin {
|
|
|
1771
1806
|
testSuiteSpan.setTag(TEST_STATUS, suiteStatus)
|
|
1772
1807
|
if (suiteError) testSuiteSpan.setTag('error', suiteError)
|
|
1773
1808
|
this.testSuiteSpan = null
|
|
1809
|
+
this.#pendingTestSuiteSpans.delete(spec.relative)
|
|
1774
1810
|
}
|
|
1775
1811
|
|
|
1776
1812
|
let testSpansPromise
|
|
@@ -2034,7 +2070,8 @@ class CypressPlugin {
|
|
|
2034
2070
|
rumTestExecutionIdCookieName: RUM_TEST_EXECUTION_ID_COOKIE_NAME,
|
|
2035
2071
|
}
|
|
2036
2072
|
|
|
2037
|
-
this.testSuiteSpan
|
|
2073
|
+
this.testSuiteSpan = this.#pendingTestSuiteSpans.get(testSuite) ||
|
|
2074
|
+
this.getTestSuiteSpan({ testSuite, testSuiteAbsolutePath })
|
|
2038
2075
|
return suitePayload
|
|
2039
2076
|
},
|
|
2040
2077
|
'dd:beforeEach': (test) => {
|