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.
- package/LICENSE-3rdparty.csv +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +225 -4
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
- package/packages/datadog-instrumentations/src/jest.js +76 -12
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
- package/packages/datadog-plugin-cypress/src/support.js +48 -8
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- package/packages/dd-trace/src/azure_metadata.js +0 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -197
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +36 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +14 -1
- package/packages/dd-trace/src/encode/agentless-json.js +141 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +15 -2
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- 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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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) {
|
|
@@ -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
|
|
15
|
+
} else if (args.options?.host) {
|
|
16
16
|
this.analyze(args.options.host)
|
|
17
17
|
}
|
|
18
18
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../../../datadog-core')
|
|
4
|
-
const {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
24
|
-
this._valuePattern = new RegExp(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
@@ -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,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
|