dd-trace 5.109.0 → 5.111.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/README.md +13 -8
- package/ci/init.js +25 -5
- package/ci/vitest-no-worker-init-setup.mjs +430 -0
- package/ext/tags.js +2 -0
- package/index.d.ts +79 -24
- package/initialize.mjs +5 -6
- package/loader-hook.mjs +154 -47
- package/package.json +20 -16
- package/packages/datadog-esbuild/index.js +26 -0
- package/packages/datadog-esbuild/src/utils.js +46 -3
- package/packages/datadog-instrumentations/src/connect.js +4 -3
- package/packages/datadog-instrumentations/src/fastify.js +4 -12
- package/packages/datadog-instrumentations/src/fs.js +8 -6
- package/packages/datadog-instrumentations/src/graphql.js +26 -484
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
- package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
- package/packages/datadog-instrumentations/src/koa.js +3 -2
- package/packages/datadog-instrumentations/src/mariadb.js +4 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +14 -2
- package/packages/datadog-instrumentations/src/next.js +10 -10
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/restify.js +4 -3
- package/packages/datadog-instrumentations/src/router.js +18 -5
- package/packages/datadog-instrumentations/src/selenium.js +1 -2
- package/packages/datadog-instrumentations/src/tedious.js +28 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
- package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -1598
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
- package/packages/datadog-plugin-aws-sdk/src/base.js +13 -9
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +83 -91
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -5
- package/packages/datadog-plugin-cypress/src/support.js +45 -1
- package/packages/datadog-plugin-express/src/code_origin.js +1 -1
- package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +627 -11
- package/packages/datadog-plugin-graphql/src/index.js +20 -8
- package/packages/datadog-plugin-graphql/src/parse.js +24 -4
- package/packages/datadog-plugin-graphql/src/validate.js +16 -5
- package/packages/datadog-plugin-http/src/client.js +8 -3
- package/packages/datadog-plugin-http/src/index.js +6 -8
- package/packages/datadog-plugin-http2/src/client.js +5 -2
- package/packages/datadog-plugin-jest/src/index.js +1 -1
- package/packages/datadog-plugin-mocha/src/index.js +1 -1
- package/packages/datadog-plugin-mysql/src/index.js +7 -2
- package/packages/datadog-plugin-openai/src/services.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-undici/src/index.js +5 -2
- package/packages/datadog-plugin-vitest/src/index.js +76 -64
- package/packages/datadog-shimmer/src/shimmer.js +37 -16
- package/packages/datadog-webpack/index.js +17 -1
- package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
- package/packages/dd-trace/src/aiguard/sdk.js +15 -3
- package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
- package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
- package/packages/dd-trace/src/appsec/channels.js +3 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
- package/packages/dd-trace/src/appsec/graphql.js +5 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
- package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
- package/packages/dd-trace/src/appsec/index.js +20 -7
- package/packages/dd-trace/src/appsec/lambda.js +135 -0
- package/packages/dd-trace/src/appsec/reporter.js +50 -10
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +27 -1
- package/packages/dd-trace/src/appsec/waf/index.js +16 -4
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
- package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
- package/packages/dd-trace/src/config/defaults.js +25 -5
- package/packages/dd-trace/src/config/generated-config-types.d.ts +596 -58
- package/packages/dd-trace/src/config/helper.js +134 -33
- package/packages/dd-trace/src/config/index.js +39 -23
- package/packages/dd-trace/src/config/major-overrides.js +4 -2
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +109 -57
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/processor.js +11 -3
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -35
- package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
- package/packages/dd-trace/src/debugger/index.js +5 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
- package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
- package/packages/dd-trace/src/exporters/agent/index.js +1 -1
- package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/request.js +1 -20
- package/packages/dd-trace/src/exporters/common/url.js +34 -0
- package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
- package/packages/dd-trace/src/guardrails/index.js +8 -5
- package/packages/dd-trace/src/index.js +6 -4
- package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
- package/packages/dd-trace/src/llmobs/index.js +17 -6
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +8 -6
- package/packages/dd-trace/src/llmobs/span_processor.js +5 -1
- package/packages/dd-trace/src/llmobs/tagger.js +58 -3
- package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
- package/packages/dd-trace/src/log/index.js +18 -20
- package/packages/dd-trace/src/openfeature/flagging_provider.js +2 -7
- package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
- package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
- package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
- package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +44 -38
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
- package/packages/dd-trace/src/opentelemetry/span.js +14 -3
- package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
- package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
- package/packages/dd-trace/src/opentracing/span_context.js +17 -0
- package/packages/dd-trace/src/opentracing/tracer.js +6 -1
- package/packages/dd-trace/src/plugin_manager.js +14 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +21 -4
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
- package/packages/dd-trace/src/plugins/util/llm.js +6 -1
- package/packages/dd-trace/src/plugins/util/test.js +91 -9
- package/packages/dd-trace/src/plugins/util/url.js +68 -1
- package/packages/dd-trace/src/plugins/util/web.js +19 -36
- package/packages/dd-trace/src/priority_sampler.js +1 -13
- package/packages/dd-trace/src/profiling/config.js +129 -198
- package/packages/dd-trace/src/profiling/constants.js +4 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
- package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
- package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
- package/packages/dd-trace/src/profiling/oom.js +71 -0
- package/packages/dd-trace/src/profiling/profiler.js +35 -23
- package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
- package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
- package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
- package/packages/dd-trace/src/proxy.js +8 -8
- package/packages/dd-trace/src/ritm.js +4 -2
- package/packages/dd-trace/src/serverless.js +1 -2
- package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
- package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
- package/packages/dd-trace/src/span_format.js +2 -1
- package/packages/dd-trace/src/span_processor.js +11 -6
- package/packages/dd-trace/src/span_stats.js +96 -78
- package/packages/dd-trace/src/startup-log.js +2 -1
- package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
- package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
- package/packages/dd-trace/src/telemetry/index.js +2 -2
- package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
- package/packages/dd-trace/src/telemetry/send-data.js +20 -18
- package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
- package/packages/dd-trace/src/tracer.js +15 -1
- package/packages/dd-trace/src/util.js +14 -0
- package/register.js +58 -1
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
- package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
finishAllTraceSpans,
|
|
10
10
|
getTestSuitePath,
|
|
11
11
|
getTestSuiteCommonTags,
|
|
12
|
+
getTestLevelsMetadataTags,
|
|
12
13
|
getTestSessionName,
|
|
13
14
|
getIsFaultyEarlyFlakeDetection,
|
|
14
15
|
TEST_SOURCE_FILE,
|
|
@@ -16,6 +17,7 @@ const {
|
|
|
16
17
|
TEST_CODE_COVERAGE_LINES_PCT,
|
|
17
18
|
TEST_CODE_OWNERS,
|
|
18
19
|
TEST_COMMAND,
|
|
20
|
+
TEST_LEVELS_METADATA,
|
|
19
21
|
TEST_SESSION_NAME,
|
|
20
22
|
TEST_SOURCE_START,
|
|
21
23
|
TEST_IS_NEW,
|
|
@@ -28,12 +30,12 @@ const {
|
|
|
28
30
|
TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
|
|
29
31
|
TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
|
|
30
32
|
TEST_HAS_FAILED_ALL_RETRIES,
|
|
31
|
-
getLibraryCapabilitiesTags,
|
|
33
|
+
getLibraryCapabilitiesTags: getDefaultLibraryCapabilitiesTags,
|
|
32
34
|
TEST_RETRY_REASON_TYPES,
|
|
33
|
-
isModifiedTest,
|
|
34
35
|
TEST_IS_MODIFIED,
|
|
35
36
|
TEST_HAS_DYNAMIC_NAME,
|
|
36
37
|
TEST_FINAL_STATUS,
|
|
38
|
+
TEST_IS_TEST_FRAMEWORK_WORKER,
|
|
37
39
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
38
40
|
const { COMPONENT } = require('../../dd-trace/src/constants')
|
|
39
41
|
const {
|
|
@@ -68,49 +70,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
68
70
|
})
|
|
69
71
|
})
|
|
70
72
|
|
|
71
|
-
this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
|
|
72
|
-
// if for whatever reason the worker does not receive valid known tests, we don't consider it as new
|
|
73
|
-
if (!knownTests.vitest) {
|
|
74
|
-
return onDone(false)
|
|
75
|
-
}
|
|
76
|
-
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
77
|
-
const testsForThisTestSuite = knownTests.vitest[testSuite] || []
|
|
78
|
-
onDone(!testsForThisTestSuite.includes(testName))
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
this.addSub('ci:vitest:test:is-attempt-to-fix', ({
|
|
82
|
-
testManagementTests,
|
|
83
|
-
testSuiteAbsolutePath,
|
|
84
|
-
testName,
|
|
85
|
-
onDone,
|
|
86
|
-
}) => {
|
|
87
|
-
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
88
|
-
const { isAttemptToFix } = this.getTestProperties(testManagementTests, testSuite, testName)
|
|
89
|
-
|
|
90
|
-
onDone(isAttemptToFix ?? false)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
this.addSub('ci:vitest:test:is-disabled', ({ testManagementTests, testSuiteAbsolutePath, testName, onDone }) => {
|
|
94
|
-
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
95
|
-
const { isDisabled } = this.getTestProperties(testManagementTests, testSuite, testName)
|
|
96
|
-
|
|
97
|
-
onDone(isDisabled)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
this.addSub('ci:vitest:test:is-quarantined', ({ testManagementTests, testSuiteAbsolutePath, testName, onDone }) => {
|
|
101
|
-
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
102
|
-
const { isQuarantined } = this.getTestProperties(testManagementTests, testSuite, testName)
|
|
103
|
-
|
|
104
|
-
onDone(isQuarantined)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
this.addSub('ci:vitest:test:is-modified', ({ modifiedFiles, testSuiteAbsolutePath, onDone }) => {
|
|
108
|
-
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
109
|
-
const isModified = isModifiedTest(testSuite, 0, 0, modifiedFiles, this.constructor.id)
|
|
110
|
-
|
|
111
|
-
onDone(isModified)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
73
|
this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
|
|
115
74
|
knownTests,
|
|
116
75
|
testFilepaths,
|
|
@@ -139,12 +98,16 @@ class VitestPlugin extends CiPlugin {
|
|
|
139
98
|
isRetryReasonAttemptToFix,
|
|
140
99
|
isRetryReasonAtr,
|
|
141
100
|
isModified,
|
|
101
|
+
isTestFrameworkWorker,
|
|
102
|
+
requestErrorTags,
|
|
142
103
|
} = ctx
|
|
143
104
|
|
|
144
105
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
145
|
-
const store = storage('legacy').getStore()
|
|
106
|
+
const store = ctx.currentStore || storage('legacy').getStore()
|
|
107
|
+
const testSuiteSpan = store?.testSuiteSpan || this.testSuiteSpan
|
|
146
108
|
|
|
147
109
|
const extraTags = {
|
|
110
|
+
...requestErrorTags,
|
|
148
111
|
[TEST_SOURCE_FILE]: testSuite,
|
|
149
112
|
}
|
|
150
113
|
if (isRetry) {
|
|
@@ -177,11 +140,14 @@ class VitestPlugin extends CiPlugin {
|
|
|
177
140
|
if (isModified) {
|
|
178
141
|
extraTags[TEST_IS_MODIFIED] = 'true'
|
|
179
142
|
}
|
|
143
|
+
if (isTestFrameworkWorker) {
|
|
144
|
+
extraTags[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
|
|
145
|
+
}
|
|
180
146
|
|
|
181
147
|
const span = this.startTestSpan(
|
|
182
148
|
testName,
|
|
183
149
|
testSuite,
|
|
184
|
-
|
|
150
|
+
testSuiteSpan,
|
|
185
151
|
extraTags
|
|
186
152
|
)
|
|
187
153
|
|
|
@@ -202,7 +168,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
202
168
|
})
|
|
203
169
|
|
|
204
170
|
this.addBind('ci:vitest:test:finish-time', (ctx) => {
|
|
205
|
-
const { status, task, attemptToFixPassed, attemptToFixFailed } = ctx
|
|
171
|
+
const { status, task, attemptToFixPassed, attemptToFixFailed, duration } = ctx
|
|
206
172
|
const span = ctx.currentStore?.span
|
|
207
173
|
|
|
208
174
|
// we store the finish time to finish at a later hook
|
|
@@ -216,7 +182,8 @@ class VitestPlugin extends CiPlugin {
|
|
|
216
182
|
span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'false')
|
|
217
183
|
}
|
|
218
184
|
|
|
219
|
-
|
|
185
|
+
const finishTime = typeof duration === 'number' ? span._startTime + duration : span._getTime()
|
|
186
|
+
this.taskToFinishTime.set(task, finishTime)
|
|
220
187
|
|
|
221
188
|
ctx.parentStore = ctx.currentStore
|
|
222
189
|
ctx.currentStore = { ...ctx.currentStore, span }
|
|
@@ -289,19 +256,29 @@ class VitestPlugin extends CiPlugin {
|
|
|
289
256
|
finishAllTraceSpans(span)
|
|
290
257
|
})
|
|
291
258
|
|
|
292
|
-
this.addSub('ci:vitest:test:skip', ({
|
|
259
|
+
this.addSub('ci:vitest:test:skip', ({
|
|
260
|
+
testName,
|
|
261
|
+
testSuiteAbsolutePath,
|
|
262
|
+
isNew,
|
|
263
|
+
isDisabled,
|
|
264
|
+
isTestFrameworkWorker,
|
|
265
|
+
requestErrorTags,
|
|
266
|
+
testSuiteSpan,
|
|
267
|
+
}) => {
|
|
293
268
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
294
269
|
const testSpan = this.startTestSpan(
|
|
295
270
|
testName,
|
|
296
271
|
testSuite,
|
|
297
|
-
this.testSuiteSpan,
|
|
272
|
+
testSuiteSpan || this.testSuiteSpan,
|
|
298
273
|
{
|
|
274
|
+
...requestErrorTags,
|
|
299
275
|
[TEST_SOURCE_FILE]: testSuite,
|
|
300
276
|
[TEST_SOURCE_START]: 1, // we can't get the proper start line in vitest
|
|
301
277
|
[TEST_STATUS]: 'skip',
|
|
302
278
|
[TEST_FINAL_STATUS]: 'skip',
|
|
303
279
|
...(isDisabled ? { [TEST_MANAGEMENT_IS_DISABLED]: 'true' } : {}),
|
|
304
280
|
...(isNew ? { [TEST_IS_NEW]: 'true' } : {}),
|
|
281
|
+
...(isTestFrameworkWorker ? { [TEST_IS_TEST_FRAMEWORK_WORKER]: 'true' } : {}),
|
|
305
282
|
}
|
|
306
283
|
)
|
|
307
284
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', this.getTestTelemetryTags(testSpan))
|
|
@@ -309,7 +286,15 @@ class VitestPlugin extends CiPlugin {
|
|
|
309
286
|
})
|
|
310
287
|
|
|
311
288
|
this.addBind('ci:vitest:test-suite:start', (ctx) => {
|
|
312
|
-
const {
|
|
289
|
+
const {
|
|
290
|
+
codeOwnersEntries,
|
|
291
|
+
repositoryRoot,
|
|
292
|
+
requestErrorTags,
|
|
293
|
+
testSuiteAbsolutePath,
|
|
294
|
+
frameworkVersion,
|
|
295
|
+
isTestFrameworkWorker,
|
|
296
|
+
isVitestNoWorkerInitActive,
|
|
297
|
+
} = ctx
|
|
313
298
|
|
|
314
299
|
const testCommand = ctx.testCommand || 'vitest run'
|
|
315
300
|
const { testSessionId, testModuleId } = ctx
|
|
@@ -327,9 +312,16 @@ class VitestPlugin extends CiPlugin {
|
|
|
327
312
|
// test suites run in a different process, so they also need to init the metadata dictionary
|
|
328
313
|
const testSessionName = getTestSessionName(this.config, trimmedCommand, this.testEnvironmentMetadata)
|
|
329
314
|
if (this.tracer._exporter.addMetadataTags) {
|
|
315
|
+
const libraryCapabilitiesTags = this.getLibraryCapabilitiesTags(frameworkVersion, {
|
|
316
|
+
isVitestNoWorkerInitActive,
|
|
317
|
+
})
|
|
330
318
|
this.tracer._exporter.addMetadataTags({
|
|
331
|
-
|
|
332
|
-
|
|
319
|
+
[TEST_LEVELS_METADATA]: {
|
|
320
|
+
[TEST_COMMAND]: testCommand,
|
|
321
|
+
[TEST_SESSION_NAME]: testSessionName,
|
|
322
|
+
...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
|
|
323
|
+
},
|
|
324
|
+
test: libraryCapabilitiesTags,
|
|
333
325
|
})
|
|
334
326
|
}
|
|
335
327
|
|
|
@@ -342,9 +334,13 @@ class VitestPlugin extends CiPlugin {
|
|
|
342
334
|
testSuite,
|
|
343
335
|
'vitest'
|
|
344
336
|
),
|
|
337
|
+
...requestErrorTags,
|
|
345
338
|
[TEST_SOURCE_FILE]: testSuite,
|
|
346
339
|
[TEST_SOURCE_START]: 1,
|
|
347
340
|
}
|
|
341
|
+
if (isTestFrameworkWorker) {
|
|
342
|
+
testSuiteMetadata[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
|
|
343
|
+
}
|
|
348
344
|
|
|
349
345
|
const codeOwners = this.getCodeOwners(testSuiteMetadata)
|
|
350
346
|
if (codeOwners) {
|
|
@@ -368,13 +364,17 @@ class VitestPlugin extends CiPlugin {
|
|
|
368
364
|
return ctx.currentStore
|
|
369
365
|
})
|
|
370
366
|
|
|
371
|
-
this.addSub('ci:vitest:test-suite:finish', ({ testSuiteSpan, status, onFinish }) => {
|
|
367
|
+
this.addSub('ci:vitest:test-suite:finish', ({ testSuiteSpan, status, deferFlush, onFinish }) => {
|
|
372
368
|
if (testSuiteSpan) {
|
|
373
369
|
testSuiteSpan.setTag(TEST_STATUS, status)
|
|
374
370
|
testSuiteSpan.finish()
|
|
375
371
|
finishAllTraceSpans(testSuiteSpan)
|
|
376
372
|
}
|
|
377
373
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
|
|
374
|
+
if (deferFlush) {
|
|
375
|
+
onFinish()
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
378
|
this.tracer._exporter.flush(onFinish)
|
|
379
379
|
if (this.runningTestProbe) {
|
|
380
380
|
this.removeDiProbe(this.runningTestProbe)
|
|
@@ -403,9 +403,15 @@ class VitestPlugin extends CiPlugin {
|
|
|
403
403
|
isEarlyFlakeDetectionEnabled,
|
|
404
404
|
isEarlyFlakeDetectionFaulty,
|
|
405
405
|
isTestManagementTestsEnabled,
|
|
406
|
+
requestErrorTags,
|
|
406
407
|
vitestPool,
|
|
408
|
+
isVitestNoWorkerInitActive,
|
|
407
409
|
onFinish,
|
|
408
410
|
}) => {
|
|
411
|
+
for (const [tag, value] of Object.entries(requestErrorTags || {})) {
|
|
412
|
+
this.testSessionSpan.setTag(tag, value)
|
|
413
|
+
this.testModuleSpan.setTag(tag, value)
|
|
414
|
+
}
|
|
409
415
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
410
416
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
411
417
|
if (error) {
|
|
@@ -432,12 +438,12 @@ class VitestPlugin extends CiPlugin {
|
|
|
432
438
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
|
|
433
439
|
this.testSessionSpan.finish()
|
|
434
440
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session', {
|
|
435
|
-
hasFailedTestReplay: this.libraryConfig?.isDiEnabled
|
|
441
|
+
hasFailedTestReplay: this.libraryConfig?.isDiEnabled && !isVitestNoWorkerInitActive ? true : undefined,
|
|
436
442
|
})
|
|
437
443
|
finishAllTraceSpans(this.testSessionSpan)
|
|
438
444
|
this.telemetry.count(TELEMETRY_TEST_SESSION, {
|
|
439
445
|
provider: this.ciProviderName,
|
|
440
|
-
autoInjected: !!this._tracerConfig.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
|
|
446
|
+
autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
|
|
441
447
|
})
|
|
442
448
|
this.tracer._exporter.flush(onFinish)
|
|
443
449
|
})
|
|
@@ -447,6 +453,19 @@ class VitestPlugin extends CiPlugin {
|
|
|
447
453
|
})
|
|
448
454
|
}
|
|
449
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Returns Vitest library capability metadata tags.
|
|
458
|
+
* @param {string} frameworkVersion - The Vitest version.
|
|
459
|
+
* @param {object} [ctx] - Diagnostic channel context.
|
|
460
|
+
* @param {boolean} [ctx.isVitestNoWorkerInitActive] - Whether no-worker init is active for this run.
|
|
461
|
+
* @returns {Record<string, string|undefined>}
|
|
462
|
+
*/
|
|
463
|
+
getLibraryCapabilitiesTags (frameworkVersion, ctx = {}) {
|
|
464
|
+
return getDefaultLibraryCapabilitiesTags(this.constructor.id, frameworkVersion, {
|
|
465
|
+
omitFailedTestReplay: ctx.isVitestNoWorkerInitActive,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
450
469
|
/**
|
|
451
470
|
* Handles the coverage report by discovering and uploading it if enabled.
|
|
452
471
|
* @param {string} rootDir - The root directory where coverage reports are located.
|
|
@@ -463,13 +482,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
463
482
|
onDone,
|
|
464
483
|
})
|
|
465
484
|
}
|
|
466
|
-
|
|
467
|
-
getTestProperties (testManagementTests, testSuite, testName) {
|
|
468
|
-
const { attempt_to_fix: isAttemptToFix, disabled: isDisabled, quarantined: isQuarantined } =
|
|
469
|
-
testManagementTests?.vitest?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
|
|
470
|
-
|
|
471
|
-
return { isAttemptToFix, isDisabled, isQuarantined }
|
|
472
|
-
}
|
|
473
485
|
}
|
|
474
486
|
|
|
475
487
|
module.exports = VitestPlugin
|
|
@@ -119,11 +119,14 @@ function wrapCallback (original, wrapper) {
|
|
|
119
119
|
* object.
|
|
120
120
|
* @param {string | symbol} name - The property key of the method to wrap.
|
|
121
121
|
* @param {(original: Function) => (...args: unknown[]) => unknown} wrapper - The wrapper function.
|
|
122
|
-
* @param {{ replaceGetter?: boolean }} [options] -
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
122
|
+
* @param {{ replaceGetter?: boolean }} [options] - By default the getter is
|
|
123
|
+
* wrapped in place, so each property access runs the wrapper. A getter+setter
|
|
124
|
+
* pair keeps its setter; a setter-only property throws. If `replaceGetter` is
|
|
125
|
+
* true, the getter is instead accessed once and replaced with one returning the
|
|
126
|
+
* resolved wrapped value — for a lazy getter+setter pair (e.g. Node 20's
|
|
127
|
+
* `fs.opendir`) the setter is rebuilt to materialize a writable data property on
|
|
128
|
+
* assignment, keeping the descriptor observationally identical for downstream
|
|
129
|
+
* consumers. Use with care! This may only be done when the getter has no side effect.
|
|
127
130
|
* @returns {Record<string | symbol, unknown> | Function | undefined} The target object with
|
|
128
131
|
* the wrapped method.
|
|
129
132
|
*/
|
|
@@ -151,17 +154,13 @@ function wrap (target, name, wrapper, options) {
|
|
|
151
154
|
enumerable: false,
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// this is not a common use case.
|
|
159
|
-
throw new Error(options?.replaceGetter
|
|
160
|
-
? 'Replacing a getter/setter pair is not supported. Implement if required.'
|
|
161
|
-
: 'Replacing setters is not supported. Implement if required.')
|
|
157
|
+
// A setter-only property has nothing to wrap. Instrumenting the setter is not
|
|
158
|
+
// implemented; a getter+setter pair is handled below.
|
|
159
|
+
if (descriptor.set && !descriptor.get) {
|
|
160
|
+
throw new Error('Replacing setters is not supported. Implement if required.')
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
const original = descriptor.value ?? options?.replaceGetter ? target[name] : descriptor.get
|
|
163
|
+
const original = (descriptor.value ?? options?.replaceGetter) ? target[name] : descriptor.get
|
|
165
164
|
|
|
166
165
|
assertMethod(target, name, original)
|
|
167
166
|
|
|
@@ -176,8 +175,30 @@ function wrap (target, name, wrapper, options) {
|
|
|
176
175
|
}
|
|
177
176
|
descriptor.value = wrapped
|
|
178
177
|
} else {
|
|
179
|
-
if (descriptor.
|
|
180
|
-
//
|
|
178
|
+
if (descriptor.set && options?.replaceGetter) {
|
|
179
|
+
// A lazy accessor pair (e.g. Node 20's `fs.opendir`). `original` already
|
|
180
|
+
// resolved the value through the getter. Keep the property an accessor pair
|
|
181
|
+
// so the shape stays observationally identical — a downstream consumer may
|
|
182
|
+
// read the descriptor or assign to it on a specific Node.js version. The
|
|
183
|
+
// getter returns the wrapped value; the setter mirrors the native lazy
|
|
184
|
+
// contract (assignment self-replaces the property with a writable data
|
|
185
|
+
// property holding the assigned value), so a caller that overwrites the
|
|
186
|
+
// method gets exactly what they set, unwrapped, as before. The descriptor
|
|
187
|
+
// is the original accessor descriptor (no `value`/`writable` slots), so
|
|
188
|
+
// reassigning `get`/`set` keeps it a valid accessor descriptor.
|
|
189
|
+
descriptor.get = () => wrapped
|
|
190
|
+
descriptor.set = function (value) {
|
|
191
|
+
Object.defineProperty(this, name, {
|
|
192
|
+
configurable: descriptor.configurable,
|
|
193
|
+
enumerable: descriptor.enumerable,
|
|
194
|
+
writable: true,
|
|
195
|
+
value,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
} else if (descriptor.get) {
|
|
199
|
+
// Wrap the getter in place; for a getter+setter pair the original setter
|
|
200
|
+
// stays untouched. `replaceGetter` (no side effect on read) instead returns
|
|
201
|
+
// the value resolved once into `wrapped`.
|
|
181
202
|
descriptor.get = options?.replaceGetter ? () => wrapped : wrapped
|
|
182
203
|
} else {
|
|
183
204
|
descriptor.value = wrapped
|
|
@@ -6,6 +6,7 @@ const fs = require('node:fs')
|
|
|
6
6
|
const instrumentations = require('../datadog-instrumentations/src/helpers/instrumentations')
|
|
7
7
|
const extractPackageAndModulePath = require('../datadog-instrumentations/src/helpers/extract-package-and-module-path')
|
|
8
8
|
const hooks = require('../datadog-instrumentations/src/helpers/hooks')
|
|
9
|
+
const { matchesOptionalPeerFile } = require('../datadog-instrumentations/src/helpers/optional-peer-bundler')
|
|
9
10
|
const { isESMFile } = require('../datadog-esbuild/src/utils')
|
|
10
11
|
const log = require('./src/log')
|
|
11
12
|
|
|
@@ -130,11 +131,26 @@ class DatadogWebpackPlugin {
|
|
|
130
131
|
nmf.hooks.afterResolve.tap(PLUGIN_NAME, (resolveData) => {
|
|
131
132
|
const { createData } = resolveData
|
|
132
133
|
const resource = createData?.resource
|
|
133
|
-
if (!resource
|
|
134
|
+
if (!resource) {
|
|
134
135
|
return
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const normalizedResource = resource.replaceAll('\\', '/')
|
|
139
|
+
|
|
140
|
+
// Rewrite optional-peer loads so installed peers get bundled and survive relocation
|
|
141
|
+
// (#8980); absent peers stay opaque, so a build that does not opt into the feature does
|
|
142
|
+
// not follow their dependency chain (#8635).
|
|
143
|
+
if (matchesOptionalPeerFile(normalizedResource)) {
|
|
144
|
+
createData.loaders = createData.loaders || []
|
|
145
|
+
createData.loaders.push({ loader: require.resolve('./src/optional-peer-loader') })
|
|
146
|
+
log.debug('INLINE: optional-peer loader applied to %s', normalizedResource)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!resource.includes('node_modules')) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
const { pkg, path: modulePath, pkgJson } = extractPackageAndModulePath(normalizedResource)
|
|
139
155
|
if (!pkg) {
|
|
140
156
|
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { rewriteOptionalPeerLoads } = require('../../datadog-instrumentations/src/helpers/optional-peer-bundler')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Webpack loader applied to the optional-peer loader files. Rewrites each
|
|
7
|
+
* `requireOptionalPeer('name')` whose peer is installed at build time into a literal
|
|
8
|
+
* `require('name')` so webpack bundles the peer (#8980). Peers that are absent stay opaque, so
|
|
9
|
+
* builds that do not opt into the feature keep the #8635 guarantee.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} source
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function optionalPeerLoader (source) {
|
|
15
|
+
this.cacheable(false)
|
|
16
|
+
return rewriteOptionalPeerLoads(source, this.context)
|
|
17
|
+
}
|
|
@@ -63,6 +63,18 @@ class AIGuardClientError extends Error {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the AI Guard host for a given Datadog site. Sites with a single subdomain level
|
|
68
|
+
* (e.g. `datadoghq.com`, `ddog-gov.com`) are served from the `app.` subdomain, while regional
|
|
69
|
+
* sites (e.g. `us3.datadoghq.com`, `ap1.datadoghq.com`) are used as-is.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} site - Datadog site (e.g. `datadoghq.com`, `us3.datadoghq.com`)
|
|
72
|
+
* @returns {string} The host to use for the AI Guard endpoint
|
|
73
|
+
*/
|
|
74
|
+
function aiGuardHost (site) {
|
|
75
|
+
return site.split('.').length === 2 ? `app.${site}` : site
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
class AIGuard extends NoopAIGuard {
|
|
67
79
|
#initialized
|
|
68
80
|
#tracer
|
|
@@ -81,20 +93,20 @@ class AIGuard extends NoopAIGuard {
|
|
|
81
93
|
constructor (tracer, config) {
|
|
82
94
|
super()
|
|
83
95
|
|
|
84
|
-
if (!config.
|
|
96
|
+
if (!config.DD_API_KEY || !config.DD_APP_KEY) {
|
|
85
97
|
log.error('AIGuard: missing api and/or app keys, use env DD_API_KEY and DD_APP_KEY')
|
|
86
98
|
this.#initialized = false
|
|
87
99
|
return
|
|
88
100
|
}
|
|
89
101
|
this.#tracer = tracer
|
|
90
102
|
this.#headers = {
|
|
91
|
-
'DD-API-KEY': config.
|
|
103
|
+
'DD-API-KEY': config.DD_API_KEY,
|
|
92
104
|
'DD-APPLICATION-KEY': config.DD_APP_KEY,
|
|
93
105
|
'DD-AI-GUARD-VERSION': tracerVersion,
|
|
94
106
|
'DD-AI-GUARD-SOURCE': 'SDK',
|
|
95
107
|
'DD-AI-GUARD-LANGUAGE': 'nodejs',
|
|
96
108
|
}
|
|
97
|
-
const endpoint = config.experimental.aiguard.endpoint || `https
|
|
109
|
+
const endpoint = config.experimental.aiguard.endpoint || `https://${aiGuardHost(config.site)}/api/v2/ai-guard`
|
|
98
110
|
this.#evaluateUrl = `${endpoint}/evaluate`
|
|
99
111
|
this.#timeout = config.experimental.aiguard.timeout
|
|
100
112
|
this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const web = require('../../plugins/util/web')
|
|
4
|
+
const { isSchemaAttribute } = require('../reporter')
|
|
5
|
+
const appsecTelemetry = require('../telemetry')
|
|
6
|
+
const sampler = require('./sampler')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Map a sampling decision into the corresponding API Security telemetry metric.
|
|
10
|
+
*
|
|
11
|
+
* The decision is done by the sampler, here the outcome is translated to emitted metrics:
|
|
12
|
+
* - SAMPLE: request.schema / request.no_schema depending on WAF schema attributes
|
|
13
|
+
* - MISSING_ROUTE: missing_route
|
|
14
|
+
* - SKIP: no metric emitted
|
|
15
|
+
*
|
|
16
|
+
* @param {import('http').IncomingMessage} req
|
|
17
|
+
* @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
|
|
18
|
+
* @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
|
|
19
|
+
*/
|
|
20
|
+
function reportRequest (req, samplingDecision, wafResult) {
|
|
21
|
+
switch (samplingDecision) {
|
|
22
|
+
case sampler.SamplingDecision.SAMPLE: {
|
|
23
|
+
const framework = getFramework(req)
|
|
24
|
+
if (hasSchemaAttributes(wafResult?.attributes)) {
|
|
25
|
+
appsecTelemetry.incrementApiSecRequestSchemaMetric(framework)
|
|
26
|
+
} else {
|
|
27
|
+
appsecTelemetry.incrementApiSecRequestNoSchemaMetric(framework)
|
|
28
|
+
}
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
case sampler.SamplingDecision.MISSING_ROUTE:
|
|
32
|
+
appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(req))
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getFramework (req) {
|
|
38
|
+
return web.root(req)?.context()?.getTag?.('component')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasSchemaAttributes (attributes) {
|
|
42
|
+
if (!attributes) return false
|
|
43
|
+
for (const key of Object.keys(attributes)) {
|
|
44
|
+
if (isSchemaAttribute(key)) return true
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
configure: sampler.configure,
|
|
51
|
+
disable: sampler.disable,
|
|
52
|
+
sampleRequest: sampler.sampleRequest,
|
|
53
|
+
reportRequest,
|
|
54
|
+
SamplingDecision: sampler.SamplingDecision,
|
|
55
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { TTLCache } = require('../../../../../vendor/dist/@isaacs/ttlcache')
|
|
4
|
+
const web = require('../../plugins/util/web')
|
|
5
|
+
const log = require('../../log')
|
|
6
|
+
const { AUTO_REJECT, USER_REJECT } = require('../../../../../ext/priority')
|
|
7
|
+
const { keepTrace } = require('../../priority_sampler')
|
|
8
|
+
const { ASM } = require('../../standalone/product')
|
|
9
|
+
const { isBlocked } = require('../blocking')
|
|
10
|
+
|
|
11
|
+
const MAX_SIZE = 4096
|
|
12
|
+
|
|
13
|
+
const SamplingDecision = Object.freeze({
|
|
14
|
+
SAMPLE: 'sample',
|
|
15
|
+
MISSING_ROUTE: 'missing_route',
|
|
16
|
+
SKIP: 'skip',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let enabled
|
|
20
|
+
let asmStandaloneEnabled
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {TTLCache}
|
|
24
|
+
*/
|
|
25
|
+
let sampledRequests
|
|
26
|
+
|
|
27
|
+
class NoopTTLCache {
|
|
28
|
+
clear () {}
|
|
29
|
+
set (_key, _value) {}
|
|
30
|
+
has (_key) { return false }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configure ({ appsec, apmTracingEnabled }) {
|
|
34
|
+
enabled = appsec.DD_API_SECURITY_ENABLED
|
|
35
|
+
asmStandaloneEnabled = apmTracingEnabled === false
|
|
36
|
+
sampledRequests = appsec.DD_API_SECURITY_SAMPLE_DELAY === 0
|
|
37
|
+
? new NoopTTLCache()
|
|
38
|
+
: new TTLCache({ max: MAX_SIZE, ttl: appsec.DD_API_SECURITY_SAMPLE_DELAY * 1000 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function disable () {
|
|
42
|
+
enabled = false
|
|
43
|
+
sampledRequests?.clear()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {import('http').IncomingMessage} req
|
|
48
|
+
* @param {import('http').ServerResponse} res
|
|
49
|
+
* @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
|
|
50
|
+
* @returns {'sample' | 'missing_route' | 'skip'}
|
|
51
|
+
*/
|
|
52
|
+
function sampleRequest (req, res, record = false) {
|
|
53
|
+
if (!enabled) return SamplingDecision.SKIP
|
|
54
|
+
|
|
55
|
+
const rootSpan = web.root(req)
|
|
56
|
+
if (!rootSpan) return SamplingDecision.SKIP
|
|
57
|
+
|
|
58
|
+
if (!asmStandaloneEnabled) {
|
|
59
|
+
let priority = getSpanPriority(rootSpan)
|
|
60
|
+
if (!priority) {
|
|
61
|
+
rootSpan._prioritySampler?.sample(rootSpan)
|
|
62
|
+
priority = getSpanPriority(rootSpan)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (priority === AUTO_REJECT || priority === USER_REJECT) {
|
|
66
|
+
return SamplingDecision.SKIP
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const resolved = resolveSamplingKey(req, res)
|
|
71
|
+
if (!resolved) return SamplingDecision.SKIP
|
|
72
|
+
|
|
73
|
+
if (record && resolved.route === null) {
|
|
74
|
+
if (resolved.status === 404 || isBlocked(res)) return SamplingDecision.SKIP
|
|
75
|
+
return SamplingDecision.MISSING_ROUTE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sampledRequests.has(resolved.key)) return SamplingDecision.SKIP
|
|
79
|
+
|
|
80
|
+
if (asmStandaloneEnabled) {
|
|
81
|
+
keepTrace(rootSpan, ASM)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (record) {
|
|
85
|
+
sampledRequests.set(resolved.key, undefined)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return SamplingDecision.SAMPLE
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {import('http').IncomingMessage} req
|
|
93
|
+
* @param {import('http').ServerResponse} res
|
|
94
|
+
* @returns {boolean} Whether this request's endpoint is currently recorded in the TTL cache.
|
|
95
|
+
*/
|
|
96
|
+
function wasSampled (req, res) {
|
|
97
|
+
const resolved = resolveSamplingKey(req, res)
|
|
98
|
+
return resolved !== null && sampledRequests.has(resolved.key)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveSamplingKey (req, res) {
|
|
102
|
+
const method = req.method
|
|
103
|
+
const status = res.statusCode
|
|
104
|
+
|
|
105
|
+
if (!method || !status) {
|
|
106
|
+
log.warn('[ASM] Unsupported groupkey for API security')
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const context = web.getContext(req)
|
|
111
|
+
const route = getRouteOrEndpoint(context, status)
|
|
112
|
+
|
|
113
|
+
// route === null signals "no route information at all". An empty string is still a valid
|
|
114
|
+
// route (dd-trace-js represents the express root path '/' as an empty path segment).
|
|
115
|
+
return { method, status, route, key: method + (route ?? '') + status }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getRouteOrEndpoint (context, statusCode) {
|
|
119
|
+
// The router plugin populates `context.paths` whenever the framework matched something.
|
|
120
|
+
// For express's root '/' route the matched path is normalized to '' (see datadog-plugin-router),
|
|
121
|
+
// so `paths.length > 0` is the signal that the framework provided route information — even when
|
|
122
|
+
// the joined string is empty. An empty `paths` array means no router involvement at all.
|
|
123
|
+
const paths = context?.paths
|
|
124
|
+
if (paths !== undefined && paths.length > 0) {
|
|
125
|
+
return paths.join('')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (statusCode === 404) return null
|
|
129
|
+
|
|
130
|
+
const endpoint = context?.span?.context()?.getTag?.('http.endpoint')
|
|
131
|
+
if (endpoint) return endpoint
|
|
132
|
+
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getSpanPriority (span) {
|
|
137
|
+
const spanContext = span.context?.()
|
|
138
|
+
return spanContext._sampling?.priority
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
configure,
|
|
143
|
+
disable,
|
|
144
|
+
sampleRequest,
|
|
145
|
+
wasSampled,
|
|
146
|
+
SamplingDecision,
|
|
147
|
+
}
|