dd-trace 5.87.0 → 5.88.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 (77) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +225 -4
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  15. package/packages/datadog-instrumentations/src/jest.js +76 -12
  16. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  17. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  18. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  19. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  20. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  21. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  22. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
  24. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  25. package/packages/datadog-plugin-jest/src/index.js +12 -2
  26. package/packages/datadog-plugin-jest/src/util.js +2 -1
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  28. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  29. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  30. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  31. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  32. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  34. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  36. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  40. package/packages/dd-trace/src/azure_metadata.js +0 -2
  41. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  43. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  44. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  45. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  46. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  47. package/packages/dd-trace/src/config/defaults.js +148 -197
  48. package/packages/dd-trace/src/config/helper.js +43 -1
  49. package/packages/dd-trace/src/config/index.js +36 -14
  50. package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
  51. package/packages/dd-trace/src/constants.js +0 -2
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  53. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  54. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  55. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  56. package/packages/dd-trace/src/exporter.js +2 -0
  57. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  58. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  59. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  60. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  61. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  62. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  63. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  64. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  65. package/packages/dd-trace/src/opentracing/span.js +6 -4
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  67. package/packages/dd-trace/src/plugins/database.js +15 -2
  68. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  69. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  70. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  71. package/packages/dd-trace/src/proxy.js +4 -0
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  73. package/packages/dd-trace/src/startup-log.js +1 -1
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  75. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  76. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  77. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -275,9 +275,11 @@ class VitestPlugin extends CiPlugin {
275
275
  this.addBind('ci:vitest:test-suite:start', (ctx) => {
276
276
  const { testSuiteAbsolutePath, frameworkVersion } = ctx
277
277
 
278
+ // TODO: Handle case where the command is not set
278
279
  this.command = getValueFromEnvSources('DD_CIVISIBILITY_TEST_COMMAND')
279
280
  this.frameworkVersion = frameworkVersion
280
281
  const testSessionSpanContext = this.tracer.extract('text_map', {
282
+ // TODO: Handle case where the session ID or module ID is not set
281
283
  'x-datadog-trace-id': getValueFromEnvSources('DD_CIVISIBILITY_TEST_SESSION_ID'),
282
284
  'x-datadog-parent-id': getValueFromEnvSources('DD_CIVISIBILITY_TEST_MODULE_ID'),
283
285
  })
@@ -301,14 +303,17 @@ class VitestPlugin extends CiPlugin {
301
303
  }
302
304
 
303
305
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
304
- const testSuiteMetadata = getTestSuiteCommonTags(
305
- this.command,
306
- this.frameworkVersion,
307
- testSuite,
308
- 'vitest'
309
- )
310
- testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
311
- testSuiteMetadata[TEST_SOURCE_START] = 1
306
+ // Request error tags are applied to test spans in the main process (worker-report:trace handler)
307
+ const testSuiteMetadata = {
308
+ ...getTestSuiteCommonTags(
309
+ this.command,
310
+ this.frameworkVersion,
311
+ testSuite,
312
+ 'vitest'
313
+ ),
314
+ [TEST_SOURCE_FILE]: testSuite,
315
+ [TEST_SOURCE_START]: 1,
316
+ }
312
317
 
313
318
  const codeOwners = this.getCodeOwners(testSuiteMetadata)
314
319
  if (codeOwners) {
@@ -39,7 +39,7 @@ class CookieAnalyzer extends Analyzer {
39
39
  }
40
40
 
41
41
  _checkOCE (context, value) {
42
- if (value && value.location) {
42
+ if (value?.location) {
43
43
  return true
44
44
  }
45
45
  return super._checkOCE(context, value)
@@ -12,7 +12,7 @@ class SSRFAnalyzer extends InjectionAnalyzer {
12
12
  this.addSub('apm:http:client:request:start', ({ args }) => {
13
13
  if (typeof args.originalUrl === 'string') {
14
14
  this.analyze(args.originalUrl)
15
- } else if (args.options && args.options.host) {
15
+ } else if (args.options?.host) {
16
16
  this.analyze(args.options.host)
17
17
  }
18
18
  })
@@ -36,7 +36,7 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
36
36
  }
37
37
 
38
38
  isLocationHeader (name) {
39
- return name && name.trim().toLowerCase() === 'location'
39
+ return name?.trim().toLowerCase() === 'location'
40
40
  }
41
41
 
42
42
  _isVulnerable (value, iastContext) {
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { storage } = require('../../../../../datadog-core')
4
- const { getNonDDCallSiteFrames } = require('../path-line')
4
+ const { getCallSiteFramesForLocation } = require('../path-line')
5
5
  const { getIastContext, getIastStackTraceId } = require('../iast-context')
6
6
  const overheadController = require('../overhead-controller')
7
7
  const { SinkIastPlugin } = require('../iast-plugin')
@@ -35,9 +35,8 @@ class Analyzer extends SinkIastPlugin {
35
35
 
36
36
  _reportEvidence (value, context, evidence) {
37
37
  const callSiteFrames = getVulnerabilityCallSiteFrames()
38
- const nonDDCallSiteFrames = getNonDDCallSiteFrames(callSiteFrames, this._getExcludedPaths())
39
-
40
- const location = this._getLocation(value, nonDDCallSiteFrames)
38
+ const frames = getCallSiteFramesForLocation(callSiteFrames, this._getExcludedPaths())
39
+ const location = this._getLocation(value, frames)
41
40
 
42
41
  if (!this._isExcluded(location)) {
43
42
  const originalLocation = this._getOriginalLocation(location)
@@ -51,7 +50,7 @@ class Analyzer extends SinkIastPlugin {
51
50
  stackId
52
51
  )
53
52
 
54
- addVulnerability(context, vulnerability, nonDDCallSiteFrames)
53
+ addVulnerability(context, vulnerability, frames)
55
54
  }
56
55
  }
57
56
 
@@ -8,29 +8,40 @@ const { getOriginalPathAndLineFromSourceMap } = require('./taint-tracking/rewrit
8
8
  const pathLine = {
9
9
  getNodeModulesPaths,
10
10
  getRelativePath,
11
- getNonDDCallSiteFrames,
11
+ getCallSiteFramesForLocation,
12
12
  ddBasePath, // Exported only for test purposes
13
13
  }
14
14
 
15
15
  const EXCLUDED_PATHS = [
16
16
  path.join(path.sep, 'node_modules', 'dc-polyfill'),
17
17
  ]
18
- const EXCLUDED_PATH_PREFIXES = [
19
- 'node:diagnostics_channel',
20
- 'diagnostics_channel',
21
- 'node:child_process',
22
- 'child_process',
23
- 'node:async_hooks',
24
- 'async_hooks',
25
- 'node:internal/async_local_storage',
26
- ]
27
18
 
28
- function getNonDDCallSiteFrames (callSiteFrames, externallyExcludedPaths) {
19
+ /**
20
+ * Processes and filters call site frames to find the best location for a vulnerability.
21
+ * Returns client frames if available, otherwise falls back to all processed frames.
22
+ * Excludes dd-trace frames and all Node.js built-in/internal modules (node:*).
23
+ * @param {CallSiteFrame[]} callSiteFrames
24
+ * @param {string[]} externallyExcludedPaths
25
+ * @returns {CallSiteFrame[]} Client frames if available, otherwise all processed frames
26
+ *
27
+ * @typedef {object} CallSiteFrame
28
+ * @property {number} id
29
+ * @property {string} file - Original file path
30
+ * @property {number} line
31
+ * @property {number} column
32
+ * @property {string} function
33
+ * @property {string} class_name
34
+ * @property {boolean} isNative
35
+ * @property {string} [path] - Relative path, added during processing
36
+ * @property {boolean} [isInternal] - Whether the frame is internal, added during processing
37
+ */
38
+ function getCallSiteFramesForLocation (callSiteFrames, externallyExcludedPaths) {
29
39
  if (!callSiteFrames) {
30
40
  return []
31
41
  }
32
42
 
33
- const result = []
43
+ const allFrames = []
44
+ const clientFrames = []
34
45
 
35
46
  for (const callsite of callSiteFrames) {
36
47
  let filepath = callsite.file?.startsWith('file://') ? fileURLToPath(callsite.file) : callsite.file
@@ -47,18 +58,22 @@ function getNonDDCallSiteFrames (callSiteFrames, externallyExcludedPaths) {
47
58
  callsite.column = column
48
59
  }
49
60
 
50
- if (
51
- !isExcluded(callsite, externallyExcludedPaths) &&
52
- (!filepath.includes(pathLine.ddBasePath) || globalThis.__DD_ESBUILD_IAST_WITH_NO_SM)
53
- ) {
61
+ if (filepath) {
54
62
  callsite.path = getRelativePath(filepath)
55
63
  callsite.isInternal = !path.isAbsolute(filepath)
56
64
 
57
- result.push(callsite)
65
+ allFrames.push(callsite)
66
+
67
+ if (
68
+ !isExcluded(callsite, externallyExcludedPaths) &&
69
+ (!filepath.includes(pathLine.ddBasePath) || globalThis.__DD_ESBUILD_IAST_WITH_NO_SM)
70
+ ) {
71
+ clientFrames.push(callsite)
72
+ }
58
73
  }
59
74
  }
60
75
 
61
- return result
76
+ return clientFrames.length > 0 ? clientFrames : allFrames
62
77
  }
63
78
 
64
79
  function getRelativePath (filepath) {
@@ -67,10 +82,12 @@ function getRelativePath (filepath) {
67
82
 
68
83
  function isExcluded (callsite, externallyExcludedPaths) {
69
84
  if (callsite.isNative) return true
85
+
70
86
  const filename = globalThis.__DD_ESBUILD_IAST_WITH_SM ? callsite.path : callsite.file
71
- if (!filename) {
87
+ if (!filename || filename.startsWith('node:')) {
72
88
  return true
73
89
  }
90
+
74
91
  let excludedPaths = EXCLUDED_PATHS
75
92
  if (externallyExcludedPaths) {
76
93
  excludedPaths = [...excludedPaths, ...externallyExcludedPaths]
@@ -82,12 +99,6 @@ function isExcluded (callsite, externallyExcludedPaths) {
82
99
  }
83
100
  }
84
101
 
85
- for (const EXCLUDED_PATH_PREFIX of EXCLUDED_PATH_PREFIXES) {
86
- if (filename.indexOf(EXCLUDED_PATH_PREFIX) === 0) {
87
- return true
88
- }
89
- }
90
-
91
102
  return false
92
103
  }
93
104
 
@@ -10,7 +10,7 @@ module.exports = function extractSensitiveRanges (evidence) {
10
10
  pattern.lastIndex = 0
11
11
 
12
12
  const regexResult = pattern.exec(evidence.value)
13
- if (regexResult && regexResult.length > 1) {
13
+ if (regexResult?.length > 1) {
14
14
  const start = regexResult.index + (regexResult[0].length - regexResult[1].length)
15
15
  const end = start + regexResult[1].length
16
16
  return [{ start, end }]
@@ -3,6 +3,7 @@
3
3
 
4
4
  const log = require('../../../../log')
5
5
  const vulnerabilities = require('../../vulnerabilities')
6
+ const defaults = require('../../../../config/defaults')
6
7
 
7
8
  const { contains, intersects, remove } = require('./range-utils')
8
9
 
@@ -14,14 +15,12 @@ const sqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyz
14
15
  const taintedRangeBasedSensitiveAnalyzer = require('./sensitive-analyzers/tainted-range-based-sensitive-analyzer')
15
16
  const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer')
16
17
 
17
- const { DEFAULT_IAST_REDACTION_NAME_PATTERN, DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./sensitive-regex')
18
-
19
18
  const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
20
19
 
21
20
  class SensitiveHandler {
22
21
  constructor () {
23
- this._namePattern = new RegExp(DEFAULT_IAST_REDACTION_NAME_PATTERN, 'gmi')
24
- this._valuePattern = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
22
+ this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
23
+ this._valuePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionValuePattern']), 'gmi')
25
24
 
26
25
  this._sensitiveAnalyzers = new Map()
27
26
  this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const crypto = require('crypto')
4
- const { DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./evidence-redaction/sensitive-regex')
4
+
5
+ const defaults = require('../../../config/defaults')
5
6
 
6
7
  const STRINGIFY_RANGE_KEY = 'DD_' + crypto.randomBytes(20).toString('hex')
7
8
  const STRINGIFY_SENSITIVE_KEY = STRINGIFY_RANGE_KEY + 'SENSITIVE'
@@ -11,7 +12,7 @@ const STRINGIFY_SENSITIVE_NOT_STRING_KEY = STRINGIFY_SENSITIVE_KEY + 'NOTSTRING'
11
12
  const KEYS_REGEX_WITH_SENSITIVE_RANGES = new RegExp(String.raw`(?:"(${STRINGIFY_RANGE_KEY}_\d+_))|(?:"(${STRINGIFY_SENSITIVE_KEY}_\d+_(\d+)_))|("${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_\d+_([\s0-9.a-zA-Z]*)")`, 'gm')
12
13
  const KEYS_REGEX_WITHOUT_SENSITIVE_RANGES = new RegExp(String.raw`"(${STRINGIFY_RANGE_KEY}_\d+_)`, 'gm')
13
14
 
14
- const sensitiveValueRegex = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
15
+ const sensitiveValueRegex = new RegExp(/** @type {string} */ (defaults['iast.redactionValuePattern']), 'gmi')
15
16
 
16
17
  function iterateObject (target, fn, levelKeys = [], depth = 10, visited = new Set()) {
17
18
  for (const key of Object.keys(target)) {
@@ -58,7 +58,6 @@ function buildMetadata () {
58
58
  WEBSITE_SITE_NAME,
59
59
  } = getEnvironmentVariables()
60
60
 
61
- const DD_AAS_DOTNET_EXTENSION_VERSION = getValueFromEnvSources('DD_AAS_DOTNET_EXTENSION_VERSION')
62
61
  const DD_AZURE_RESOURCE_GROUP = getValueFromEnvSources('DD_AZURE_RESOURCE_GROUP')
63
62
 
64
63
  const subscriptionID = extractSubscriptionID(WEBSITE_OWNER_NAME)
@@ -77,7 +76,6 @@ function buildMetadata () {
77
76
  : (WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME))
78
77
 
79
78
  return trimObject({
80
- extensionVersion: DD_AAS_DOTNET_EXTENSION_VERSION,
81
79
  functionRuntimeVersion: FUNCTIONS_EXTENSION_VERSION,
82
80
  instanceID: WEBSITE_INSTANCE_ID,
83
81
  instanceName: COMPUTERNAME,
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const request = require('../../exporters/common/request')
3
+ const request = require('../requests/request')
4
4
  const id = require('../../id')
5
5
  const log = require('../../log')
6
6
  const { getValueFromEnvSources } = require('../../config/helper')
@@ -206,6 +206,7 @@ class CiVisibilityExporter extends BufferingExporter {
206
206
  requireGit,
207
207
  isEarlyFlakeDetectionEnabled,
208
208
  earlyFlakeDetectionNumRetries,
209
+ earlyFlakeDetectionSlowTestRetries,
209
210
  earlyFlakeDetectionFaultyThreshold,
210
211
  isFlakyTestRetriesEnabled,
211
212
  isDiEnabled,
@@ -222,6 +223,7 @@ class CiVisibilityExporter extends BufferingExporter {
222
223
  requireGit,
223
224
  isEarlyFlakeDetectionEnabled: isEarlyFlakeDetectionEnabled && this._config.isEarlyFlakeDetectionEnabled,
224
225
  earlyFlakeDetectionNumRetries,
226
+ earlyFlakeDetectionSlowTestRetries,
225
227
  earlyFlakeDetectionFaultyThreshold,
226
228
  isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && this._config.isFlakyTestRetriesEnabled,
227
229
  flakyTestRetriesCount: this._config.flakyTestRetriesCount,
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const request = require('../../exporters/common/request')
3
+ const request = require('../requests/request')
4
4
  const log = require('../../log')
5
5
  const { getValueFromEnvSources } = require('../../config/helper')
6
6
  const {
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const request = require('../../exporters/common/request')
4
3
  const id = require('../../id')
5
4
  const log = require('../../log')
6
5
  const { getValueFromEnvSources } = require('../../config/helper')
@@ -13,8 +12,10 @@ const {
13
12
  TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE,
14
13
  } = require('../telemetry')
15
14
  const { writeSettingsToCache } = require('../test-optimization-cache')
15
+ const request = require('./request')
16
16
 
17
17
  const DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES = 2
18
+ const DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES = { '5s': 10, '10s': 5, '30s': 3, '5m': 2 }
18
19
  const DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD = 30
19
20
 
20
21
  function getLibraryConfiguration ({
@@ -115,6 +116,8 @@ function getLibraryConfiguration ({
115
116
  isEarlyFlakeDetectionEnabled: isKnownTestsEnabled && (earlyFlakeDetectionConfig?.enabled ?? false),
116
117
  earlyFlakeDetectionNumRetries:
117
118
  earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] || DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES,
119
+ earlyFlakeDetectionSlowTestRetries:
120
+ earlyFlakeDetectionConfig?.slow_test_retries ?? DEFAULT_EARLY_FLAKE_DETECTION_SLOW_TEST_RETRIES,
118
121
  earlyFlakeDetectionFaultyThreshold:
119
122
  earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD,
120
123
  isFlakyTestRetriesEnabled,
@@ -0,0 +1,236 @@
1
+ 'use strict'
2
+
3
+ const http = require('http')
4
+ const https = require('https')
5
+ const zlib = require('zlib')
6
+
7
+ const { storage } = require('../../../../datadog-core')
8
+ const log = require('../../log')
9
+ const { httpAgent, httpsAgent } = require('../../exporters/common/agents')
10
+ const { urlToHttpOptions } = require('../../exporters/common/url-to-http-options-polyfill')
11
+
12
+ const RATE_LIMIT_MAX_WAIT_MS = 30_000
13
+ const RETRY_BASE_MS = 5000
14
+ const RETRY_JITTER_MS = 2500
15
+
16
+ /**
17
+ * Calculates retry delay with jitter to prevent thundering herd.
18
+ * Delay is RETRY_BASE_MS + random(0, RETRY_JITTER_MS) (e.g. 5–7.5 seconds).
19
+ *
20
+ * @returns {number} Delay in milliseconds
21
+ */
22
+ function getRetryDelay () {
23
+ return RETRY_BASE_MS + (Math.random() * RETRY_JITTER_MS)
24
+ }
25
+
26
+ /**
27
+ * Determines if a network error is retriable (transient failures only).
28
+ * ECONNREFUSED is retried because it can be transient (service starting up,
29
+ * restarts, rolling deploys, k8s pod/readiness transitions). ENOTFOUND is
30
+ * excluded as it indicates DNS failure or wrong host and is usually not transient.
31
+ *
32
+ * @param {Error} err - The error to check
33
+ * @returns {boolean}
34
+ */
35
+ function isRetriableNetworkError (err) {
36
+ if (!err.code) return false
37
+ return err.code === 'ECONNREFUSED' ||
38
+ err.code === 'ECONNRESET' ||
39
+ err.code === 'ETIMEDOUT' ||
40
+ err.code === 'EPIPE'
41
+ }
42
+
43
+ function parseUrl (urlObjOrString) {
44
+ if (urlObjOrString !== null && typeof urlObjOrString === 'object') {
45
+ return urlToHttpOptions(urlObjOrString)
46
+ }
47
+
48
+ const url = urlToHttpOptions(new URL(urlObjOrString))
49
+
50
+ if (url.protocol === 'unix:' && url.hostname === '.') {
51
+ const udsPath = urlObjOrString.slice(5)
52
+ url.path = udsPath
53
+ url.pathname = udsPath
54
+ }
55
+
56
+ return url
57
+ }
58
+
59
+ /**
60
+ * Simplified HTTP request for test optimization (library config). Uses common HTTP agents.
61
+ * Retries: 429 (with X-ratelimit-reset, max 30s wait),
62
+ * >=500 and transient network errors (5–7.5s delay with jitter). Max one retry.
63
+ * Destroys connections on errors to prevent reuse of bad connections. Preserves
64
+ * original status code across retries for telemetry.
65
+ *
66
+ * @param {string} data - Request body (e.g. JSON string)
67
+ * @param {object} options - { url, path?, method?, headers?, timeout? } (may be mutated)
68
+ * @param {Function} callback - (err, res, statusCode) => void
69
+ */
70
+ function request (data, options, callback) {
71
+ const headers = options.headers ? { ...options.headers } : {}
72
+ headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
73
+
74
+ const opts = { ...options, method: 'POST', headers }
75
+
76
+ if (opts.url) {
77
+ const url = parseUrl(opts.url)
78
+ if (url.protocol === 'unix:') {
79
+ opts.socketPath = url.pathname
80
+ } else {
81
+ opts.path = opts.path ?? url.path
82
+ opts.protocol = url.protocol
83
+ opts.hostname = url.hostname
84
+ opts.port = url.port
85
+ }
86
+ }
87
+
88
+ const timeout = opts.timeout || 2000
89
+ const isSecure = opts.protocol === 'https:'
90
+ const client = isSecure ? https : http
91
+
92
+ if (!opts.socketPath) {
93
+ opts.agent = isSecure ? httpsAgent : httpAgent
94
+ }
95
+
96
+ let hasRetried = false
97
+ let firstStatusCode = null
98
+
99
+ const makeRequest = () => {
100
+ storage('legacy').run({ noop: true }, () => {
101
+ const req = client.request(opts, (res) => {
102
+ // Capture non-2xx status code as soon as we see it so telemetry preserves it if the retry
103
+ // fails with a network error (no HTTP response) before 'end' fires
104
+ if (res.statusCode >= 400 && firstStatusCode === null) {
105
+ firstStatusCode = res.statusCode
106
+ }
107
+
108
+ const chunks = []
109
+
110
+ res.setTimeout(timeout)
111
+
112
+ res.on('data', chunk => {
113
+ chunks.push(chunk)
114
+ })
115
+
116
+ res.once('end', () => {
117
+ const buffer = Buffer.concat(chunks)
118
+
119
+ if (res.statusCode >= 200 && res.statusCode <= 299) {
120
+ const isGzip = res.headers['content-encoding'] === 'gzip'
121
+ if (isGzip) {
122
+ zlib.gunzip(buffer, (err, result) => {
123
+ if (err) {
124
+ log.error('Could not gunzip response: %s', err.message)
125
+ callback(null, '', res.statusCode)
126
+ } else {
127
+ callback(null, result.toString(), res.statusCode)
128
+ }
129
+ })
130
+ } else {
131
+ callback(null, buffer.toString(), res.statusCode)
132
+ }
133
+ return
134
+ }
135
+
136
+ if (res.statusCode === 429 && !hasRetried) {
137
+ const resetHeader = res.headers['x-ratelimit-reset']
138
+ const resetTs = (resetHeader === null || resetHeader === undefined)
139
+ ? Number.NaN
140
+ : Number.parseInt(resetHeader, 10)
141
+ const waitMs = Number.isFinite(resetTs) ? Math.max(0, resetTs * 1000 - Date.now()) : Number.NaN
142
+
143
+ if (Number.isFinite(waitMs) && waitMs <= RATE_LIMIT_MAX_WAIT_MS) {
144
+ hasRetried = true
145
+ setTimeout(makeRequest, waitMs)
146
+ return
147
+ }
148
+
149
+ if (!Number.isFinite(waitMs) || waitMs > RATE_LIMIT_MAX_WAIT_MS) {
150
+ log.debug('Rate limited (429): drop payload (wait %sms > %sms or invalid header)',
151
+ Number.isFinite(waitMs) ? waitMs : 'N/A', RATE_LIMIT_MAX_WAIT_MS)
152
+ }
153
+ } else if (res.statusCode >= 500 && !hasRetried) {
154
+ try {
155
+ if (req.socket) req.socket.destroy()
156
+ } catch {
157
+ // ignore
158
+ }
159
+ hasRetried = true
160
+ setTimeout(makeRequest, getRetryDelay())
161
+ return
162
+ }
163
+
164
+ const error = buildError(res, buffer, opts)
165
+ // Use original status code if this is a failed retry
166
+ callback(error, null, firstStatusCode === null ? res.statusCode : firstStatusCode)
167
+ })
168
+ })
169
+
170
+ req.once('error', err => {
171
+ try {
172
+ if (req.socket) req.socket.destroy()
173
+ } catch {
174
+ // ignore
175
+ }
176
+
177
+ // Retry on retriable network errors
178
+ if (!hasRetried && isRetriableNetworkError(err)) {
179
+ hasRetried = true
180
+ setTimeout(makeRequest, getRetryDelay())
181
+ return
182
+ }
183
+
184
+ // Pass original status code (if any) for accurate telemetry
185
+ callback(err, null, firstStatusCode)
186
+ })
187
+
188
+ req.setTimeout(timeout, () => {
189
+ try {
190
+ if (typeof req.abort === 'function') {
191
+ req.abort()
192
+ } else {
193
+ req.destroy()
194
+ }
195
+ } catch {
196
+ // ignore
197
+ }
198
+ })
199
+
200
+ req.write(data, 'utf8')
201
+ req.end()
202
+ })
203
+ }
204
+
205
+ makeRequest()
206
+ }
207
+
208
+ /**
209
+ * @param {object} res - IncomingMessage
210
+ * @param {Buffer} buffer - Response body
211
+ * @param {object} options - Request options
212
+ * @returns {Error}
213
+ */
214
+ function buildError (res, buffer, options) {
215
+ let errorMessage = ''
216
+ try {
217
+ const fullUrl = new URL(
218
+ options.path,
219
+ options.url || options.hostname || `http://localhost:${options.port}`
220
+ ).href
221
+ errorMessage = `Error from ${fullUrl}: ${res.statusCode} ${http.STATUS_CODES[res.statusCode]}.`
222
+ } catch {
223
+ // ignore
224
+ }
225
+
226
+ const responseData = buffer.toString()
227
+ if (responseData) {
228
+ errorMessage += ` Response from the endpoint: "${responseData}"`
229
+ }
230
+
231
+ const error = new log.NoTransmitError(errorMessage)
232
+ error.status = res.statusCode
233
+ return error
234
+ }
235
+
236
+ module.exports = request
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const request = require('../../exporters/common/request')
3
+ const request = require('../requests/request')
4
4
  const id = require('../../id')
5
5
  const { getValueFromEnvSources } = require('../../config/helper')
6
6
  const log = require('../../log')