dd-trace 6.18.0 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -198,11 +198,11 @@ function wrapSmithySend (send) {
198
198
  }
199
199
 
200
200
  return channels.start.runStores(ctx, () => {
201
- // When the region is not set this never resolves so we can't await.
201
+ // Resolve region tags independently; send reports provider failures through its promise or callback.
202
202
  this.config.region().then(region => {
203
203
  ctx.region = region
204
204
  channels.region.publish(ctx)
205
- })
205
+ }, () => {})
206
206
 
207
207
  if (cbExists) {
208
208
  args[args.length - 1] = shimmer.wrapCallback(cb, cb => function (err, result) {
@@ -44,6 +44,7 @@ const chunkEmitTimes = new WeakMap()
44
44
  * tools: Map<string, ToolState>,
45
45
  * subagents: Map<string, Record<string, unknown>>,
46
46
  * prompt?: string,
47
+ * systemPrompt?: string | string[] | { type: string, prompt?: string | string[], append?: string },
47
48
  * model?: string,
48
49
  * resume?: string,
49
50
  * maxTurns?: number,
@@ -235,6 +236,7 @@ function onQueryStart (ctx) {
235
236
  const prompt = queryArg.prompt
236
237
  const sessionCtx = {
237
238
  prompt: typeof prompt === 'string' ? prompt : undefined,
239
+ systemPrompt: options.systemPrompt,
238
240
  model: options.model,
239
241
  resume: options.resume,
240
242
  maxTurns: options.maxTurns,
@@ -451,6 +453,7 @@ function processStep (
451
453
  llmCh.traceSync(() => {}, {
452
454
  model,
453
455
  usage,
456
+ systemPrompt: parentToolUseId ? undefined : sessionCtx?.systemPrompt,
454
457
  startTime: stepStartTime,
455
458
  finishTime: chunkEmitTimes.get(chunks[messageEndIdx - 1]),
456
459
  chunks,
@@ -529,7 +532,8 @@ function processChunks (chunks, agentCtx) {
529
532
  const sessionCtx = agentCtx.sessionCtx
530
533
  const getLifecycle = createStreamLookup(chunks)
531
534
 
532
- const { type, subtype, ...rest } = chunks[0]
535
+ const initChunk = chunks.find(chunk => chunk.type === 'system' && chunk.subtype === 'init') || chunks[0]
536
+ const { type, subtype, ...rest } = initChunk
533
537
  Object.assign(agentCtx, rest)
534
538
  if (sessionCtx) {
535
539
  if (sessionCtx.sessionId) agentCtx.session_id = sessionCtx.sessionId
@@ -1418,13 +1418,49 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
1418
1418
 
1419
1419
  if (isImpactedTestsEnabled) {
1420
1420
  const setIsModified = (receivedIsModified) => { isModified = receivedIsModified }
1421
- const scenarios = gherkinDocument.feature?.children?.filter(
1422
- children => pickle.astNodeIds.includes(children.scenario.id)
1423
- ).map(scenario => scenario.scenario)
1421
+ const gherkinNodes = []
1422
+ // Prefix ranges catch removed Backgrounds, which have no AST node in the target document.
1423
+ const gherkinScopeRanges = []
1424
+ const feature = gherkinDocument.feature
1425
+ const featureChildren = feature?.children
1426
+ if (featureChildren?.length) {
1427
+ const firstFeatureChild = featureChildren[0]
1428
+ const firstFeatureNode = firstFeatureChild.background ?? firstFeatureChild.rule ?? firstFeatureChild.scenario
1429
+ const firstFeatureLine = firstFeatureNode.tags?.[0]?.location.line ?? firstFeatureNode.location.line
1430
+ const featurePrefix = [feature.location.line, firstFeatureLine - 1]
1431
+ let featureBackground
1432
+ for (const child of featureChildren) {
1433
+ if (child.background) {
1434
+ featureBackground = child.background
1435
+ continue
1436
+ }
1437
+ const rule = child.rule
1438
+ const children = rule?.children ?? [child]
1439
+ let ruleBackground
1440
+ for (const { background } of children) {
1441
+ if (background) ruleBackground = background
1442
+ }
1443
+ for (const { scenario } of children) {
1444
+ if (scenario && pickle.astNodeIds.includes(scenario.id)) {
1445
+ if (!featureBackground) gherkinScopeRanges.push(featurePrefix)
1446
+ if (rule?.children?.length && !ruleBackground) {
1447
+ const firstRuleChild = rule.children.at(0)
1448
+ const firstRuleNode = firstRuleChild.background ?? firstRuleChild.scenario
1449
+ const firstRuleLine = firstRuleNode.tags?.[0]?.location.line ?? firstRuleNode.location.line
1450
+ gherkinScopeRanges.push([rule.location.line, firstRuleLine - 1])
1451
+ }
1452
+ if (featureBackground) gherkinNodes.push(featureBackground)
1453
+ if (ruleBackground) gherkinNodes.push(ruleBackground)
1454
+ gherkinNodes.push(scenario)
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1424
1459
  const stepIds = testCase?.testSteps?.flatMap(testStep => testStep.stepDefinitionIds)
1425
1460
 
1426
1461
  isModifiedCh.publish({
1427
- scenarios,
1462
+ gherkinNodes,
1463
+ gherkinScopeRanges,
1428
1464
  testFileAbsolutePath: gherkinDocument.uri,
1429
1465
  modifiedFiles,
1430
1466
  stepIds,
@@ -2,12 +2,6 @@
2
2
 
3
3
  module.exports = {
4
4
  // Only list unprefixed node modules. They will automatically be instrumented as prefixed and unprefixed.
5
- '@supabase/auth-js': { esmFirst: true, fn: () => require('../supabase') },
6
- '@supabase/functions-js': { esmFirst: true, fn: () => require('../supabase') },
7
- '@supabase/postgrest-js': { esmFirst: true, fn: () => require('../supabase') },
8
- '@supabase/realtime-js': { esmFirst: true, fn: () => require('../supabase') },
9
- '@supabase/storage-js': { esmFirst: true, fn: () => require('../supabase') },
10
- '@supabase/supabase-js': { esmFirst: true, fn: () => require('../supabase') },
11
5
  child_process: () => require('../child_process'),
12
6
  crypto: () => require('../crypto'),
13
7
  dns: () => require('../dns'),
@@ -142,7 +136,6 @@ module.exports = {
142
136
  'pino-pretty': () => require('../pino'),
143
137
  playwright: () => require('../playwright'),
144
138
  'playwright-core': () => require('../playwright'),
145
- postgres: { esmFirst: true, fn: () => require('../postgres') },
146
139
  'promise-js': () => require('../promise-js'),
147
140
  promise: () => require('../promise'),
148
141
  protobufjs: () => require('../protobufjs'),
@@ -17,10 +17,10 @@ const registry = [
17
17
  { instrumentations: require('./instrumentations/modelcontextprotocol-sdk') },
18
18
  { instrumentations: require('./instrumentations/openai-agents') },
19
19
  { instrumentations: require('./instrumentations/playwright') },
20
- { instrumentations: require('./instrumentations/postgres') },
20
+ { activate: true, instrumentations: require('./instrumentations/postgres') },
21
21
  { instrumentations: require('./instrumentations/webdriverio') },
22
22
  { instrumentations: require('./instrumentations/aws-durable-execution-sdk-js') },
23
- { instrumentations: require('./instrumentations/supabase') },
23
+ { activate: true, instrumentations: require('./instrumentations/supabase') },
24
24
  ]
25
25
 
26
26
  const activatedModules = new Set()
@@ -88,6 +88,31 @@ module.exports = [
88
88
  },
89
89
  channelName: 'resolveLanguageModel',
90
90
  },
91
+ // AI SDK v7 filters runtime context from tracing channel events unless each key is included.
92
+ {
93
+ module: {
94
+ name: 'ai',
95
+ versionRange: '>=7.0.0',
96
+ filePath: 'dist/index.js',
97
+ },
98
+ functionQuery: {
99
+ functionName: 'generateText',
100
+ kind: 'Async',
101
+ },
102
+ channelName: 'includeRuntimeContext',
103
+ },
104
+ {
105
+ module: {
106
+ name: 'ai',
107
+ versionRange: '>=7.0.0',
108
+ filePath: 'dist/index.js',
109
+ },
110
+ functionQuery: {
111
+ functionName: 'streamText',
112
+ kind: 'Sync',
113
+ },
114
+ channelName: 'includeRuntimeContext',
115
+ },
91
116
  {
92
117
  module: {
93
118
  name: 'ai',
@@ -3,6 +3,14 @@
3
3
  // Playwright keeps several hook targets in private local classes/functions.
4
4
  // Keep these rewrites limited to bundled internals that addHook cannot wrap.
5
5
  module.exports = [
6
+ ...[
7
+ ['>=1.38.0 <1.60.0', 'lib/common/config.js'],
8
+ ['>=1.60.0', 'lib/common/index.js'],
9
+ ].map(([versionRange, filePath]) => ({
10
+ module: { name: 'playwright', versionRange, filePath },
11
+ functionQuery: { className: 'FullProjectInternal', methodName: 'constructor', kind: 'Sync' },
12
+ channelName: 'FullProjectInternal',
13
+ })),
6
14
  {
7
15
  module: {
8
16
  name: '@playwright/test',
@@ -77,6 +77,8 @@
77
77
  "jasmine-core/lib/jasmine-core/jasmine.js": "jasmine-core",
78
78
  "mercurius/index.js": "mercurius",
79
79
  "playwright-core/lib/coreBundle.js": "playwright-core",
80
+ "playwright/lib/common/config.js": "playwright",
81
+ "playwright/lib/common/index.js": "playwright",
80
82
  "playwright/lib/index.js": "playwright",
81
83
  "playwright/lib/runner/index.js": "playwright",
82
84
  "playwright/lib/runner/lastRun.js": "playwright",
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ const { stripVTControlCharacters } = require('node:util')
4
+
5
+ const { MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION: MAX_LENGTH } =
6
+ require('../../../dd-trace/src/encode/tags-processors')
7
+ const log = require('../../../dd-trace/src/log')
8
+
9
+ /**
10
+ * @typedef {{ name?: unknown, type?: unknown, message?: unknown, stack?: unknown }} SuiteError
11
+ * @typedef {object} JestResults
12
+ * @property {number} [numFailedTestSuites]
13
+ * @property {number} [numFailedTests]
14
+ * @property {{ testExecError?: SuiteError, failureMessage?: unknown, numFailingTests?: number }[]} [testResults]
15
+ */
16
+
17
+ /**
18
+ * Includes complete, deduplicated suite errors only when all details fit.
19
+ *
20
+ * @param {JestResults} results
21
+ * @returns {Error}
22
+ */
23
+ function getSessionError (results = {}) {
24
+ const { numFailedTestSuites = 0, numFailedTests = 0 } = results
25
+ const summary = `Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`
26
+ const error = new Error(summary)
27
+ const groups = new Map()
28
+ let size = 0
29
+ try {
30
+ const { testResults } = results
31
+ if (!testResults) return error
32
+ for (const result of testResults) {
33
+ let name, message, stack
34
+ try {
35
+ const { testExecError } = result
36
+ if (testExecError && !result.numFailingTests) {
37
+ name = readField(testExecError.name)
38
+ if (name === '') name = readField(testExecError.type)
39
+ message = readField(testExecError.message)
40
+ stack = readField(testExecError.stack)
41
+ } else {
42
+ // Jest's report includes assertion failures, hook errors and any suite execution error.
43
+ name = 'Error'
44
+ message = readField(result.failureMessage)
45
+ stack = message
46
+ }
47
+ } catch {
48
+ log.debug('Skipping unreadable Jest suite error in the test session summary')
49
+ continue
50
+ }
51
+ if (name === undefined || message === undefined || stack === undefined) return omitDetails(error)
52
+ if (!message && !stack) continue
53
+ name ||= 'Error'
54
+ const key = JSON.stringify([name, message, stack])
55
+ const group = groups.get(key)
56
+ if (group) {
57
+ group.count++
58
+ } else {
59
+ // Retained text must fit across error.message and error.stack; also bound grouping memory.
60
+ size += name.length + message.length + stack.length
61
+ if (size > 2 * MAX_LENGTH) return omitDetails(error)
62
+ groups.set(key, { name, message, stack, count: 1 })
63
+ }
64
+ }
65
+ } catch {
66
+ log.debug('Unable to read Jest results for the test session summary')
67
+ return error
68
+ }
69
+ if (!groups.size) return error
70
+
71
+ const errors = [...groups.keys()].sort().map(key => groups.get(key))
72
+ let message = summary
73
+ let stack = ''
74
+ for (const { name, message: detail, stack: originalStack, count } of errors) {
75
+ message += `\n\n${name}: ${detail} (${count} suite${count === 1 ? '' : 's'})`
76
+ const prefix = errors.length === 1 ? '' : `Affected suites: ${count}\n`
77
+ stack += (stack ? '\n\n' : '') + prefix + (originalStack || `${name}: ${detail}`)
78
+ if (message.length > MAX_LENGTH || stack.length > MAX_LENGTH) return omitDetails(error)
79
+ }
80
+ error.name = errors.length === 1 ? errors[0].name : 'Error'
81
+ error.message = message
82
+ error.stack = stack
83
+ return error
84
+ }
85
+
86
+ /**
87
+ * Returns undefined for oversized input before scanning or copying it.
88
+ * @param {unknown} value
89
+ */
90
+ function readField (value) {
91
+ if (typeof value !== 'string') return ''
92
+ if (value.length > MAX_LENGTH) return
93
+ return stripVTControlCharacters(value)
94
+ }
95
+
96
+ /** @param {Error} error */
97
+ function omitDetails (error) {
98
+ error.message += '\n\nError details exceed the size limit. See test and suite events for full details.'
99
+ return error
100
+ }
101
+
102
+ module.exports = { getSessionError }
@@ -58,6 +58,7 @@ const {
58
58
  addCoverageBackfillUntestedFiles,
59
59
  getCoverageBackfillFiles,
60
60
  } = require('./jest/coverage-backfill')
61
+ const { getSessionError } = require('./jest/session-error')
61
62
  const {
62
63
  getChannelPromise,
63
64
  publishWithCompletion,
@@ -2739,13 +2740,6 @@ function applySkippedCoverageToJestCoverageMap (coverageMap, rootDir) {
2739
2740
  )
2740
2741
  }
2741
2742
 
2742
- function getSessionFinishError (results) {
2743
- const numFailedTestSuites = results?.numFailedTestSuites || 0
2744
- const numFailedTests = results?.numFailedTests || 0
2745
-
2746
- return new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
2747
- }
2748
-
2749
2743
  function getTestSessionCoveragePayload (results, fallbackRootDir) {
2750
2744
  const payload = {}
2751
2745
  if (!shouldReportCodeCoverageLinesPct()) return payload
@@ -2847,7 +2841,7 @@ function getTestSessionFinishPayload (status, error, extra = {}) {
2847
2841
  async function finishBailTestSession (results, fallbackRootDir) {
2848
2842
  await waitForTestSessionFinish(getTestSessionFinishPayload(
2849
2843
  'fail',
2850
- getSessionFinishError(results),
2844
+ getSessionError(results),
2851
2845
  getTestSessionCoveragePayload(results, fallbackRootDir)
2852
2846
  ))
2853
2847
  }
@@ -3170,7 +3164,6 @@ function getCliWrapper (isNewJestVersion) {
3170
3164
 
3171
3165
  const {
3172
3166
  results: {
3173
- numFailedTestSuites,
3174
3167
  numFailedTests,
3175
3168
  numPassedTests,
3176
3169
  numRuntimeErrorTestSuites = 0,
@@ -3351,7 +3344,7 @@ function getCliWrapper (isNewJestVersion) {
3351
3344
  status = testSessionEmptyReason ? 'skip' : 'pass'
3352
3345
  } else {
3353
3346
  status = 'fail'
3354
- error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
3347
+ error = getSessionError(result.results)
3355
3348
  }
3356
3349
 
3357
3350
  await waitForTestSessionFinish(getTestSessionFinishPayload(status, error, {
@@ -75,6 +75,7 @@ const artifactsRecorderScreenshotPathCh =
75
75
  const snapshotRecorderScreenshotPathCh = tracingChannel('orchestrion:playwright:SnapshotRecorder_createAttachmentPath')
76
76
  const saveAutomaticVideoCh = tracingChannel('orchestrion:playwright:saveAutomaticVideo')
77
77
  const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
78
+ const fullProjectInternalCh = tracingChannel('orchestrion:playwright:FullProjectInternal')
78
79
 
79
80
  const testToCtx = new WeakMap()
80
81
  const originalTestIdsByRetry = new WeakMap()
@@ -91,6 +92,8 @@ const isFailureScreenshotUploadEnabled =
91
92
  getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
92
93
  const isFailureVideoUploadEnabled =
93
94
  getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED') === true
95
+ const shouldEnableFailureScreenshots = getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED', true) === true
96
+ const shouldEnableFailureVideos = getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED', true) === true
94
97
 
95
98
  let applyRepeatEachIndex = null
96
99
  let reporterError
@@ -1057,6 +1060,8 @@ function testEndHandler ({
1057
1060
  if (isFlakyTestRetriesEnabled && !testProperties.attemptToFix && !test._ddIsEfdRetry &&
1058
1061
  !(test._ddIsNew || test._ddIsModified) &&
1059
1062
  atrRetryCount != null && atrRetryCount > 0 &&
1063
+ // Serial suites can add skipped results before a test's first execution.
1064
+ results.some((result, index) => index < results.length - 1 && result.status !== 'skipped') &&
1060
1065
  !willRetry && testResultStatus !== expectedStatus &&
1061
1066
  testStatuses.every(status => status === 'fail')) {
1062
1067
  test._ddHasFailedAllRetries = true
@@ -1882,6 +1887,27 @@ dispatcherRunCh.subscribe({
1882
1887
  },
1883
1888
  })
1884
1889
 
1890
+ fullProjectInternalCh.subscribe({
1891
+ end ({ self, error }) {
1892
+ if (error || !libraryConfigurationCh.hasSubscribers) return
1893
+
1894
+ // Workers reload the config, so apply capture settings as each project is resolved in either process.
1895
+ const { project } = self
1896
+ if (shouldEnableFailureScreenshots && !isFailureScreenshotCaptureEnabled([project])) {
1897
+ const screenshot = project.use.screenshot
1898
+ project.use.screenshot = typeof screenshot === 'object' && screenshot !== null
1899
+ ? { ...screenshot, mode: 'only-on-failure' }
1900
+ : 'only-on-failure'
1901
+ }
1902
+ if (shouldEnableFailureVideos && !isFailureVideoCaptureEnabled([project])) {
1903
+ const video = project.use.video
1904
+ project.use.video = typeof video === 'object' && video !== null
1905
+ ? { ...video, mode: 'retain-on-failure' }
1906
+ : 'retain-on-failure'
1907
+ }
1908
+ },
1909
+ })
1910
+
1885
1911
  dispatcherCreateWorkerCh.subscribe({
1886
1912
  end (ctx) {
1887
1913
  onDispatcherCreateWorker(ctx.self, ctx.result)
@@ -1981,6 +1981,9 @@ function getWrappedOn (on) {
1981
1981
  if (event !== 'message') {
1982
1982
  return on.apply(this, arguments)
1983
1983
  }
1984
+ // Reply through Vitest's pool worker, which owns its version-specific serialization.
1985
+ // The listener's `this` is the underlying ChildProcess or Worker instead.
1986
+ const worker = this
1984
1987
  // `arguments[1]` is the callback function, which
1985
1988
  // we modify to intercept our messages to not interfere
1986
1989
  // with vitest's own messages
@@ -1988,7 +1991,7 @@ function getWrappedOn (on) {
1988
1991
  if (message.type !== 'Buffer' && Array.isArray(message)) {
1989
1992
  const [interprocessCode, data] = message
1990
1993
  if (
1991
- handleEfdAdmissionMessage(this, interprocessCode, data) ||
1994
+ handleEfdAdmissionMessage(worker, interprocessCode, data) ||
1992
1995
  handleWorkerReport(interprocessCode, data)
1993
1996
  ) {
1994
1997
  // If we execute the callback vitest crashes, as the message is not supported
@@ -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
- const lastExecutionStatus = task.result.state
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' : 'pass',
1142
+ finalStatus: isSkippedByTestManagement ? 'skip' : state,
1112
1143
  earlyFlakeAbortReason: efdSlowAbortedTasks.has(task) ? 'slow' : undefined,
1113
1144
  promises,
1114
1145
  ...testCtx.currentStore,