dd-trace 5.116.0 → 5.118.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 (98) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +3 -3
  45. package/packages/datadog-instrumentations/src/cucumber.js +11 -12
  46. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  47. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  50. package/packages/datadog-instrumentations/src/jest.js +18 -17
  51. package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
  52. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  53. package/packages/datadog-instrumentations/src/playwright.js +3 -24
  54. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
  55. package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
  56. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  57. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  58. package/packages/datadog-plugin-cucumber/src/index.js +2 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  60. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  61. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  62. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  63. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  64. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  65. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  66. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  67. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  68. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  70. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  71. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  72. package/packages/dd-trace/src/exporter.js +16 -9
  73. package/packages/dd-trace/src/llmobs/index.js +9 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  75. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  76. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  77. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  78. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  79. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  80. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  81. package/packages/dd-trace/src/llmobs/util.js +56 -3
  82. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  83. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  84. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  85. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  86. package/packages/dd-trace/src/plugins/index.js +1 -0
  87. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  88. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  89. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  90. package/packages/dd-trace/src/span_processor.js +1 -1
  91. package/packages/dd-trace/src/span_stats.js +22 -4
  92. package/ci/test-optimization-validation/command-suitability.js +0 -471
  93. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  94. package/ci/test-optimization-validation/init-probe.js +0 -246
  95. package/ci/test-optimization-validation/late-initialization.js +0 -63
  96. package/ci/test-optimization-validation/local-command.js +0 -163
  97. package/ci/test-optimization-validation/setup-runner.js +0 -97
  98. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../dd-trace/src/log')
4
+
5
+ const SPAN_KIND_BY_TYPE = {
6
+ agent: 'agent',
7
+ function: 'tool',
8
+ handoff: 'tool',
9
+ guardrail: 'task',
10
+ custom: 'task',
11
+ generation: 'llm',
12
+ response: 'llm',
13
+ }
14
+
15
+ // Lifecycle methods are awaited by agents-core. Share one resolved Promise
16
+ // across every callback so we don't allocate per span event.
17
+ const RESOLVED = Promise.resolve()
18
+
19
+ /**
20
+ * dd-trace-js implementation of the agents-core `TracingProcessor` interface.
21
+ * Registered via `addTraceProcessor(new DDOpenAIAgentsProcessor(integration))` inside
22
+ * the `@openai/agents` module load hook. Mirrors Python's LLMObsTraceProcessor.
23
+ *
24
+ * Each agents-core Span / Trace lifecycle event turns into a dd-trace span
25
+ * (APM + LLMObs-annotated) keyed off the agents-core spanId / traceId. Parent
26
+ * hierarchy is resolved through the agents-core parentId chain, which gives us
27
+ * correct multi-agent handoff nesting that ctx-argument capture cannot provide.
28
+ *
29
+ * agents-core awaits the lifecycle methods, so each one returns a settled
30
+ * Promise even though the work is synchronous.
31
+ */
32
+ class DDOpenAIAgentsProcessor {
33
+ /**
34
+ * @param {() => (import('./integration').OpenAIAgentsIntegration | undefined)} getIntegration - Lazy accessor for the
35
+ * current OpenAIAgentsIntegration singleton. Read on each lifecycle event
36
+ * so re-instantiating the plugin doesn't strand the processor against an
37
+ * old integration reference inside agents-core.
38
+ */
39
+ constructor (getIntegration) {
40
+ this._getIntegration = getIntegration
41
+ }
42
+
43
+ onTraceStart (oaiTrace) {
44
+ const integration = this._getIntegration()
45
+ if (!integration?.enabled) return RESOLVED
46
+ try {
47
+ integration.startTrace(oaiTrace)
48
+ } catch (err) {
49
+ log.warn('[openai-agents] onTraceStart failed: %s', err)
50
+ }
51
+ return RESOLVED
52
+ }
53
+
54
+ onTraceEnd (oaiTrace) {
55
+ const integration = this._getIntegration()
56
+ if (!integration?.enabled) return RESOLVED
57
+ try {
58
+ integration.endTrace(oaiTrace)
59
+ } catch (err) {
60
+ log.warn('[openai-agents] onTraceEnd failed: %s', err)
61
+ }
62
+ return RESOLVED
63
+ }
64
+
65
+ onSpanStart (oaiSpan) {
66
+ const integration = this._getIntegration()
67
+ if (!integration?.enabled) return RESOLVED
68
+ if (!oaiSpan?.spanData) return RESOLVED // guard NoopSpan
69
+ const kind = SPAN_KIND_BY_TYPE[oaiSpan.spanData.type]
70
+ if (!kind) return RESOLVED // span types without an LLMObs kind are not traced
71
+ try {
72
+ integration.startSpan(oaiSpan, kind)
73
+ } catch (err) {
74
+ log.warn('[openai-agents] onSpanStart failed: %s', err)
75
+ }
76
+ return RESOLVED
77
+ }
78
+
79
+ onSpanEnd (oaiSpan) {
80
+ const integration = this._getIntegration()
81
+ if (!integration?.enabled) return RESOLVED
82
+ if (!oaiSpan?.spanData) return RESOLVED
83
+ try {
84
+ integration.endSpan(oaiSpan)
85
+ } catch (err) {
86
+ log.warn('[openai-agents] onSpanEnd failed: %s', err)
87
+ }
88
+ return RESOLVED
89
+ }
90
+
91
+ forceFlush () {
92
+ // dd-trace exports on its own schedule; nothing to force here.
93
+ return RESOLVED
94
+ }
95
+
96
+ shutdown () {
97
+ const integration = this._getIntegration()
98
+ if (!integration) return RESOLVED
99
+ try {
100
+ integration.clearState()
101
+ } catch (err) {
102
+ log.warn('[openai-agents] shutdown cleanup failed: %s', err)
103
+ }
104
+ return RESOLVED
105
+ }
106
+ }
107
+
108
+ module.exports = { DDOpenAIAgentsProcessor }
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ // agents-core's `error` is a plain `{ message, data }` object, not a JS Error
4
+ // — there's no constructor to name and no stack. We tag a stable type constant
5
+ // and stringify `data` into the message so the LLMObs error shape stays
6
+ // consistent with other integrations.
7
+ const AGENTS_ERROR_TYPE = 'AgentsCoreError'
8
+
9
+ /**
10
+ * Build the dd-trace span name from an agents-core oai-span. Handoffs collapse
11
+ * the target agent name into snake_case under a `transfer_to_` prefix; other
12
+ * span types use the SDK-provided name, falling back to
13
+ * `openai_agents.<type>` (or `openai_agents.request` when even the type is
14
+ * missing).
15
+ *
16
+ * @param {object} oaiSpan
17
+ * @returns {string}
18
+ */
19
+ function deriveSpanName (oaiSpan) {
20
+ const spanData = oaiSpan.spanData
21
+ if (spanData?.type === 'handoff') {
22
+ const toAgent = spanData.to_agent || ''
23
+ if (toAgent) return `transfer_to_${toAgent.replaceAll(' ', '_').toLowerCase()}`
24
+ }
25
+ if (spanData?.name) return spanData.name
26
+ return spanData?.type ? `openai_agents.${spanData.type}` : 'openai_agents.request'
27
+ }
28
+
29
+ /**
30
+ * Apply agents-core's error shape onto a dd-trace span. No-op when the
31
+ * oai-span has no error attached.
32
+ *
33
+ * @param {object} ddSpan
34
+ * @param {object} oaiSpan
35
+ */
36
+ function applyError (ddSpan, oaiSpan) {
37
+ const err = oaiSpan.error
38
+ if (!err) return
39
+
40
+ ddSpan.setTag('error', true)
41
+
42
+ let errorMessage = err.message || 'Error'
43
+ if (err.data) {
44
+ try {
45
+ errorMessage = JSON.stringify(err.data)
46
+ } catch {
47
+ // circular / non-serializable — fall back to the raw message
48
+ }
49
+ }
50
+
51
+ ddSpan.setTag('error.type', AGENTS_ERROR_TYPE)
52
+ ddSpan.setTag('error.message', errorMessage)
53
+ ddSpan.setTag('error.stack', err.stack || '')
54
+ }
55
+
56
+ module.exports = {
57
+ AGENTS_ERROR_TYPE,
58
+ deriveSpanName,
59
+ applyError,
60
+ }
@@ -388,7 +388,10 @@ class PlaywrightPlugin extends CiPlugin {
388
388
  earlyFlakeAbortReason,
389
389
  onDone,
390
390
  }) => {
391
- if (!span) return
391
+ if (!span) {
392
+ onDone?.()
393
+ return
394
+ }
392
395
 
393
396
  const isRUMActive = span.context().getTag(TEST_IS_RUM_ACTIVE)
394
397
 
@@ -400,7 +400,7 @@ class VitestPlugin extends CiPlugin {
400
400
  return ctx.currentStore
401
401
  })
402
402
 
403
- this.addSub('ci:vitest:test-suite:finish', ({ testSuiteSpan, status, deferFlush, onFinish }) => {
403
+ this.addSub('ci:vitest:test-suite:finish', ({ testSuiteSpan, status, deferFlush, onDone }) => {
404
404
  if (testSuiteSpan) {
405
405
  testSuiteSpan.setTag(TEST_STATUS, status)
406
406
  testSuiteSpan.finish()
@@ -408,10 +408,10 @@ class VitestPlugin extends CiPlugin {
408
408
  }
409
409
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
410
410
  if (deferFlush) {
411
- onFinish()
411
+ onDone()
412
412
  return
413
413
  }
414
- this.tracer._exporter.flush(onFinish)
414
+ this.tracer._exporter.flush(onDone)
415
415
  if (this.runningTestProbe) {
416
416
  this.removeDiProbe(this.runningTestProbe)
417
417
  }
@@ -442,7 +442,7 @@ class VitestPlugin extends CiPlugin {
442
442
  requestErrorTags,
443
443
  vitestPool,
444
444
  isVitestNoWorkerInitActive,
445
- onFinish,
445
+ onDone,
446
446
  }) => {
447
447
  for (const [tag, value] of Object.entries(requestErrorTags || {})) {
448
448
  this.testSessionSpan.setTag(tag, value)
@@ -481,7 +481,7 @@ class VitestPlugin extends CiPlugin {
481
481
  provider: this.ciProviderName,
482
482
  autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
483
483
  })
484
- this.tracer._exporter.flush(onFinish)
484
+ this.tracer._exporter.flush(onDone)
485
485
  })
486
486
 
487
487
  this.addSub('ci:vitest:coverage-report', ({ rootDir, onDone }) => {
@@ -20,6 +20,7 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags'
20
20
  const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
21
21
 
22
22
  const hostname = getHostname()
23
+ const EMPTY_SETTINGS = Object.freeze({})
23
24
 
24
25
  function getTestConfigurationTags (tags) {
25
26
  if (!tags) {
@@ -308,60 +309,58 @@ class CiVisibilityExporter extends BufferingExporter {
308
309
  }
309
310
 
310
311
  // Takes into account potential kill switches
311
- filterConfiguration (remoteConfiguration) {
312
- if (!remoteConfiguration) {
313
- return {}
314
- }
312
+ filterConfiguration (remoteConfiguration = EMPTY_SETTINGS) {
313
+ const { testOptimization } = this._config
315
314
  const {
316
- isCodeCoverageEnabled,
317
- isSuitesSkippingEnabled,
318
- isItrEnabled,
319
- requireGit,
320
- isEarlyFlakeDetectionEnabled,
315
+ DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: isEarlyFlakeDetectionAllowed,
316
+ DD_CIVISIBILITY_FLAKY_RETRY_COUNT: flakyTestRetriesCount = 0,
317
+ DD_CIVISIBILITY_FLAKY_RETRY_ENABLED: isFlakyTestRetriesAllowed,
318
+ DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED: isImpactedTestsAllowed,
319
+ DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: earlyFlakeDetectionRetryCount,
320
+ DD_TEST_FAILED_TEST_REPLAY_ENABLED: isFailedTestReplayAllowed,
321
+ DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: configuredAttemptToFixRetries = 0,
322
+ DD_TEST_MANAGEMENT_ENABLED: isTestManagementAllowed,
323
+ } = testOptimization
324
+ const hasEarlyFlakeDetectionRetryCount = earlyFlakeDetectionRetryCount !== undefined
325
+ const configuredSlowTestRetries = remoteConfiguration.earlyFlakeDetectionSlowTestRetries ?? EMPTY_SETTINGS
326
+ const earlyFlakeDetectionSlowTestRetries = hasEarlyFlakeDetectionRetryCount
327
+ ? Object.freeze({
328
+ '5s': earlyFlakeDetectionRetryCount,
329
+ '10s': earlyFlakeDetectionRetryCount,
330
+ '30s': earlyFlakeDetectionRetryCount,
331
+ '5m': earlyFlakeDetectionRetryCount,
332
+ })
333
+ : Object.isFrozen(configuredSlowTestRetries)
334
+ ? configuredSlowTestRetries
335
+ : Object.freeze({ ...configuredSlowTestRetries })
336
+ const earlyFlakeDetectionNumRetries = hasEarlyFlakeDetectionRetryCount
337
+ ? earlyFlakeDetectionRetryCount
338
+ : remoteConfiguration.earlyFlakeDetectionNumRetries ?? 0
339
+ const testManagementAttemptToFixRetries =
340
+ remoteConfiguration.testManagementAttemptToFixRetries ?? configuredAttemptToFixRetries
341
+
342
+ return Object.freeze({
343
+ isCodeCoverageEnabled: remoteConfiguration.isCodeCoverageEnabled === true,
344
+ isSuitesSkippingEnabled: remoteConfiguration.isSuitesSkippingEnabled === true,
345
+ isItrEnabled: remoteConfiguration.isItrEnabled === true,
346
+ requireGit: remoteConfiguration.requireGit === true,
347
+ isEarlyFlakeDetectionEnabled:
348
+ remoteConfiguration.isEarlyFlakeDetectionEnabled === true && isEarlyFlakeDetectionAllowed === true,
321
349
  earlyFlakeDetectionNumRetries,
322
350
  earlyFlakeDetectionSlowTestRetries,
323
- earlyFlakeDetectionFaultyThreshold,
324
- isFlakyTestRetriesEnabled,
325
- isDiEnabled,
326
- isKnownTestsEnabled,
327
- isTestManagementEnabled,
351
+ earlyFlakeDetectionFaultyThreshold: remoteConfiguration.earlyFlakeDetectionFaultyThreshold ?? 30,
352
+ isFlakyTestRetriesEnabled:
353
+ remoteConfiguration.isFlakyTestRetriesEnabled === true && isFlakyTestRetriesAllowed === true,
354
+ flakyTestRetriesCount,
355
+ isDiEnabled: remoteConfiguration.isDiEnabled === true && isFailedTestReplayAllowed === true,
356
+ isKnownTestsEnabled: remoteConfiguration.isKnownTestsEnabled === true,
357
+ isTestManagementEnabled:
358
+ remoteConfiguration.isTestManagementEnabled === true && isTestManagementAllowed === true,
328
359
  testManagementAttemptToFixRetries,
329
- isImpactedTestsEnabled,
330
- isCoverageReportUploadEnabled,
331
- } = remoteConfiguration
332
- const { testOptimization } = this._config
333
- const earlyFlakeDetectionRetryCount =
334
- testOptimization.DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT
335
- const hasEarlyFlakeDetectionRetryCount = earlyFlakeDetectionRetryCount !== undefined
336
- return {
337
- isCodeCoverageEnabled,
338
- isSuitesSkippingEnabled,
339
- isItrEnabled,
340
- requireGit,
341
- isEarlyFlakeDetectionEnabled:
342
- isEarlyFlakeDetectionEnabled && testOptimization.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
343
- earlyFlakeDetectionNumRetries:
344
- hasEarlyFlakeDetectionRetryCount ? earlyFlakeDetectionRetryCount : earlyFlakeDetectionNumRetries,
345
- earlyFlakeDetectionSlowTestRetries: hasEarlyFlakeDetectionRetryCount
346
- ? {
347
- '5s': earlyFlakeDetectionRetryCount,
348
- '10s': earlyFlakeDetectionRetryCount,
349
- '30s': earlyFlakeDetectionRetryCount,
350
- '5m': earlyFlakeDetectionRetryCount,
351
- }
352
- : earlyFlakeDetectionSlowTestRetries,
353
- earlyFlakeDetectionFaultyThreshold,
354
- isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_ENABLED,
355
- flakyTestRetriesCount: testOptimization.DD_CIVISIBILITY_FLAKY_RETRY_COUNT,
356
- isDiEnabled: isDiEnabled && testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED,
357
- isKnownTestsEnabled,
358
- isTestManagementEnabled: isTestManagementEnabled && testOptimization.DD_TEST_MANAGEMENT_ENABLED,
359
- testManagementAttemptToFixRetries:
360
- testManagementAttemptToFixRetries ?? testOptimization.DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES,
361
360
  isImpactedTestsEnabled:
362
- isImpactedTestsEnabled && testOptimization.DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED,
363
- isCoverageReportUploadEnabled,
364
- }
361
+ remoteConfiguration.isImpactedTestsEnabled === true && isImpactedTestsAllowed === true,
362
+ isCoverageReportUploadEnabled: remoteConfiguration.isCoverageReportUploadEnabled === true,
363
+ })
365
364
  }
366
365
 
367
366
  sendGitMetadata (repositoryUrl) {
@@ -12,12 +12,145 @@ const {
12
12
  TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE,
13
13
  } = require('../telemetry')
14
14
  const { writeSettingsToCache } = require('../test-optimization-cache')
15
- const { validateSettingsResponse } = require('../test-optimization-http-cache-schema')
15
+ const { MAX_RETRIES, validateSettingsResponse } = require('../test-optimization-http-cache-schema')
16
16
  const request = require('./request')
17
17
 
18
18
  const DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES = 2
19
- const DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES = { '5s': 10, '10s': 5, '30s': 3, '5m': 2 }
19
+ const DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES = Object.freeze({
20
+ '5s': 10,
21
+ '10s': 5,
22
+ '30s': 3,
23
+ '5m': 2,
24
+ })
20
25
  const DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD = 30
26
+ const EARLY_FLAKE_DETECTION_RETRY_BUCKETS = Object.keys(DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES)
27
+
28
+ /**
29
+ * @typedef {object} EarlyFlakeDetectionSettings
30
+ * @property {boolean} enabled
31
+ * @property {number} numRetries
32
+ * @property {Readonly<Record<string, number>>} slowTestRetries
33
+ * @property {number} faultyThreshold
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} TestManagementSettings
38
+ * @property {boolean} enabled
39
+ * @property {number|undefined} attemptToFixRetries
40
+ */
41
+
42
+ /**
43
+ * @param {unknown} value
44
+ * @returns {value is Record<string, unknown>}
45
+ */
46
+ function isRecord (value) {
47
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
48
+ }
49
+
50
+ /**
51
+ * @param {unknown} value
52
+ * @returns {value is number}
53
+ */
54
+ function isNonNegativeInteger (value) {
55
+ return Number.isInteger(value) && value >= 0
56
+ }
57
+
58
+ /**
59
+ * @param {unknown} value
60
+ * @returns {value is number}
61
+ */
62
+ function isRetryCount (value) {
63
+ return isNonNegativeInteger(value) && value <= MAX_RETRIES
64
+ }
65
+
66
+ /**
67
+ * @param {unknown} value
68
+ * @returns {Readonly<Record<string, number>>|undefined}
69
+ */
70
+ function parseSlowTestRetries (value) {
71
+ if (!isRecord(value)) return
72
+
73
+ const slowTestRetries = {}
74
+ for (const bucket of EARLY_FLAKE_DETECTION_RETRY_BUCKETS) {
75
+ if (!Object.hasOwn(value, bucket)) continue
76
+
77
+ const retries = value[bucket]
78
+ if (!isRetryCount(retries)) return
79
+ slowTestRetries[bucket] = retries
80
+ }
81
+ return Object.freeze(slowTestRetries)
82
+ }
83
+
84
+ /**
85
+ * @param {unknown} value
86
+ * @param {boolean} isKnownTestsEnabled
87
+ * @returns {EarlyFlakeDetectionSettings}
88
+ */
89
+ function parseEarlyFlakeDetectionSettings (value, isKnownTestsEnabled) {
90
+ if (!isRecord(value) || value.enabled !== true) {
91
+ return {
92
+ enabled: false,
93
+ numRetries: DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
94
+ slowTestRetries: DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
95
+ faultyThreshold: DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
96
+ }
97
+ }
98
+
99
+ let isValid = true
100
+ let numRetries = DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES
101
+ let slowTestRetries = DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES
102
+ if (Object.hasOwn(value, 'slow_test_retries')) {
103
+ const parsedSlowTestRetries = parseSlowTestRetries(value.slow_test_retries)
104
+ if (parsedSlowTestRetries) {
105
+ slowTestRetries = parsedSlowTestRetries
106
+ numRetries = parsedSlowTestRetries['5s'] ?? DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES
107
+ } else {
108
+ isValid = false
109
+ }
110
+ }
111
+
112
+ let faultyThreshold = DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD
113
+ if (Object.hasOwn(value, 'faulty_session_threshold')) {
114
+ if (isNonNegativeInteger(value.faulty_session_threshold) && value.faulty_session_threshold <= 100) {
115
+ faultyThreshold = value.faulty_session_threshold
116
+ } else {
117
+ isValid = false
118
+ }
119
+ }
120
+
121
+ return {
122
+ enabled: isKnownTestsEnabled && isValid,
123
+ numRetries,
124
+ slowTestRetries,
125
+ faultyThreshold,
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @param {unknown} value
131
+ * @returns {TestManagementSettings}
132
+ */
133
+ function parseTestManagementSettings (value) {
134
+ if (!isRecord(value) || value.enabled !== true) {
135
+ return {
136
+ enabled: false,
137
+ attemptToFixRetries: undefined,
138
+ }
139
+ }
140
+
141
+ const attemptToFixRetries = value.attempt_to_fix_retries
142
+ if (attemptToFixRetries !== undefined && !isRetryCount(attemptToFixRetries)) {
143
+ return {
144
+ enabled: false,
145
+ attemptToFixRetries: undefined,
146
+ }
147
+ }
148
+
149
+ return {
150
+ enabled: true,
151
+ attemptToFixRetries,
152
+ }
153
+ }
21
154
 
22
155
  function parseJsonResponse (rawJson) {
23
156
  return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
@@ -28,40 +161,35 @@ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), optio
28
161
  if (options.validateRequiredFields) {
29
162
  validateSettingsResponse(parsedResponse, options)
30
163
  }
31
- const {
32
- code_coverage: isCodeCoverageEnabled,
33
- tests_skipping: isSuitesSkippingEnabled,
34
- itr_enabled: isItrEnabled,
35
- require_git: requireGit,
36
- early_flake_detection: earlyFlakeDetectionConfig,
37
- flaky_test_retries_enabled: isFlakyTestRetriesEnabled,
38
- di_enabled: isDiEnabled,
39
- known_tests_enabled: isKnownTestsEnabled,
40
- test_management: testManagementConfig,
41
- impacted_tests_enabled: isImpactedTestsEnabled,
42
- coverage_report_upload_enabled: isCoverageReportUploadEnabled,
43
- } = parsedResponse?.data?.attributes ?? parsedResponse
164
+ const parsedAttributes = parsedResponse?.data?.attributes ?? parsedResponse
165
+ if (!isRecord(parsedAttributes)) {
166
+ throw new TypeError('Invalid settings response: attributes must be an object')
167
+ }
168
+ const attributes = parsedAttributes
169
+ const isKnownTestsEnabled = attributes.known_tests_enabled === true
170
+ const isFlakyTestRetriesEnabled = attributes.flaky_test_retries_enabled === true
171
+ const earlyFlakeDetection = parseEarlyFlakeDetectionSettings(
172
+ attributes.early_flake_detection,
173
+ isKnownTestsEnabled
174
+ )
175
+ const testManagement = parseTestManagementSettings(attributes.test_management)
44
176
 
45
177
  const settings = {
46
- isCodeCoverageEnabled,
47
- isSuitesSkippingEnabled,
48
- isItrEnabled,
49
- requireGit,
50
- isEarlyFlakeDetectionEnabled: isKnownTestsEnabled && (earlyFlakeDetectionConfig?.enabled ?? false),
51
- earlyFlakeDetectionNumRetries:
52
- earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] ?? DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
53
- earlyFlakeDetectionSlowTestRetries:
54
- earlyFlakeDetectionConfig?.slow_test_retries ?? DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
55
- earlyFlakeDetectionFaultyThreshold:
56
- earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
178
+ isCodeCoverageEnabled: attributes.code_coverage === true,
179
+ isSuitesSkippingEnabled: attributes.tests_skipping === true,
180
+ isItrEnabled: attributes.itr_enabled === true,
181
+ requireGit: attributes.require_git === true,
182
+ isEarlyFlakeDetectionEnabled: earlyFlakeDetection.enabled,
183
+ earlyFlakeDetectionNumRetries: earlyFlakeDetection.numRetries,
184
+ earlyFlakeDetectionSlowTestRetries: earlyFlakeDetection.slowTestRetries,
185
+ earlyFlakeDetectionFaultyThreshold: earlyFlakeDetection.faultyThreshold,
57
186
  isFlakyTestRetriesEnabled,
58
- isDiEnabled: isDiEnabled && isFlakyTestRetriesEnabled,
187
+ isDiEnabled: attributes.di_enabled === true && isFlakyTestRetriesEnabled,
59
188
  isKnownTestsEnabled,
60
- isTestManagementEnabled: (testManagementConfig?.enabled ?? false),
61
- testManagementAttemptToFixRetries:
62
- testManagementConfig?.attempt_to_fix_retries,
63
- isImpactedTestsEnabled,
64
- isCoverageReportUploadEnabled: isCoverageReportUploadEnabled ?? false,
189
+ isTestManagementEnabled: testManagement.enabled,
190
+ testManagementAttemptToFixRetries: testManagement.attemptToFixRetries,
191
+ isImpactedTestsEnabled: attributes.impacted_tests_enabled === true,
192
+ isCoverageReportUploadEnabled: attributes.coverage_report_upload_enabled === true,
65
193
  }
66
194
 
67
195
  log.debug('Remote settings: %j', settings)
@@ -82,7 +210,7 @@ function parseLibraryConfigurationResponse (rawJson, config = getConfig(), optio
82
210
  log.debug('Code coverage report upload was disabled by the environment variable')
83
211
  }
84
212
 
85
- return settings
213
+ return Object.freeze(settings)
86
214
  }
87
215
 
88
216
  function getLibraryConfiguration ({
@@ -30,7 +30,7 @@ const RETRY_THRESHOLD_PATTERN = /^\d+(?:ms|s|m|h)$/
30
30
  const MAX_VALIDATION_MODULES = 1000
31
31
  const MAX_VALIDATION_SUITES = 10_000
32
32
  const MAX_VALIDATION_TESTS = 100_000
33
- const MAX_VALIDATION_RETRIES = 100
33
+ const MAX_RETRIES = 100
34
34
  const MAX_VALIDATION_STRING_BYTES = 4096
35
35
 
36
36
  function isObject (value) {
@@ -75,7 +75,8 @@ function validateSettingsResponse (response, options = {}) {
75
75
  assertBooleanFields(attributes.test_management, ['enabled'], 'settings test_management')
76
76
  assertOptionalBoundedNumber(
77
77
  attributes.early_flake_detection.faulty_session_threshold,
78
- 'settings early_flake_detection faulty_session_threshold'
78
+ 'settings early_flake_detection faulty_session_threshold',
79
+ { integer: true }
79
80
  )
80
81
  assertRetryMap(attributes.early_flake_detection.slow_test_retries)
81
82
  assertOptionalBoundedNumber(
@@ -216,8 +217,8 @@ function assertBooleanFields (object, keys, endpoint) {
216
217
  function assertOptionalBoundedNumber (value, endpoint, { integer = false } = {}) {
217
218
  if (value === undefined) return
218
219
  if (!Number.isFinite(value) || (integer && !Number.isInteger(value)) ||
219
- value < 0 || value > MAX_VALIDATION_RETRIES) {
220
- throw new TypeError(`Invalid ${endpoint}: value must be between 0 and ${MAX_VALIDATION_RETRIES}`)
220
+ value < 0 || value > MAX_RETRIES) {
221
+ throw new TypeError(`Invalid ${endpoint}: value must be between 0 and ${MAX_RETRIES}`)
221
222
  }
222
223
  }
223
224
 
@@ -249,6 +250,7 @@ function assertValidationString (value, field) {
249
250
  }
250
251
 
251
252
  module.exports = {
253
+ MAX_RETRIES,
252
254
  validateKnownTestsResponse,
253
255
  validateSettingsResponse,
254
256
  validateSkippableTestsResponse,
@@ -333,6 +333,7 @@ export interface GeneratedConfig {
333
333
  DD_TRACE_NODE_SERIALIZE_ENABLED: boolean;
334
334
  DD_TRACE_NYC_ENABLED: boolean;
335
335
  DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP: string;
336
+ DD_TRACE_OPENAI_AGENTS_ENABLED: boolean;
336
337
  DD_TRACE_OPENAI_ENABLED: boolean;
337
338
  DD_TRACE_OPENSEARCH_ENABLED: boolean;
338
339
  DD_TRACE_OPENSEARCH_PROJECT_OPENSEARCH_ENABLED: boolean;
@@ -1028,6 +1029,7 @@ export interface GeneratedEnvVarConfig {
1028
1029
  DD_TRACE_NODE_SERIALIZE_ENABLED: boolean;
1029
1030
  DD_TRACE_NYC_ENABLED: boolean;
1030
1031
  DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP: string;
1032
+ DD_TRACE_OPENAI_AGENTS_ENABLED: boolean;
1031
1033
  DD_TRACE_OPENAI_ENABLED: boolean;
1032
1034
  DD_TRACE_OPENSEARCH_ENABLED: boolean;
1033
1035
  DD_TRACE_OPENSEARCH_PROJECT_OPENSEARCH_ENABLED: boolean;
@@ -3452,6 +3452,13 @@
3452
3452
  "default": "true"
3453
3453
  }
3454
3454
  ],
3455
+ "DD_TRACE_OPENAI_AGENTS_ENABLED": [
3456
+ {
3457
+ "implementation": "A",
3458
+ "type": "boolean",
3459
+ "default": "true"
3460
+ }
3461
+ ],
3455
3462
  "DD_TRACE_OPENSEARCH_ENABLED": [
3456
3463
  {
3457
3464
  "implementation": "A",
@@ -31,7 +31,7 @@ class SpanStatsEncoder extends AgentEncoder {
31
31
  }
32
32
 
33
33
  _encodeStat (bytes, stat) {
34
- bytes.writeMapPrefix(15)
34
+ bytes.writeMapPrefix(17)
35
35
 
36
36
  this._encodeString(bytes, 'Service')
37
37
  const service = stat.Service || DEFAULT_SERVICE_NAME
@@ -79,6 +79,12 @@ class SpanStatsEncoder extends AgentEncoder {
79
79
 
80
80
  this._encodeString(bytes, 'srv_src')
81
81
  this._encodeString(bytes, stat.srv_src || '')
82
+
83
+ this._encodeString(bytes, 'SpanKind')
84
+ this._encodeString(bytes, stat.SpanKind || '')
85
+
86
+ this._encodeString(bytes, 'GRPCStatusCode')
87
+ this._encodeString(bytes, stat.GRPCStatusCode || '')
82
88
  }
83
89
 
84
90
  _encodeBucket (bytes, bucket) {