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.
Files changed (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -12,13 +12,16 @@ const { getTestManagementTests: getTestManagementTestsRequest } =
12
12
  require('../test-management/get-test-management-tests')
13
13
  const { writeSettingsToCache } = require('../test-optimization-cache')
14
14
  const { CACHE_MISS, TestOptimizationHttpCache } = require('../test-optimization-http-cache')
15
+ const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../telemetry')
15
16
  const { uploadCoverageReport: uploadCoverageReportRequest } = require('../requests/upload-coverage-report')
16
17
  const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot')
17
18
  const { parsers } = require('../../config/parsers')
18
19
  const log = require('../../log')
20
+ const spanFormat = require('../../span_format')
19
21
  const { getSegment } = require('../../util')
20
22
  const BufferingExporter = require('../../exporters/common/buffering-exporter')
21
23
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
24
+ const { TEST_STATUS } = require('../../plugins/util/test')
22
25
  const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
23
26
 
24
27
  const hostname = getHostname()
@@ -57,6 +60,17 @@ function getIsTestSessionTrace (trace) {
57
60
  const GIT_UPLOAD_TIMEOUT = 60_000 // 60 seconds
58
61
  const CAN_USE_CI_VIS_PROTOCOL_TIMEOUT = GIT_UPLOAD_TIMEOUT
59
62
  const MAX_COVERAGE_REPORT_FLAGS = 32
63
+ const FINAL_FLUSH_TIMEOUT = 10_000
64
+ const FINAL_FLUSH_FALLBACK_DELAY = 100
65
+
66
+ /**
67
+ * @returns {Error}
68
+ */
69
+ function createFinalFlushTimeoutError () {
70
+ const error = new Error('Timed out waiting for Test Optimization to flush')
71
+ error.code = 'ERR_DD_TEST_OPTIMIZATION_FLUSH_TIMEOUT'
72
+ return error
73
+ }
60
74
 
61
75
  function appendLogTag (tags, key, value) {
62
76
  if (value !== undefined) {
@@ -87,6 +101,12 @@ function getLogTags (logMessage, { env, version }, gitRepositoryUrl, gitCommitSh
87
101
  }
88
102
 
89
103
  class CiVisibilityExporter extends BufferingExporter {
104
+ #finalFlush
105
+ #deferredTestSessionTraces = []
106
+ #pendingScreenshotUploads = new Set()
107
+ #screenshotFlushWaiters = new Set()
108
+ #deferredTestSuiteSpans = new Map()
109
+
90
110
  constructor (config, options = {}) {
91
111
  super(config)
92
112
  this._timer = undefined
@@ -137,18 +157,7 @@ class CiVisibilityExporter extends BufferingExporter {
137
157
  }
138
158
  })
139
159
 
140
- const flush = () => {
141
- if (this._writer) {
142
- this._writer.flush()
143
- }
144
- if (this._coverageWriter) {
145
- this._coverageWriter.flush()
146
- }
147
- if (this._logsWriter) {
148
- this._logsWriter.flush()
149
- }
150
- }
151
- globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(flush.bind(this))
160
+ globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(() => this.flush(() => {}))
152
161
  }
153
162
 
154
163
  shouldRequestSkippableSuites () {
@@ -399,18 +408,189 @@ class CiVisibilityExporter extends BufferingExporter {
399
408
  }
400
409
 
401
410
  export (trace) {
411
+ this.#resetFinalFlush()
412
+
413
+ if (this.#deferredTestSuiteSpans.size === 0) {
414
+ this.#exportTrace(trace)
415
+ return
416
+ }
417
+
418
+ let hasDeferredTestSuiteSpan = false
419
+ let immediateTrace
420
+ for (let index = 0; index < trace.length; index++) {
421
+ const formattedSpan = trace[index]
422
+ const spanId = formattedSpan.span_id?.toString()
423
+ const deferredTestSuiteSpan = this.#deferredTestSuiteSpans.get(spanId)
424
+ if (deferredTestSuiteSpan) {
425
+ if (!hasDeferredTestSuiteSpan && index > 0) immediateTrace = trace.slice(0, index)
426
+ hasDeferredTestSuiteSpan = true
427
+ if (deferredTestSuiteSpan.emitted || (this._isInitialized && !this.canReportSessionTraces())) {
428
+ this.#deferredTestSuiteSpans.delete(spanId)
429
+ } else {
430
+ deferredTestSuiteSpan.formattedSpan = formattedSpan
431
+ }
432
+ } else if (hasDeferredTestSuiteSpan) {
433
+ immediateTrace ??= []
434
+ immediateTrace.push(formattedSpan)
435
+ }
436
+ }
437
+
438
+ if (!hasDeferredTestSuiteSpan) this.#exportTrace(trace)
439
+ else if (immediateTrace) this.#exportTrace(immediateTrace)
440
+ }
441
+
442
+ /**
443
+ * Exports spans that are not retained for late updates to session, module, or suite events.
444
+ *
445
+ * @param {Array<object>} trace
446
+ * @returns {void}
447
+ */
448
+ #exportTrace (trace) {
402
449
  // Until it's initialized, we just store the traces as is
403
450
  if (!this._isInitialized) {
404
451
  this._traceBuffer.push(trace)
405
452
  return
406
453
  }
407
- if (!this.canReportSessionTraces() && getIsTestSessionTrace(trace)) {
454
+ const isTestSessionTrace = getIsTestSessionTrace(trace)
455
+ if (!this.canReportSessionTraces() && isTestSessionTrace) {
408
456
  return
409
457
  }
410
- this._export(trace)
458
+ if (this._export(trace, undefined, undefined, isTestSessionTrace) === false && isTestSessionTrace) {
459
+ this.#deferredTestSessionTraces.push(trace)
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Retains a completed suite until finalization so a later framework error can still update it.
465
+ *
466
+ * @param {import('../../opentracing/span')} testSuiteSpan
467
+ * @returns {void}
468
+ */
469
+ deferTestSuiteSpan (testSuiteSpan) {
470
+ this.#resetFinalFlush()
471
+ this.#deferredTestSuiteSpans.set(testSuiteSpan.context()._spanId.toString(), {
472
+ span: testSuiteSpan,
473
+ formattedSpan: undefined,
474
+ emitted: false,
475
+ })
476
+ }
477
+
478
+ /**
479
+ * Retains formatted test suite events received from test framework workers until finalization.
480
+ *
481
+ * @param {Array<object>} trace
482
+ * @returns {void}
483
+ */
484
+ exportTraceWithDeferredTestSuite (trace) {
485
+ for (const formattedSpan of trace) {
486
+ if (formattedSpan.type !== 'test_suite_end') continue
487
+
488
+ this.#deferredTestSuiteSpans.set(formattedSpan.span_id.toString(), {
489
+ span: undefined,
490
+ formattedSpan,
491
+ emitted: false,
492
+ })
493
+ }
494
+ this.export(trace)
495
+ }
496
+
497
+ /**
498
+ * Applies a late framework error to every completed suite retained by the current finalization boundary.
499
+ *
500
+ * @param {Error} error
501
+ * @returns {void}
502
+ */
503
+ setDeferredTestSuiteError (error) {
504
+ for (const { span, formattedSpan, emitted } of this.#deferredTestSuiteSpans.values()) {
505
+ if (emitted) continue
506
+ if (span) {
507
+ span.setTag(TEST_STATUS, 'fail')
508
+ span.setTag('error', error)
509
+ } else if (formattedSpan) {
510
+ formattedSpan.meta[TEST_STATUS] = 'fail'
511
+ spanFormat.addError(formattedSpan, error)
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Appends a completed suite while preserving the ordinary writer call shape outside finalization.
518
+ *
519
+ * @param {object} formattedSpan
520
+ * @param {{ deadline?: number }} [options]
521
+ * @returns {boolean|undefined}
522
+ */
523
+ #appendDeferredTestSuiteSpan (formattedSpan, options) {
524
+ const appended = options === undefined
525
+ ? this._writer.append([formattedSpan])
526
+ : this._writer.append([formattedSpan], options)
527
+
528
+ if (appended !== false && this._config.isCiVisibility) {
529
+ incrementCountMetric(TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION)
530
+ }
531
+ return appended
532
+ }
533
+
534
+ /**
535
+ * Appends completed suites before their module and session parents are exported.
536
+ *
537
+ * @param {{ deadline?: number }} [options]
538
+ * @returns {void}
539
+ */
540
+ exportDeferredTestSuiteSpans (options) {
541
+ if (!this._writer || !this.canReportSessionTraces()) return
542
+
543
+ for (const [spanId, deferredTestSuiteSpan] of this.#deferredTestSuiteSpans) {
544
+ const { span, formattedSpan, emitted } = deferredTestSuiteSpan
545
+ if (emitted) continue
546
+ const updatedSpan = span && spanFormat(span)
547
+ if (span && formattedSpan) {
548
+ formattedSpan.error = updatedSpan.error
549
+ Object.assign(formattedSpan.meta, updatedSpan.meta)
550
+ Object.assign(formattedSpan.metrics, updatedSpan.metrics)
551
+ if (this.#appendDeferredTestSuiteSpan(formattedSpan, options) === false) continue
552
+ this.#deferredTestSuiteSpans.delete(spanId)
553
+ } else if (formattedSpan) {
554
+ if (this.#appendDeferredTestSuiteSpan(formattedSpan, options) === false) continue
555
+ this.#deferredTestSuiteSpans.delete(spanId)
556
+ } else {
557
+ if (this.#appendDeferredTestSuiteSpan(updatedSpan, options) === false) continue
558
+ deferredTestSuiteSpan.emitted = true
559
+ }
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Discards completed suites that the selected transport cannot deliver.
565
+ *
566
+ * @returns {void}
567
+ */
568
+ resetDeferredTestSuiteSpans () {
569
+ this.#deferredTestSuiteSpans.clear()
570
+ }
571
+
572
+ /**
573
+ * Retries session, module, and suite traces rejected by writer backpressure within the final deadline.
574
+ *
575
+ * @param {{ deadline?: number }} options final-flush options
576
+ * @returns {void}
577
+ */
578
+ #exportDeferredTestSessionTraces (options) {
579
+ if (!this._writer || !this.canReportSessionTraces()) return
580
+
581
+ let retainedCount = 0
582
+
583
+ for (const trace of this.#deferredTestSessionTraces) {
584
+ if (this._writer.append(trace, options) === false) {
585
+ this.#deferredTestSessionTraces[retainedCount++] = trace
586
+ }
587
+ }
588
+ this.#deferredTestSessionTraces.length = retainedCount
411
589
  }
412
590
 
413
591
  exportCoverage (formattedCoverage) {
592
+ this.#resetFinalFlush()
593
+
414
594
  // Until it's initialized, we just store the coverages as is
415
595
  if (!this._isInitialized) {
416
596
  this._coverageBuffer.push(formattedCoverage)
@@ -455,6 +635,7 @@ class CiVisibilityExporter extends BufferingExporter {
455
635
  return
456
636
  }
457
637
 
638
+ this.#resetFinalFlush()
458
639
  this._export(
459
640
  this.formatLogMessage(testEnvironmentMetadata, logMessage),
460
641
  this._logsWriter,
@@ -462,32 +643,140 @@ class CiVisibilityExporter extends BufferingExporter {
462
643
  )
463
644
  }
464
645
 
465
- flush (done = () => {}) {
466
- if (!this._isInitialized) {
467
- return done()
646
+ flush (done) {
647
+ const isFinalFlush = typeof done === 'function'
648
+ const onDone = done || (() => {})
649
+ let finalFlush
650
+
651
+ if (isFinalFlush && this.#finalFlush) {
652
+ if (this.#finalFlush.completed) onDone(this.#finalFlush.error)
653
+ else this.#finalFlush.callbacks.push(onDone)
654
+ return
468
655
  }
469
656
 
470
- // TODO: safe to do them at once? Or do we want to do them one by one?
471
- const writers = [
472
- this._writer,
473
- this._coverageWriter,
474
- this._logsWriter,
475
- ].filter(Boolean)
657
+ if (isFinalFlush && !this._isInitialized &&
658
+ this._traceBuffer.length === 0 && this._coverageBuffer.length === 0 &&
659
+ this.#deferredTestSuiteSpans.size === 0 && this.#deferredTestSessionTraces.length === 0 &&
660
+ this.#pendingScreenshotUploads.size === 0) {
661
+ onDone()
662
+ return
663
+ }
664
+
665
+ if (isFinalFlush) {
666
+ finalFlush = {
667
+ callbacks: [onDone],
668
+ completed: false,
669
+ error: undefined,
670
+ }
671
+ this.#finalFlush = finalFlush
672
+ }
476
673
 
477
- let remaining = writers.length
674
+ const deadline = isFinalFlush ? Date.now() + FINAL_FLUSH_TIMEOUT : undefined
675
+ let hasCompleted = false
676
+ let initializationTimeoutId
677
+
678
+ const fallbackTimeoutId = isFinalFlush
679
+ ? setTimeout(() => {
680
+ complete(createFinalFlushTimeoutError())
681
+ }, FINAL_FLUSH_TIMEOUT + FINAL_FLUSH_FALLBACK_DELAY)
682
+ : undefined
683
+
684
+ const complete = (error) => {
685
+ if (hasCompleted) return
686
+ hasCompleted = true
687
+ clearTimeout(fallbackTimeoutId)
688
+ clearTimeout(initializationTimeoutId)
689
+ this.#screenshotFlushWaiters.delete(flushWriters)
690
+ if (error) log.error('Error flushing Test Optimization data', error)
691
+ if (!isFinalFlush) {
692
+ onDone(error)
693
+ return
694
+ }
478
695
 
479
- if (remaining === 0) {
480
- return done()
696
+ finalFlush.completed = true
697
+ finalFlush.error = error
698
+ const callbacks = finalFlush.callbacks
699
+ finalFlush.callbacks = []
700
+ for (const callback of callbacks) {
701
+ try {
702
+ callback(error)
703
+ } catch (callbackError) {
704
+ log.error('Error completing Test Optimization flush callback', callbackError)
705
+ }
706
+ }
481
707
  }
482
708
 
483
- const onFlushComplete = () => {
484
- remaining -= 1
709
+ const flushWriters = () => {
710
+ if (isFinalFlush && this.#pendingScreenshotUploads.size !== 0) {
711
+ this.#screenshotFlushWaiters.add(flushWriters)
712
+ return
713
+ }
714
+
715
+ const options = deadline === undefined ? undefined : { deadline }
716
+ if (isFinalFlush) {
717
+ this.exportDeferredTestSuiteSpans(options)
718
+ this.#exportDeferredTestSessionTraces(options)
719
+ }
720
+
721
+ const writers = [
722
+ this._writer,
723
+ this._coverageWriter,
724
+ this._logsWriter,
725
+ ].filter(Boolean)
726
+
727
+ let remaining = writers.length
728
+ let flushError
729
+
485
730
  if (remaining === 0) {
486
- done()
731
+ complete()
732
+ return
733
+ }
734
+
735
+ const onFlushComplete = (error) => {
736
+ flushError ||= error
737
+ remaining -= 1
738
+ if (remaining === 0) complete(flushError)
487
739
  }
740
+
741
+ for (const writer of writers) writer.flush(onFlushComplete, options)
742
+ }
743
+
744
+ if (isFinalFlush && this._initializationRequest) {
745
+ const initializationRequest = this._initializationRequest
746
+ const { controller, options } = initializationRequest
747
+ initializationRequest.finalFlush = finalFlush
748
+ options.deadline = deadline
749
+ initializationTimeoutId = setTimeout(() => {
750
+ const error = createFinalFlushTimeoutError()
751
+ if (initializationRequest.finalFlush === finalFlush) controller.abort(error)
752
+ complete(error)
753
+ }, Math.max(0, deadline - Date.now()))
754
+ }
755
+
756
+ if (!isFinalFlush) {
757
+ if (this._isInitialized) flushWriters()
758
+ else complete()
759
+ return
760
+ }
761
+
762
+ if (this._isInitialized) {
763
+ flushWriters()
764
+ return
488
765
  }
489
766
 
490
- for (const writer of writers) writer.flush(onFlushComplete)
767
+ this._canUseCiVisProtocolPromise.then(() => {
768
+ clearTimeout(initializationTimeoutId)
769
+ if (!hasCompleted) flushWriters()
770
+ })
771
+ }
772
+
773
+ /**
774
+ * Allows later test activity to establish a new finalization boundary.
775
+ *
776
+ * @returns {void}
777
+ */
778
+ #resetFinalFlush () {
779
+ this.#finalFlush = undefined
491
780
  }
492
781
 
493
782
  exportUncodedCoverages () {
@@ -572,13 +861,55 @@ class CiVisibilityExporter extends BufferingExporter {
572
861
  * @param {string} options.traceId - Test trace id used as the screenshot key
573
862
  * @param {string} options.idempotencyKey - Stable per-artifact key, reused on retry
574
863
  * @param {number} options.capturedAtMs - Capture time in epoch milliseconds
864
+ * @param {AbortSignal} [options.signal] - Additional signal used to cancel the upload
575
865
  * @param {Function} callback - Callback function (err)
576
866
  */
577
- uploadTestScreenshot ({ filePath, traceId, idempotencyKey, capturedAtMs }, callback) {
867
+ uploadTestScreenshot ({ filePath, traceId, idempotencyKey, capturedAtMs, signal }, callback) {
578
868
  if (!this._testScreenshotUploadUrl) {
579
869
  return callback(new Error('Test screenshot upload URL not configured'))
580
870
  }
581
871
 
872
+ this.#resetFinalFlush()
873
+ const controller = new AbortController()
874
+ const deadline = Date.now() + FINAL_FLUSH_TIMEOUT
875
+ let settled = false
876
+ const complete = (error) => {
877
+ if (settled) return
878
+ settled = true
879
+ clearTimeout(timeoutId)
880
+ signal?.removeEventListener('abort', onAbort)
881
+
882
+ try {
883
+ callback(error)
884
+ } finally {
885
+ this.#pendingScreenshotUploads.delete(controller)
886
+ if (this.#pendingScreenshotUploads.size === 0) {
887
+ const waiters = [...this.#screenshotFlushWaiters]
888
+ this.#screenshotFlushWaiters.clear()
889
+ for (const waiter of waiters) queueMicrotask(waiter)
890
+ }
891
+ }
892
+ }
893
+ const onAbort = () => {
894
+ const error = signal.reason || Object.assign(new Error('Test screenshot upload aborted'), { code: 'ABORT_ERR' })
895
+ controller.abort(error)
896
+ complete(error)
897
+ }
898
+
899
+ this.#pendingScreenshotUploads.add(controller)
900
+ signal?.addEventListener('abort', onAbort, { once: true })
901
+ const timeoutId = setTimeout(() => {
902
+ const error = createFinalFlushTimeoutError()
903
+ controller.abort(error)
904
+ complete(error)
905
+ }, FINAL_FLUSH_TIMEOUT)
906
+ timeoutId.unref?.()
907
+
908
+ if (signal?.aborted) {
909
+ onAbort()
910
+ return
911
+ }
912
+
582
913
  uploadTestScreenshotRequest({
583
914
  filePath,
584
915
  traceId,
@@ -587,7 +918,9 @@ class CiVisibilityExporter extends BufferingExporter {
587
918
  url: this._testScreenshotUploadUrl,
588
919
  isEvpProxy: !!this._isUsingEvpProxy,
589
920
  evpProxyPrefix: this.evpProxyPrefix,
590
- }, callback)
921
+ deadline,
922
+ signal: controller.signal,
923
+ }, complete)
591
924
  }
592
925
  }
593
926
 
@@ -0,0 +1,201 @@
1
+ 'use strict'
2
+
3
+ const { Readable } = require('node:stream')
4
+
5
+ const commonRequest = require('../../exporters/common/request')
6
+ const {
7
+ RATE_LIMIT_MAX_WAIT_MS,
8
+ getMaxAttempts,
9
+ getRetryDelay,
10
+ isRetriableNetworkError,
11
+ } = require('../../exporters/common/retry')
12
+ const { getRateLimitResetDelay } = require('../requests/rate-limit')
13
+
14
+ /**
15
+ * @param {AbortSignal} signal
16
+ * @returns {Error}
17
+ */
18
+ function getAbortError (signal) {
19
+ return signal.reason || Object.assign(new Error('Request aborted'), { code: 'ABORT_ERR' })
20
+ }
21
+
22
+ /**
23
+ * Buffers a stream once so a Test Optimization exporter request can safely retry it.
24
+ *
25
+ * @param {Readable} data
26
+ * @param {AbortSignal} [signal]
27
+ * @param {(error: Error|null, data?: Buffer) => void} callback
28
+ * @returns {void}
29
+ */
30
+ function bufferReadable (data, signal, callback) {
31
+ const chunks = []
32
+ let settled = false
33
+
34
+ const cleanup = (keepErrorListener = false) => {
35
+ data.removeListener('data', onData)
36
+ data.removeListener('end', onEnd)
37
+ if (!keepErrorListener) data.removeListener('error', onError)
38
+ signal?.removeEventListener('abort', onAbort)
39
+ }
40
+ const onData = chunk => chunks.push(chunk)
41
+ const onEnd = () => {
42
+ if (settled) return
43
+ settled = true
44
+ cleanup()
45
+ callback(null, Buffer.concat(chunks))
46
+ }
47
+ const onError = error => {
48
+ if (settled) return
49
+ settled = true
50
+ cleanup()
51
+ callback(error)
52
+ }
53
+ const onAbort = () => {
54
+ if (settled) return
55
+ settled = true
56
+ cleanup(true)
57
+ data.once('close', () => data.removeListener('error', onError))
58
+ data.destroy()
59
+ callback(getAbortError(signal))
60
+ }
61
+
62
+ data.on('data', onData)
63
+ data.once('end', onEnd)
64
+ data.once('error', onError)
65
+ signal?.addEventListener('abort', onAbort, { once: true })
66
+ if (signal?.aborted) onAbort()
67
+ }
68
+
69
+ /**
70
+ * Sends Test Optimization exporter data through the common single-attempt transport while
71
+ * keeping retry and finalization policy scoped to Test Optimization.
72
+ *
73
+ * @param {Buffer|string|Readable|Array<Buffer|string>} data
74
+ * @param {object} options
75
+ * @param {(error: Error|null, result?: string|null, statusCode?: number,
76
+ * headers?: import('node:http').IncomingHttpHeaders) => void} callback
77
+ * @returns {void}
78
+ */
79
+ function request (data, options, callback) {
80
+ const { signal } = options
81
+ if (signal?.aborted) return callback(getAbortError(signal))
82
+
83
+ if (data instanceof Readable) {
84
+ bufferReadable(data, signal, (error, bufferedData) => {
85
+ if (error) return callback(error)
86
+ requestBuffered(bufferedData, options, callback)
87
+ })
88
+ return
89
+ }
90
+
91
+ requestBuffered(data, options, callback)
92
+ }
93
+
94
+ /**
95
+ * Applies the Test Optimization retry policy to replayable request data.
96
+ *
97
+ * @param {Buffer|string|Array<Buffer|string>} data
98
+ * @param {object} options
99
+ * @param {(error: Error|null, result?: string|null, statusCode?: number,
100
+ * headers?: import('node:http').IncomingHttpHeaders) => void} callback
101
+ * @returns {void}
102
+ */
103
+ function requestBuffered (data, options, callback) {
104
+ const { signal } = options
105
+ const timeout = options.timeout || 2000
106
+ let retryTimer
107
+ let settled = false
108
+
109
+ const complete = (error, result, statusCode, headers) => {
110
+ if (settled) return
111
+ settled = true
112
+ clearTimeout(retryTimer)
113
+ signal?.removeEventListener('abort', onAbort)
114
+ callback(error, result, statusCode, headers)
115
+ }
116
+ const onAbort = () => complete(getAbortError(signal))
117
+
118
+ signal?.addEventListener('abort', onAbort, { once: true })
119
+ if (signal?.aborted) return onAbort()
120
+
121
+ /** @param {number} attemptIndex */
122
+ const attempt = attemptIndex => {
123
+ if (settled) return
124
+ if (signal?.aborted) return onAbort()
125
+
126
+ const deadline = options.deadline
127
+ const remaining = deadline === undefined ? Infinity : deadline - Date.now()
128
+ if (remaining <= 0) {
129
+ const error = new Error('Test Optimization request reached its finalization deadline')
130
+ error.code = 'ERR_DD_TEST_OPTIMIZATION_FLUSH_TIMEOUT'
131
+ complete(error)
132
+ return
133
+ }
134
+
135
+ if (deadline !== undefined && !commonRequest.writable) {
136
+ retryTimer = setTimeout(attempt, Math.min(50, remaining), attemptIndex)
137
+ retryTimer.unref?.()
138
+ return
139
+ }
140
+
141
+ const attemptOptions = {
142
+ ...options,
143
+ headers: options.headers ? { ...options.headers } : undefined,
144
+ retry: false,
145
+ }
146
+ if (deadline !== undefined) attemptOptions.timeout = Math.max(1, Math.min(timeout, remaining))
147
+
148
+ commonRequest(data, attemptOptions, (error, result, statusCode, headers) => {
149
+ if (settled) return
150
+ if (!error) {
151
+ complete(null, result, statusCode, headers)
152
+ return
153
+ }
154
+
155
+ const responseStatus = statusCode ?? error.status
156
+ const isRetriableHttpError = options.deadline !== undefined &&
157
+ (responseStatus === 429 || responseStatus >= 500)
158
+ if (options.retry === false || (attemptIndex >= getMaxAttempts(attemptOptions) &&
159
+ (isRetriableNetworkError(error) || isRetriableHttpError))) {
160
+ complete(error, result, statusCode, headers)
161
+ return
162
+ }
163
+
164
+ if (!isRetriableNetworkError(error) && !isRetriableHttpError) {
165
+ complete(error, result, statusCode, headers)
166
+ return
167
+ }
168
+
169
+ let retryDelay
170
+ if (responseStatus === 429) {
171
+ const resetDelay = getRateLimitResetDelay(headers)
172
+ if (Number.isFinite(resetDelay)) {
173
+ const retryRemaining = options.deadline - Date.now()
174
+ if (resetDelay > RATE_LIMIT_MAX_WAIT_MS || resetDelay >= retryRemaining) {
175
+ complete(error, result, statusCode, headers)
176
+ return
177
+ }
178
+ retryDelay = resetDelay
179
+ }
180
+ }
181
+
182
+ let delay = retryDelay ?? getRetryDelay(attemptOptions, attemptIndex)
183
+ if (options.deadline !== undefined && retryDelay === undefined) {
184
+ const retryRemaining = options.deadline - Date.now()
185
+ if (retryRemaining <= 0) {
186
+ complete(error, result, statusCode, headers)
187
+ return
188
+ }
189
+ const retryAttemptTimeout = timeout < retryRemaining ? timeout : Math.ceil(retryRemaining / 2)
190
+ delay = Math.min(delay, Math.max(0, retryRemaining - retryAttemptTimeout))
191
+ }
192
+
193
+ retryTimer = setTimeout(attempt, delay, attemptIndex + 1)
194
+ retryTimer.unref?.()
195
+ })
196
+ }
197
+
198
+ attempt(1)
199
+ }
200
+
201
+ module.exports = request