dd-trace 6.21.0 → 6.22.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 (204) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  16. package/index.d.ts +25 -14
  17. package/initialize.mjs +1 -3
  18. package/openfeature/rules-based.d.ts +11 -0
  19. package/openfeature/rules-based.js +7 -0
  20. package/package.json +12 -10
  21. package/packages/datadog-core/src/storage.js +16 -0
  22. package/packages/datadog-esbuild/index.js +18 -12
  23. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  24. package/packages/datadog-instrumentations/src/ai.js +3 -3
  25. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  26. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  27. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  28. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  29. package/packages/datadog-instrumentations/src/cucumber.js +48 -33
  30. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  31. package/packages/datadog-instrumentations/src/electron.js +6 -6
  32. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  33. package/packages/datadog-instrumentations/src/fs.js +2 -2
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  35. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  36. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
  37. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  38. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  39. package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
  40. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  41. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
  42. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  47. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  48. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  49. package/packages/datadog-instrumentations/src/jest.js +3 -10
  50. package/packages/datadog-instrumentations/src/knex.js +9 -5
  51. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  52. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  53. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  54. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -2
  55. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
  56. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  57. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  58. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  59. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  60. package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
  61. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  62. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  63. package/packages/datadog-instrumentations/src/openai.js +7 -0
  64. package/packages/datadog-instrumentations/src/pino.js +5 -6
  65. package/packages/datadog-instrumentations/src/redis.js +27 -13
  66. package/packages/datadog-instrumentations/src/router.js +30 -0
  67. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  68. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
  69. package/packages/datadog-instrumentations/src/vitest-main.js +9 -15
  70. package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
  71. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  72. package/packages/datadog-instrumentations/src/vitest.js +2 -2
  73. package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
  74. package/packages/datadog-instrumentations/src/ws.js +1 -4
  75. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  76. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  77. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  78. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  79. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  80. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  82. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  83. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  84. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  85. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  86. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -5
  87. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  88. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  89. package/packages/datadog-plugin-cypress/src/support.js +1 -4
  90. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  91. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  92. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  93. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  94. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  95. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  96. package/packages/datadog-plugin-mocha/src/index.js +7 -10
  97. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
  98. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  99. package/packages/datadog-plugin-next/src/index.js +1 -2
  100. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  101. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  102. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  103. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  104. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  105. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  106. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  107. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  108. package/packages/datadog-webpack/index.js +55 -43
  109. package/packages/datadog-webpack/src/loader.js +56 -8
  110. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  111. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  112. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  113. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  114. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  115. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  116. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  117. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  118. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  119. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  120. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  121. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  122. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  123. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  124. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  125. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  126. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  127. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  128. package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
  129. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  132. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  133. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  134. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  135. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  136. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  137. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  138. package/packages/dd-trace/src/config/defaults.js +5 -5
  139. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
  140. package/packages/dd-trace/src/config/helper.js +34 -3
  141. package/packages/dd-trace/src/config/index.js +10 -9
  142. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  143. package/packages/dd-trace/src/config/parsers.js +2 -2
  144. package/packages/dd-trace/src/config/supported-configurations.json +35 -0
  145. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  146. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  147. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
  148. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  149. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  150. package/packages/dd-trace/src/dogstatsd.js +1 -1
  151. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  152. package/packages/dd-trace/src/exporters/common/request.js +1 -5
  153. package/packages/dd-trace/src/id.js +14 -1
  154. package/packages/dd-trace/src/lambda/handler.js +179 -19
  155. package/packages/dd-trace/src/lambda/index.js +15 -1
  156. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  157. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +12 -0
  159. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  160. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  161. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  162. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  163. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  165. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
  166. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  167. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  168. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  169. package/packages/dd-trace/src/llmobs/span_processor.js +1 -2
  170. package/packages/dd-trace/src/llmobs/tagger.js +2 -2
  171. package/packages/dd-trace/src/llmobs/util.js +1 -2
  172. package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
  173. package/packages/dd-trace/src/log/log.js +1 -5
  174. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  175. package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
  176. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  177. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  179. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  180. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
  181. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  182. package/packages/dd-trace/src/otel-sampling.js +4 -5
  183. package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
  184. package/packages/dd-trace/src/plugins/database.js +7 -16
  185. package/packages/dd-trace/src/plugins/util/git.js +9 -25
  186. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  187. package/packages/dd-trace/src/plugins/util/test.js +48 -4
  188. package/packages/dd-trace/src/priority_sampler.js +1 -2
  189. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  190. package/packages/dd-trace/src/proxy.js +1 -2
  191. package/packages/dd-trace/src/ritm.js +4 -2
  192. package/packages/dd-trace/src/serverless.js +5 -0
  193. package/packages/dd-trace/src/span_processor.js +13 -17
  194. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  195. package/packages/dd-trace/src/util.js +29 -0
  196. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  197. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  198. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  199. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  200. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  201. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  202. package/vendor/dist/protobufjs/index.js +1 -1
  203. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  204. package/vendor/dist/shell-quote/index.js +1 -1
@@ -38,7 +38,7 @@ const id = require('../../id')
38
38
  const DatadogSpanContext = require('../span_context')
39
39
  const log = require('../../log')
40
40
  const tags = require('../../../../../ext/tags')
41
- const { getConfiguredEnvName } = require('../../config/helper')
41
+ const { getValidatedConfiguredEnvName } = require('../../config/helper')
42
42
  const { setAllBaggageItems, getAllBaggageItems, removeAllBaggageItems } = require('../../baggage')
43
43
  const { hasTraceSourcePropagationTag } = require('../../standalone/tracesource')
44
44
  const telemetryMetrics = require('../../telemetry/metrics')
@@ -134,8 +134,7 @@ function isOptionalDatadogTraceStateField (key, injection) {
134
134
  * @returns {DatadogSpanContext | undefined}
135
135
  */
136
136
  function extractGenericContext (traceId, spanId, radix) {
137
- if (!traceId || invalidSegment.test(traceId)) return
138
- if (!spanId) return
137
+ if (!traceId || invalidSegment.test(traceId) || !spanId) return
139
138
 
140
139
  return new DatadogSpanContext({
141
140
  traceId: id(traceId, radix),
@@ -305,13 +304,25 @@ class TextMapPropagator {
305
304
  /** @type {string[] | undefined} Source array that `#baggageTagKeysSet` was built from. */
306
305
  #baggageTagKeysSetSource
307
306
 
308
- /** @type {import('../../config')} */
307
+ /** @type {ReturnType<import('../../config')>} */
309
308
  #config
310
309
 
311
310
  /** @type {typeof extractB3SingleContext | typeof extractB3MultiContext} */
312
311
  #extractB3Context
313
312
 
314
- /** @param {import('../../config')} config */
313
+ /** Whether the `b3` injection style resolves to single-header propagation. */
314
+ #b3InjectIsSingleHeader
315
+
316
+ /** Cached origin used to resolve the v5 `b3` injection style. */
317
+ #b3InjectOrigin
318
+
319
+ /** Cached state of the v5 experimental B3 setting. */
320
+ #b3ExperimentalEnabled
321
+
322
+ /** Cached origin of the shared v5 propagation style. */
323
+ #b3SharedOrigin
324
+
325
+ /** @param {ReturnType<import('../../config')>} config */
315
326
  constructor (config) {
316
327
  this.#config = config
317
328
 
@@ -319,16 +330,44 @@ class TextMapPropagator {
319
330
  // expect single, legacy `DD_TRACE_PROPAGATION_STYLE` callers expect multi.
320
331
  /* istanbul ignore else: v5 fallback */
321
332
  if (DD_MAJOR >= 6) {
333
+ this.#b3InjectIsSingleHeader = true
322
334
  this.#extractB3Context = extractB3SingleContext
323
335
  } else {
324
- const envName = getConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE')
336
+ const envName = getValidatedConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE')
325
337
  // eslint-disable-next-line eslint-rules/eslint-env-aliases
326
338
  this.#extractB3Context = envName === 'OTEL_PROPAGATORS'
327
339
  ? extractB3SingleContext
328
340
  : extractB3MultiContext
341
+
342
+ this.#refreshB3InjectionStyle()
329
343
  }
330
344
  }
331
345
 
346
+ #refreshB3InjectionStyle () {
347
+ const origin = this.#config.getOrigin?.('tracePropagationStyle.inject') ?? 'default'
348
+ const experimentalEnabled = this.#config.experimental?.b3 === true
349
+ const sharedOrigin = origin === 'calculated'
350
+ ? this.#config.getOrigin?.('DD_TRACE_PROPAGATION_STYLE') ?? 'default'
351
+ : undefined
352
+ if (origin === this.#b3InjectOrigin &&
353
+ experimentalEnabled === this.#b3ExperimentalEnabled &&
354
+ sharedOrigin === this.#b3SharedOrigin) return
355
+
356
+ let envName
357
+ if (origin === 'calculated' && !experimentalEnabled && sharedOrigin !== 'remote_config') {
358
+ envName = getValidatedConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE')
359
+ } else if (origin === 'env_var' ||
360
+ origin === 'local_stable_config' ||
361
+ origin === 'fleet_stable_config') {
362
+ envName = getValidatedConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE_INJECT')
363
+ }
364
+ // eslint-disable-next-line eslint-rules/eslint-env-aliases
365
+ this.#b3InjectIsSingleHeader = envName === 'OTEL_PROPAGATORS'
366
+ this.#b3InjectOrigin = origin
367
+ this.#b3ExperimentalEnabled = experimentalEnabled
368
+ this.#b3SharedOrigin = sharedOrigin
369
+ }
370
+
332
371
  /**
333
372
  * Returns a `Set` view of `#config.baggageTagKeys` that is rebuilt only
334
373
  * when the source array reference changes. Avoids an `O(n)` `Set` alloc
@@ -574,9 +613,8 @@ class TextMapPropagator {
574
613
  * @returns {Record<string, string> | undefined}
575
614
  */
576
615
  #injectB3MultipleHeaders (spanContext, carrier) {
577
- // v5 also accepts the legacy `'b3'` spelling for multi; v6+ routes `'b3'` to single-header.
578
616
  const hasB3multi = this.#hasPropagationStyle('inject', 'b3multi') ||
579
- (DD_MAJOR < 6 && this.#hasPropagationStyle('inject', 'b3'))
617
+ (this.#hasPropagationStyle('inject', 'b3') && !this.#usesB3SingleHeaderInjection())
580
618
  if (!hasB3multi) return
581
619
 
582
620
  carrier ??= {}
@@ -603,7 +641,7 @@ class TextMapPropagator {
603
641
  #injectB3SingleHeader (spanContext, carrier) {
604
642
  // v6+ keeps `'b3 single header'` as a back-compat alias for callers that bypass parser normalisation.
605
643
  const hasB3SingleHeader = this.#hasPropagationStyle('inject', 'b3 single header') ||
606
- (DD_MAJOR >= 6 && this.#hasPropagationStyle('inject', 'b3'))
644
+ (this.#hasPropagationStyle('inject', 'b3') && this.#usesB3SingleHeaderInjection())
607
645
  if (!hasB3SingleHeader) return
608
646
 
609
647
  carrier ??= {}
@@ -620,6 +658,11 @@ class TextMapPropagator {
620
658
  return carrier
621
659
  }
622
660
 
661
+ #usesB3SingleHeaderInjection () {
662
+ if (DD_MAJOR < 6) this.#refreshB3InjectionStyle()
663
+ return this.#b3InjectIsSingleHeader
664
+ }
665
+
623
666
  /**
624
667
  * @param {DatadogSpanContext} spanContext
625
668
  * @param {Record<string, string> | undefined} carrier
@@ -982,11 +1025,8 @@ class TextMapPropagator {
982
1025
  const traceparent = { version }
983
1026
  // W3C Trace Context §3.3.1.1: multiple tracestate fields MUST be combined per RFC 7230 §3.2.2.
984
1027
  const tracestate = TraceState.fromString(readTracestate(carrier))
985
- if (invalidSegment.test(traceId)) return
986
- if (invalidSegment.test(spanId)) return
987
-
988
1028
  // Version ff is considered invalid
989
- if (version === 'ff') return
1029
+ if (invalidSegment.test(traceId) || invalidSegment.test(spanId) || version === 'ff') return
990
1030
 
991
1031
  // Version 00 should have no tail, but future versions may
992
1032
  if (tail && version === '00') return
@@ -11,11 +11,10 @@ const UINT64_MASK = 2n ** 64n - 1n
11
11
  /**
12
12
  * @param {unknown} value
13
13
  * @param {number} minLength
14
- * @param {number} maxLength
15
14
  * @returns {value is string}
16
15
  */
17
- function isLowerHex (value, minLength, maxLength) {
18
- if (typeof value !== 'string' || value.length < minLength || value.length > maxLength) return false
16
+ function isLowerHex (value, minLength) {
17
+ if (typeof value !== 'string' || value.length < minLength || value.length > 14) return false
19
18
 
20
19
  for (let index = 0; index < value.length; index++) {
21
20
  const code = value.charCodeAt(index)
@@ -112,11 +111,11 @@ function sanitizeFields (state) {
112
111
  let randomValue = state.get('rv')
113
112
  let threshold = state.get('th')
114
113
 
115
- if (!isLowerHex(randomValue, 14, 14)) {
114
+ if (!isLowerHex(randomValue, 14)) {
116
115
  state.delete('rv')
117
116
  randomValue = undefined
118
117
  }
119
- if (!isLowerHex(threshold, 1, 14)) {
118
+ if (!isLowerHex(threshold, 1)) {
120
119
  state.delete('th')
121
120
  threshold = undefined
122
121
  }
@@ -20,8 +20,9 @@
20
20
  // and the OTel ContextManager activates spans via
21
21
  // storage('legacy').run({ span: ddSpan }, ...), so the active span
22
22
  // our writer reads from legacy storage is the same DatadogSpan in
23
- // both cases. The OTel-visible trace/span IDs come from the same
24
- // _ddContext.toTraceId/toSpanId calls we use here.
23
+ // both cases. The record's trace/span IDs are read from the same
24
+ // Identifier bytes (and _dd.p.tid) behind _ddContext.toTraceId/toSpanId,
25
+ // and the spec pins them byte-for-byte to those methods.
25
26
 
26
27
  const { isMainThread, threadId } = require('worker_threads')
27
28
 
@@ -40,6 +41,10 @@ const {
40
41
  } = require('./profiling/webspan-utils')
41
42
  const webTagsCache = require('./web-tags-cache')
42
43
 
44
+ /** @typedef {import('./opentracing/span')} DatadogSpan */
45
+ /** @typedef {import('./opentracing/span_context')} DatadogSpanContext */
46
+ /** @typedef {InstanceType<typeof import('@datadog/pprof').otelThreadCtx.ThreadContext>} ThreadContext */
47
+
43
48
  // OTEP-4947 duplicates are last-wins, so appending a later endpoint value would
44
49
  // correctly overwrite an earlier one — but an out-of-process reader sampling the
45
50
  // record mid-request would still see the interim value (e.g. a bare `GET`) as
@@ -124,6 +129,7 @@ function traceFlagsOf (spanContext) {
124
129
  // waiting on; undefined while the span has no web-server ancestry.
125
130
  // Distinguishes a stale announcement from a live one when a nearer
126
131
  // web-server span takes over.
132
+ // awaitsOwnEndpoint: this is the request span, waiting on its own endpoint.
127
133
  const CachedSym = Symbol('OtelThreadCtx.cached')
128
134
 
129
135
  let started = false
@@ -131,12 +137,59 @@ let ThreadContext
131
137
  let getContext
132
138
  let clearContext
133
139
 
140
+ // Reusable: the ThreadContext constructor copies the ids into its own record.
141
+ const traceIdBytes = Buffer.alloc(16)
142
+ const spanIdBytes = new Uint8Array(8)
143
+
144
+ /**
145
+ * Same bytes as decoding toTraceId(true); unexpected shapes fall back to that.
146
+ *
147
+ * @param {DatadogSpanContext} spanContext
148
+ * @returns {Uint8Array}
149
+ */
150
+ function getTraceIdBytes (spanContext) {
151
+ const low = spanContext._traceId.toBuffer()
152
+ if (low.length === 16) {
153
+ for (let i = 0; i < 16; i++) traceIdBytes[i] = low[i]
154
+ return traceIdBytes
155
+ }
156
+ if (low.length !== 8) return Buffer.from(spanContext.toTraceId(true), 'hex')
157
+ const high = spanContext._trace.tags['_dd.p.tid']
158
+ if (high) {
159
+ // write() stops at the first non-hex character, so only a valid _dd.p.tid fills all 8 bytes.
160
+ if (typeof high !== 'string' || high.length !== 16 || traceIdBytes.write(high, 0, 8, 'hex') !== 8) {
161
+ return Buffer.from(spanContext.toTraceId(true), 'hex')
162
+ }
163
+ } else {
164
+ for (let i = 0; i < 8; i++) traceIdBytes[i] = 0
165
+ }
166
+ for (let i = 0; i < 8; i++) traceIdBytes[i + 8] = low[i]
167
+ return traceIdBytes
168
+ }
169
+
170
+ /**
171
+ * @param {DatadogSpanContext} spanContext
172
+ * @returns {Uint8Array}
173
+ */
174
+ function getSpanIdBytes (spanContext) {
175
+ const bytes = spanContext._spanId.toBuffer()
176
+ if (bytes.length !== 8) return Buffer.from(spanContext.toSpanId(true), 'hex')
177
+ for (let i = 0; i < 8; i++) spanIdBytes[i] = bytes[i]
178
+ return spanIdBytes
179
+ }
180
+
181
+ /**
182
+ * @param {DatadogSpan} span
183
+ * @returns {ThreadContext | undefined} The span's record, or undefined when the span's trace or span id is malformed.
184
+ */
134
185
  function getOrBuildContext (span) {
135
186
  let cached = span[CachedSym]
136
187
  if (cached !== undefined && cached.context !== undefined) return cached.context
137
188
  const spanContext = span.context()
138
- const traceId = Uint8Array.from(Buffer.from(spanContext.toTraceId(true), 'hex'))
139
- const spanId = Uint8Array.from(Buffer.from(spanContext.toSpanId(true), 'hex'))
189
+ const traceId = getTraceIdBytes(spanContext)
190
+ const spanId = getSpanIdBytes(spanContext)
191
+ // The native constructor throws on any other id length, and onEnter runs inside user code.
192
+ if (traceId.length !== 16 || spanId.length !== 8) return
140
193
  // Local root span: the first entry in the trace's started-spans list, or
141
194
  // this span itself when it IS the root. Encoded as 16-char lowercase hex
142
195
  // per the libdatadog convention.
@@ -152,7 +205,7 @@ function getOrBuildContext (span) {
152
205
  attrs[THREAD_NAME_IDX] = THREAD_NAME
153
206
  attrs[THREAD_ID_IDX] = THREAD_ID
154
207
  if (cached === undefined) {
155
- cached = {}
208
+ cached = { context: undefined, webTags: undefined, awaitsOwnEndpoint: false }
156
209
  span[CachedSym] = cached
157
210
  }
158
211
  // The flags are the one part of the record that can still change after
@@ -163,17 +216,22 @@ function getOrBuildContext (span) {
163
216
  // built before the decision keeps reading as not sampled.
164
217
  cached.context = new ThreadContext(traceId, spanId, traceFlagsOf(spanContext), attrs)
165
218
  cached.webTags = webTags
166
- if (endpoint === undefined) awaitEndpoint(webTags, cached)
219
+ if (endpoint === undefined) awaitEndpoint(span, webTags, cached)
167
220
  return cached.context
168
221
  }
169
222
 
170
223
  // Enlist a record built without an endpoint, so the resolution announcement for
171
224
  // its request fills it in.
172
- function awaitEndpoint (webTags, cached) {
225
+ function awaitEndpoint (span, webTags, cached) {
173
226
  // No web-server span in this span's ancestry, so no endpoint is coming. Should
174
227
  // one appear later, webTagsCache announces that separately — see
175
228
  // onWebTagsResolved.
176
229
  if (webTags === undefined) return
230
+ // The request span waits on its own announcement; only descendants need the shared list.
231
+ if (webTags === span.context().getTags()) {
232
+ cached.awaitsOwnEndpoint = true
233
+ return
234
+ }
177
235
  const waiting = pendingEndpoints.get(webTags)
178
236
  if (waiting === undefined) {
179
237
  pendingEndpoints.set(webTags, [cached])
@@ -193,12 +251,13 @@ function appendEndpoint (context, endpoint) {
193
251
 
194
252
  function onEnter () {
195
253
  if (!started) return
196
- const span = getActiveSpan()
197
- if (!span) {
198
- clearContext()
254
+ const span = /** @type {DatadogSpan | null | undefined} */ (getActiveSpan())
255
+ const context = span && getOrBuildContext(span)
256
+ if (!context) {
257
+ // Clearing an empty frame would still make pprof install a new AsyncContextFrame.
258
+ if (getContext() !== undefined) clearContext()
199
259
  return
200
260
  }
201
- const context = getOrBuildContext(span)
202
261
  // Skip if this CPED already holds the same context. Same allocation-churn
203
262
  // fix as the wall profiler in dd-trace-js#8638.
204
263
  if (getContext() === context) return
@@ -246,11 +305,18 @@ function onSpanFinished (span) {
246
305
  function onEndpointResolved (span) {
247
306
  if (!started) return
248
307
  const webTags = span.context().getTags()
308
+ const own = span[CachedSym]
309
+ const ownWaits = own !== undefined && own.awaitsOwnEndpoint
249
310
  const waiting = pendingEndpoints.get(webTags)
250
- if (waiting === undefined) return
311
+ if (!ownWaits && waiting === undefined) return
251
312
  const endpoint = finalEndpoint(webTags)
252
313
  // webTagsCache only announces settled endpoints, so this is belt and braces.
253
314
  if (endpoint === undefined) return
315
+ if (ownWaits) {
316
+ own.awaitsOwnEndpoint = false
317
+ if (own.context !== undefined && own.webTags === webTags) appendEndpoint(own.context, endpoint)
318
+ }
319
+ if (waiting === undefined) return
254
320
  pendingEndpoints.delete(webTags)
255
321
  for (const cached of waiting) {
256
322
  // A record whose span has since been attributed to a nearer web-server span
@@ -281,7 +347,7 @@ function onWebTagsResolved (span) {
281
347
  // ThreadContext would strand every async-context frame holding this one.
282
348
  // The work is still nested in the outer request, which makes that the least
283
349
  // wrong of the values available.
284
- awaitEndpoint(webTags, cached)
350
+ awaitEndpoint(span, webTags, cached)
285
351
  } else {
286
352
  appendEndpoint(cached.context, endpoint)
287
353
  }
@@ -25,7 +25,7 @@ class DatabasePlugin extends StoragePlugin {
25
25
  // trails ddh — so per-query work shrinks to encoding dddb / dddbs / ddh and concatenating.
26
26
  #dbmEnvFragment
27
27
  #dbmEndFragment
28
- // Cache the rendered prefix per `${db.name}\0${out.host}\0${dbmService}`. The triple is
28
+ // Cache the rendered prefix per `${db.name}\0${out.host}\0${serviceName}`. The triple is
29
29
  // connection-stable for a real workload, so the steady state is one `LRUCache.get` plus the
30
30
  // optional per-call `,ddprs='...'` suffix.
31
31
  #dbmPrefixCache = new LRUCache({ max: DBM_PREFIX_CACHE_MAX })
@@ -61,23 +61,15 @@ class DatabasePlugin extends StoragePlugin {
61
61
  this.#dbmPrefixCache.set(cacheKey, prefix)
62
62
  }
63
63
 
64
- if (peerData !== undefined && peerData[PEER_SERVICE_SOURCE_KEY] === PEER_SERVICE_KEY) {
65
- return `${prefix},ddprs='${encode(peerData[PEER_SERVICE_KEY])}'`
64
+ // `dddbs` never follows the peer service: it is always the configured database service, and the
65
+ // peer service is reported through `ddprs` only, and only when it was set explicitly.
66
+ const peerService = peerData?.[PEER_SERVICE_KEY]
67
+ if (peerService && peerData[PEER_SERVICE_SOURCE_KEY] === PEER_SERVICE_KEY) {
68
+ return `${prefix},ddprs='${encode(peerService)}'`
66
69
  }
67
70
  return prefix
68
71
  }
69
72
 
70
- /**
71
- * @param {string} tracerService
72
- * @param {object} peerData
73
- */
74
- #getDbmServiceName (tracerService, peerData) {
75
- if (this._tracerConfig.spanComputePeerService) {
76
- return this.getPeerServiceRemap(peerData)[PEER_SERVICE_KEY] || tracerService
77
- }
78
- return tracerService
79
- }
80
-
81
73
  /**
82
74
  * @param {import('../../../..').Span} span
83
75
  * @param {string} serviceName
@@ -91,8 +83,7 @@ class DatabasePlugin extends StoragePlugin {
91
83
  }
92
84
 
93
85
  const peerData = this.getPeerService(span.context().getTags())
94
- const dbmService = this.#getDbmServiceName(serviceName, peerData)
95
- const servicePropagation = this.#createDBMPropagationCommentService(dbmService, span, peerData)
86
+ const servicePropagation = this.#createDBMPropagationCommentService(serviceName, span, peerData)
96
87
 
97
88
  let dbmComment = servicePropagation
98
89
 
@@ -45,7 +45,6 @@ const legacyStorage = storage('legacy')
45
45
  const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB
46
46
 
47
47
  function sanitizedExec (
48
- cmd,
49
48
  flags,
50
49
  operationMetric,
51
50
  durationMetric,
@@ -61,7 +60,7 @@ function sanitizedExec (
61
60
  startTime = Date.now()
62
61
  }
63
62
  try {
64
- let result = cachedExec(cmd, flags, { stdio: 'pipe' }).toString()
63
+ let result = cachedExec('git', flags, { stdio: 'pipe' }).toString()
65
64
 
66
65
  if (shouldTrim) {
67
66
  result = result.replaceAll(/(\r\n|\n|\r)/g, '')
@@ -106,7 +105,6 @@ function isGitAvailable () {
106
105
 
107
106
  function isShallowRepository () {
108
107
  return sanitizedExec(
109
- 'git',
110
108
  ['rev-parse', '--is-shallow-repository'],
111
109
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'check_shallow' } },
112
110
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'check_shallow' } },
@@ -115,7 +113,7 @@ function isShallowRepository () {
115
113
  }
116
114
 
117
115
  function getGitVersion () {
118
- const gitVersionString = sanitizedExec('git', ['version'])
116
+ const gitVersionString = sanitizedExec(['version'])
119
117
  const gitVersionMatches = /** @type {RegExpMatch} */ (gitVersionString.match(/git version (\d+)\.(\d+)\.(\d+)/))
120
118
  try {
121
119
  return {
@@ -138,8 +136,8 @@ function unshallowRepository (parentOnly = false) {
138
136
  log.warn('Git version is <2.27, so git unshallow will not proceed')
139
137
  return
140
138
  }
141
- const defaultRemoteName = sanitizedExec('git', ['config', '--default', 'origin', '--get', 'clone.defaultRemoteName'])
142
- const revParseHead = sanitizedExec('git', ['rev-parse', 'HEAD'])
139
+ const defaultRemoteName = sanitizedExec(['config', '--default', 'origin', '--get', 'clone.defaultRemoteName'])
140
+ const revParseHead = sanitizedExec(['rev-parse', 'HEAD'])
143
141
 
144
142
  const baseGitOptions = [
145
143
  'fetch',
@@ -167,7 +165,6 @@ function unshallowRepository (parentOnly = false) {
167
165
  { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
168
166
  )
169
167
  const upstreamRemote = sanitizedExec(
170
- 'git',
171
168
  ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']
172
169
  )
173
170
  flags = [
@@ -186,7 +183,6 @@ function unshallowRepository (parentOnly = false) {
186
183
  )
187
184
  // We use sanitizedExec here because if this last option fails, we'll give up.
188
185
  sanitizedExec(
189
- 'git',
190
186
  baseGitOptions,
191
187
  null,
192
188
  null,
@@ -199,7 +195,6 @@ function unshallowRepository (parentOnly = false) {
199
195
 
200
196
  function getRepositoryUrl () {
201
197
  return sanitizedExec(
202
- 'git',
203
198
  ['config', '--get', 'remote.origin.url'],
204
199
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_repository' } },
205
200
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_repository' } },
@@ -233,7 +228,6 @@ function getGitDiff (baseCommit, targetCommit) {
233
228
  flags.push(targetCommit)
234
229
  }
235
230
  return sanitizedExec(
236
- 'git',
237
231
  flags,
238
232
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'diff' } },
239
233
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'diff' } },
@@ -244,7 +238,6 @@ function getGitDiff (baseCommit, targetCommit) {
244
238
 
245
239
  function getGitRemoteName () {
246
240
  const upstreamRemote = sanitizedExec(
247
- 'git',
248
241
  ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
249
242
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_remote_name' } },
250
243
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_remote_name' } },
@@ -256,7 +249,6 @@ function getGitRemoteName () {
256
249
  }
257
250
 
258
251
  const remotes = sanitizedExec(
259
- 'git',
260
252
  ['remote'],
261
253
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_remote_name' } },
262
254
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_remote_name' } },
@@ -269,7 +261,6 @@ function getGitRemoteName () {
269
261
 
270
262
  function getSourceBranch () {
271
263
  return sanitizedExec(
272
- 'git',
273
264
  ['rev-parse', '--abbrev-ref', 'HEAD'],
274
265
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_source_branch' } },
275
266
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_source_branch' } },
@@ -315,7 +306,6 @@ function checkAndFetchBranch (branch, remoteName) {
315
306
 
316
307
  function getLocalBranches (remoteName) {
317
308
  const localBranches = sanitizedExec(
318
- 'git',
319
309
  ['for-each-ref', '--format=%(refname:short)', `refs/remotes/${remoteName}`],
320
310
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_local_branches' } },
321
311
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_local_branches' } },
@@ -331,7 +321,6 @@ function getLocalBranches (remoteName) {
331
321
 
332
322
  function getMergeBase (baseBranch, sourceBranch) {
333
323
  return sanitizedExec(
334
- 'git',
335
324
  ['merge-base', baseBranch, sourceBranch],
336
325
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_merge_base' } },
337
326
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_merge_base' } },
@@ -341,7 +330,6 @@ function getMergeBase (baseBranch, sourceBranch) {
341
330
 
342
331
  function getCounts (sourceBranch, candidateBranch) {
343
332
  const counts = sanitizedExec(
344
- 'git',
345
333
  ['rev-list', '--left-right', '--count', `${candidateBranch}...${sourceBranch}`],
346
334
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_counts' } },
347
335
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_counts' } },
@@ -460,7 +448,6 @@ function generatePackFilesForCommits (commitsToUpload) {
460
448
 
461
449
  function getRepositoryRoot () {
462
450
  return sanitizedExec(
463
- 'git',
464
451
  ['rev-parse', '--show-toplevel']
465
452
  )
466
453
  }
@@ -488,13 +475,13 @@ function getGitMetadata (ciMetadata) {
488
475
  committerName,
489
476
  committerEmail,
490
477
  committerDate,
491
- ] = sanitizedExec('git', ['show', '-s', '--format=%an,%ae,%aI,%cn,%ce,%cI']).split(',')
478
+ ] = sanitizedExec(['show', '-s', '--format=%an,%ae,%aI,%cn,%ce,%cI']).split(',')
492
479
 
493
480
  const tags = {
494
481
  [GIT_COMMIT_MESSAGE]:
495
- commitMessage || sanitizedExec('git', ['show', '-s', '--format=%B'], null, null, null, false),
496
- [GIT_BRANCH]: branch || sanitizedExec('git', ['rev-parse', '--abbrev-ref', 'HEAD']),
497
- [GIT_COMMIT_SHA]: commitSHA || sanitizedExec('git', ['rev-parse', 'HEAD']),
482
+ commitMessage || sanitizedExec(['show', '-s', '--format=%B'], null, null, null, false),
483
+ [GIT_BRANCH]: branch || sanitizedExec(['rev-parse', '--abbrev-ref', 'HEAD']),
484
+ [GIT_COMMIT_SHA]: commitSHA || sanitizedExec(['rev-parse', 'HEAD']),
498
485
  [CI_WORKSPACE_PATH]: ciWorkspacePath || getRepositoryRoot(),
499
486
  }
500
487
 
@@ -513,7 +500,6 @@ function getGitMetadata (ciMetadata) {
513
500
  headCommitterEmail,
514
501
  headCommitMessage,
515
502
  ] = sanitizedExec(
516
- 'git',
517
503
  [
518
504
  'show',
519
505
  '-s',
@@ -539,7 +525,7 @@ function getGitMetadata (ciMetadata) {
539
525
 
540
526
  const entries = [
541
527
  GIT_REPOSITORY_URL,
542
- filterSensitiveInfoFromRepository(repositoryUrl || sanitizedExec('git', ['ls-remote', '--get-url'])),
528
+ filterSensitiveInfoFromRepository(repositoryUrl || sanitizedExec(['ls-remote', '--get-url'])),
543
529
  GIT_COMMIT_AUTHOR_DATE, authorDate,
544
530
  GIT_COMMIT_AUTHOR_NAME, ciAuthorName || authorName,
545
531
  GIT_COMMIT_AUTHOR_EMAIL, ciAuthorEmail || authorEmail,
@@ -563,7 +549,6 @@ function getGitInformationDiscrepancy () {
563
549
  const gitRepositoryUrl = getRepositoryUrl()
564
550
 
565
551
  const gitCommitSHA = sanitizedExec(
566
- 'git',
567
552
  ['rev-parse', 'HEAD'],
568
553
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_commit_sha' } },
569
554
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_commit_sha' } },
@@ -577,7 +562,6 @@ function fetchHeadCommitSha (headSha) {
577
562
  const remoteName = getGitRemoteName()
578
563
 
579
564
  sanitizedExec(
580
- 'git',
581
565
  [
582
566
  'fetch',
583
567
  '--update-shallow',
@@ -51,11 +51,7 @@ const supportedProxies = {
51
51
  }
52
52
 
53
53
  function createInferredProxySpan (headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) {
54
- if (!headers) {
55
- return null
56
- }
57
-
58
- if (!tracer._config?.inferredProxyServicesEnabled) {
54
+ if (!headers || !tracer._config?.inferredProxyServicesEnabled) {
59
55
  return null
60
56
  }
61
57
 
@@ -5,6 +5,7 @@ const fs = require('fs')
5
5
  const { URL } = require('url')
6
6
  const { stripVTControlCharacters } = require('node:util')
7
7
  const { getLageTestSessionName } = require('../../ci-visibility/lage')
8
+ const resolveRunfilePath = require('../../ci-visibility/resolve-runfile-path')
8
9
  const log = require('../../log')
9
10
  const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper')
10
11
  const { getSegment } = require('../../util')
@@ -250,8 +251,11 @@ const TEST_LEVEL_EVENT_TYPES = [
250
251
  'test_module_end',
251
252
  'test_session_end',
252
253
  ]
254
+ const TEST_PARALLELIZATION_ENABLED = 'test.parallelization.enabled'
255
+ const TEST_PARALLELIZATION_PLAN_TAG_PREFIX = 'test.parallelization.plan.'
253
256
  const TEST_LEVELS_METADATA = 'test_levels'
254
257
  const TEST_LEVELS_METADATA_TAGS = [
258
+ TEST_PARALLELIZATION_ENABLED,
255
259
  CI_JOB_ID,
256
260
  CI_JOB_NAME,
257
261
  CI_JOB_URL,
@@ -809,12 +813,49 @@ function getTestEnvironmentMetadata (testFramework, config, shouldSkipGitMetadat
809
813
  ...userProvidedGitMetadata,
810
814
  ...runtimeAndOSMetadata,
811
815
  }
816
+ if (/^(true|1)$/i.test(getEnvironmentVariable('_DD_TEST_PARALLELIZATION_ENABLED'))) {
817
+ metadata[TEST_PARALLELIZATION_ENABLED] = 'true'
818
+ Object.assign(metadata, getTestParallelizationPlanMetadata())
819
+ }
812
820
  if (config && config.service) {
813
821
  metadata['service.name'] = config.service
814
822
  }
815
823
  return removeInvalidMetadata(metadata)
816
824
  }
817
825
 
826
+ /** @returns {Record<string, string>} */
827
+ function getTestParallelizationPlanMetadata () {
828
+ try {
829
+ let metadataFile = getEnvironmentVariable('_DD_TEST_PARALLELIZATION_METADATA_FILE')
830
+ if (!metadataFile) {
831
+ // The manifest locator belongs to DDTest's artifact protocol, rather than tracer configuration.
832
+ // eslint-disable-next-line eslint-rules/eslint-process-env
833
+ const env = process.env
834
+ const manifestFile = env.TEST_OPTIMIZATION_MANIFEST_FILE || env.DD_TEST_OPTIMIZATION_MANIFEST_FILE
835
+ const planDirectory = manifestFile
836
+ ? path.dirname(resolveRunfilePath(manifestFile, env))
837
+ : path.join(process.cwd(), '.testoptimization')
838
+ metadataFile = path.join(planDirectory, 'plan-metadata.json')
839
+ }
840
+ const { tags } = JSON.parse(fs.readFileSync(metadataFile, 'utf8'))
841
+ if (!tags || typeof tags !== 'object' || Array.isArray(tags) ||
842
+ Object.values(tags).some(value => typeof value !== 'string')) {
843
+ return {}
844
+ }
845
+ const metadata = {}
846
+ for (const [tag, value] of Object.entries(tags)) {
847
+ if (tag.startsWith(TEST_PARALLELIZATION_PLAN_TAG_PREFIX) &&
848
+ tag.length > TEST_PARALLELIZATION_PLAN_TAG_PREFIX.length && value) {
849
+ metadata[tag] = value
850
+ }
851
+ }
852
+ return metadata
853
+ } catch (err) {
854
+ log.debug('Failed to read DDTest plan metadata: %s', err.message)
855
+ return {}
856
+ }
857
+ }
858
+
818
859
  function getTestParametersString (parametersByTestName, testName) {
819
860
  if (!parametersByTestName[testName]) {
820
861
  return ''
@@ -845,6 +886,11 @@ function getTestLevelsMetadataTags (testEnvironmentMetadata) {
845
886
  testLevelsMetadataTags[key] = value
846
887
  }
847
888
  }
889
+ for (const [key, value] of Object.entries(testEnvironmentMetadata)) {
890
+ if (key.startsWith(TEST_PARALLELIZATION_PLAN_TAG_PREFIX) && value !== undefined) {
891
+ testLevelsMetadataTags[key] = value
892
+ }
893
+ }
848
894
  return testLevelsMetadataTags
849
895
  }
850
896
 
@@ -1556,8 +1602,7 @@ function applySkippedCoverageToFileCoverage (fileCoverage, skippedBitmap) {
1556
1602
  let updated = false
1557
1603
  for (const [statementId, statementLocation] of Object.entries(fileCoverage.data.statementMap)) {
1558
1604
  const startLine = statementLocation?.start?.line
1559
- if (!isLineCoveredByBitmap(skippedBitmap, startLine)) continue
1560
- if (fileCoverage.data.s[statementId] > 0) continue
1605
+ if (!isLineCoveredByBitmap(skippedBitmap, startLine) || fileCoverage.data.s[statementId] > 0) continue
1561
1606
 
1562
1607
  fileCoverage.data.s[statementId] = 1
1563
1608
  updated = true
@@ -2256,8 +2301,7 @@ function recordTestManagementExecution (execution, executions = testManagementEx
2256
2301
  * }} execution
2257
2302
  */
2258
2303
  function recordAttemptToFixExecution (attemptToFixExecutions, execution) {
2259
- if (!execution?.testName) return
2260
- if (getValueFromEnvSources('DD_TEST_MANAGEMENT_REPORT_ENABLED') === false) return
2304
+ if (!execution?.testName || getValueFromEnvSources('DD_TEST_MANAGEMENT_REPORT_ENABLED') === false) return
2261
2305
 
2262
2306
  const { testSuite, testName, status, isDisabled, isQuarantined } = execution
2263
2307
  const identity = getTestOptimizationIdentity(testSuite, testName)