dd-trace 5.109.0 → 5.110.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/ci/init.js +4 -3
- package/index.d.ts +66 -24
- package/loader-hook.mjs +88 -2
- package/package.json +18 -14
- 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/graphql.js +17 -6
- 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/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/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.js +264 -7
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
- 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-cypress/src/cypress-plugin.js +9 -1
- package/packages/datadog-plugin-http/src/client.js +8 -3
- package/packages/datadog-plugin-http2/src/client.js +5 -2
- package/packages/datadog-plugin-mysql/src/index.js +7 -2
- 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 +7 -1
- 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 +13 -1
- 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/index.js +10 -6
- package/packages/dd-trace/src/appsec/reporter.js +1 -0
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
- package/packages/dd-trace/src/config/defaults.js +22 -3
- package/packages/dd-trace/src/config/generated-config-types.d.ts +535 -1
- package/packages/dd-trace/src/config/helper.js +133 -33
- package/packages/dd-trace/src/config/index.js +4 -1
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/supported-configurations.json +21 -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/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 +16 -5
- 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/sdk.js +5 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
- package/packages/dd-trace/src/llmobs/tagger.js +55 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
- 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/instruments.js +2 -1
- 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 +11 -4
- 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 +2 -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/opentracing/propagation/text_map.js +3 -3
- package/packages/dd-trace/src/opentracing/span_context.js +17 -0
- package/packages/dd-trace/src/plugin_manager.js +7 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -1
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
- package/packages/dd-trace/src/plugins/util/test.js +80 -0
- 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/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 +6 -0
- package/packages/dd-trace/src/telemetry/send-data.js +12 -10
- 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/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
|
@@ -74,6 +74,9 @@ const EXCLUDED_META_KEYS = new Set([
|
|
|
74
74
|
TRACE_ID_128,
|
|
75
75
|
])
|
|
76
76
|
|
|
77
|
+
// DD-only error tags that should not appear as attributes when OTel trace semantics are enabled.
|
|
78
|
+
const DD_ERROR_META_KEYS = new Set(['error.message'])
|
|
79
|
+
|
|
77
80
|
/**
|
|
78
81
|
* OtlpTraceTransformer transforms DD-formatted spans to OTLP trace JSON format.
|
|
79
82
|
*
|
|
@@ -87,13 +90,17 @@ const EXCLUDED_META_KEYS = new Set([
|
|
|
87
90
|
* @augments OtlpTransformerBase
|
|
88
91
|
*/
|
|
89
92
|
class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
93
|
+
#otelTraceSemanticsEnabled
|
|
94
|
+
|
|
90
95
|
/**
|
|
91
96
|
* Creates a new OtlpTraceTransformer instance.
|
|
92
97
|
*
|
|
93
98
|
* @param {import('@opentelemetry/api').Attributes} resourceAttributes - Resource attributes
|
|
99
|
+
* @param {boolean} [otelTraceSemanticsEnabled] - When true, do not emit Datadog-only attributes as span attributes
|
|
94
100
|
*/
|
|
95
|
-
constructor (resourceAttributes) {
|
|
101
|
+
constructor (resourceAttributes, otelTraceSemanticsEnabled) {
|
|
96
102
|
super(resourceAttributes, 'http/json', 'traces')
|
|
103
|
+
this.#otelTraceSemanticsEnabled = otelTraceSemanticsEnabled
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
@@ -184,24 +191,30 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
184
191
|
#buildAttributes (span) {
|
|
185
192
|
const attributes = []
|
|
186
193
|
|
|
187
|
-
// Add top-level DD span fields as OTLP attributes
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
// Add top-level DD span fields as OTLP attributes.
|
|
195
|
+
// When OTel trace semantics are enabled, these Datadog-only concepts
|
|
196
|
+
// are not added to the span attributes so the output conforms to pure
|
|
197
|
+
// OpenTelemetry semantics.
|
|
198
|
+
if (!this.#otelTraceSemanticsEnabled) {
|
|
199
|
+
if (span.service) {
|
|
200
|
+
attributes.push({ key: 'service.name', value: { stringValue: span.service } })
|
|
201
|
+
}
|
|
202
|
+
if (span.name) {
|
|
203
|
+
attributes.push({ key: 'operation.name', value: { stringValue: span.name } })
|
|
204
|
+
}
|
|
205
|
+
if (span.resource) {
|
|
206
|
+
attributes.push({ key: 'resource.name', value: { stringValue: span.resource } })
|
|
207
|
+
}
|
|
208
|
+
if (span.type) {
|
|
209
|
+
attributes.push({ key: 'span.type', value: { stringValue: span.type } })
|
|
210
|
+
}
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
// Add meta string tags, skipping keys that map to dedicated OTLP fields
|
|
202
214
|
if (span.meta) {
|
|
203
215
|
for (const [key, value] of Object.entries(span.meta)) {
|
|
204
216
|
if (EXCLUDED_META_KEYS.has(key)) continue
|
|
217
|
+
if (this.#otelTraceSemanticsEnabled && DD_ERROR_META_KEYS.has(key)) continue
|
|
205
218
|
attributes.push({ key, value: { stringValue: value } })
|
|
206
219
|
}
|
|
207
220
|
}
|
|
@@ -371,8 +371,8 @@ class TextMapPropagator {
|
|
|
371
371
|
|
|
372
372
|
_hasTraceIdConflict (w3cSpanContext, firstSpanContext) {
|
|
373
373
|
return w3cSpanContext !== undefined &&
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
firstSpanContext.toTraceId(true) === w3cSpanContext.toTraceId(true) &&
|
|
375
|
+
firstSpanContext.toSpanId() !== w3cSpanContext.toSpanId()
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
_hasParentIdInTags (spanContext) {
|
|
@@ -875,7 +875,7 @@ class TextMapPropagator {
|
|
|
875
875
|
(!tracestateSamplingPriority || tracestateSamplingPriority >= 0)) {
|
|
876
876
|
samplingPriority = AUTO_REJECT
|
|
877
877
|
} else if (traceparentSampled === 1 &&
|
|
878
|
-
|
|
878
|
+
(!tracestateSamplingPriority || tracestateSamplingPriority < 0)) {
|
|
879
879
|
samplingPriority = AUTO_KEEP
|
|
880
880
|
}
|
|
881
881
|
}
|
|
@@ -63,6 +63,7 @@ class DatadogSpanContext {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
toTraceparent () {
|
|
66
|
+
this._ensureSamplingPriority()
|
|
66
67
|
const flags = this._sampling.priority >= AUTO_KEEP ? '01' : '00'
|
|
67
68
|
const traceId = this.toTraceId(true)
|
|
68
69
|
const spanId = this.toSpanId(true)
|
|
@@ -70,6 +71,22 @@ class DatadogSpanContext {
|
|
|
70
71
|
return `${version}-${traceId}-${spanId}-${flags}`
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Materialize the lazy priority-sampling decision for this trace, the same
|
|
76
|
+
* way {@link DatadogTracer#inject} does before propagation. The auto priority
|
|
77
|
+
* is otherwise computed at flush time, so the W3C sampled flag reads "drop"
|
|
78
|
+
* for a freshly started, not-yet-flushed span — see
|
|
79
|
+
* https://github.com/DataDog/dd-trace-js/issues/2547.
|
|
80
|
+
*
|
|
81
|
+
* The root span is only used to reach the priority sampler; `this` is passed
|
|
82
|
+
* to `sample()` so a manual sampling tag set directly on this context is
|
|
83
|
+
* honored, matching what `inject(this)` would decide.
|
|
84
|
+
*/
|
|
85
|
+
_ensureSamplingPriority () {
|
|
86
|
+
if (this._sampling.priority !== undefined) return
|
|
87
|
+
this._trace.started[0]?._prioritySampler?.sample(this)
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
/**
|
|
74
91
|
* Set a tag value.
|
|
75
92
|
* @param {string} key - Tag key
|
|
@@ -60,7 +60,11 @@ function maybeEnable (Plugin) {
|
|
|
60
60
|
|
|
61
61
|
function getEnabled (Plugin) {
|
|
62
62
|
const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
|
|
63
|
-
|
|
63
|
+
// skipDefault: only an explicitly configured value should drive enablement here. A registered
|
|
64
|
+
// default of `false` (e.g. an experimental plugin like `nats`) must not be read as an explicit
|
|
65
|
+
// "disabled via configuration option" — that path both logs a misleading line and nulls the
|
|
66
|
+
// plugin class, bypassing the experimental opt-in handled by `loadPlugin`.
|
|
67
|
+
return getValueFromEnvSources(normalizePluginEnvName(envName), true)
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
// TODO this must always be a singleton.
|
|
@@ -164,6 +168,7 @@ module.exports = class PluginManager {
|
|
|
164
168
|
clientIpEnabled,
|
|
165
169
|
clientIpHeader,
|
|
166
170
|
DD_TRACE_MEMCACHED_COMMAND_ENABLED,
|
|
171
|
+
DD_TRACE_OTEL_SEMANTICS_ENABLED,
|
|
167
172
|
DD_TEST_SESSION_NAME,
|
|
168
173
|
DD_AGENTLESS_LOG_SUBMISSION_ENABLED,
|
|
169
174
|
isTestDynamicInstrumentationEnabled,
|
|
@@ -181,6 +186,7 @@ module.exports = class PluginManager {
|
|
|
181
186
|
dbmPropagationMode,
|
|
182
187
|
dsmEnabled,
|
|
183
188
|
DD_TRACE_MEMCACHED_COMMAND_ENABLED,
|
|
189
|
+
DD_TRACE_OTEL_SEMANTICS_ENABLED,
|
|
184
190
|
site,
|
|
185
191
|
url,
|
|
186
192
|
headers: headerTags || [],
|
|
@@ -31,6 +31,7 @@ const Plugin = require('./plugin')
|
|
|
31
31
|
const { getRepositoryRoot } = require('./util/git')
|
|
32
32
|
const {
|
|
33
33
|
getTestEnvironmentMetadata,
|
|
34
|
+
getTestLevelsMetadataTags,
|
|
34
35
|
getTestSessionName,
|
|
35
36
|
getCodeOwnersFileEntries,
|
|
36
37
|
getTestParentSpan,
|
|
@@ -44,6 +45,7 @@ const {
|
|
|
44
45
|
TEST_MODULE_ID,
|
|
45
46
|
TEST_SESSION_ID,
|
|
46
47
|
TEST_COMMAND,
|
|
48
|
+
TEST_LEVELS_METADATA,
|
|
47
49
|
TEST_MODULE,
|
|
48
50
|
TEST_SESSION_NAME,
|
|
49
51
|
getTestSuiteCommonTags,
|
|
@@ -219,7 +221,13 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
219
221
|
this.testEnvironmentMetadata
|
|
220
222
|
)
|
|
221
223
|
|
|
222
|
-
const metadataTags = {
|
|
224
|
+
const metadataTags = {
|
|
225
|
+
[TEST_LEVELS_METADATA]: {
|
|
226
|
+
[TEST_COMMAND]: command,
|
|
227
|
+
[TEST_SESSION_NAME]: testSessionName,
|
|
228
|
+
...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
223
231
|
// tracer might not be initialized correctly
|
|
224
232
|
if (this.tracer._exporter.addMetadataTags) {
|
|
225
233
|
this.tracer._exporter.addMetadataTags(metadataTags)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { extractPathFromUrl } = require('./url')
|
|
4
|
+
|
|
5
|
+
// OpenTelemetry HTTP semantic-convention attribute names, emitted in place of
|
|
6
|
+
// the Datadog ones when `DD_TRACE_OTEL_SEMANTICS_ENABLED` is set.
|
|
7
|
+
// See https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
8
|
+
const HTTP_REQUEST_METHOD = 'http.request.method'
|
|
9
|
+
const HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'
|
|
10
|
+
const URL_FULL = 'url.full'
|
|
11
|
+
const URL_PATH = 'url.path'
|
|
12
|
+
const URL_SCHEME = 'url.scheme'
|
|
13
|
+
const URL_QUERY = 'url.query'
|
|
14
|
+
const SERVER_ADDRESS = 'server.address'
|
|
15
|
+
const SERVER_PORT = 'server.port'
|
|
16
|
+
const USER_AGENT_ORIGINAL = 'user_agent.original'
|
|
17
|
+
const CLIENT_ADDRESS = 'client.address'
|
|
18
|
+
const NETWORK_PEER_ADDRESS = 'network.peer.address'
|
|
19
|
+
const HTTP_REQUEST_METHOD_ORIGINAL = 'http.request.method_original'
|
|
20
|
+
|
|
21
|
+
// Known HTTP methods (RFC 9110 + PATCH RFC 5789 + QUERY httpbis draft). A verb
|
|
22
|
+
// outside this set is reported as `_OTHER` with the raw value preserved on
|
|
23
|
+
// `http.request.method_original`, per the OTel HTTP semantic conventions.
|
|
24
|
+
const KNOWN_METHODS = new Set([
|
|
25
|
+
'CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'QUERY', 'TRACE',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
// Datadog HTTP meta keys replaced by OTel names — omitted when rebuilding meta.
|
|
29
|
+
const DD_HTTP_META_KEYS = new Set([
|
|
30
|
+
'http.method', 'http.status_code', 'http.useragent', 'http.client_ip', 'http.endpoint', 'http.url', 'out.host',
|
|
31
|
+
])
|
|
32
|
+
const NETWORK_DESTINATION_PORT = 'network.destination.port'
|
|
33
|
+
|
|
34
|
+
// IPv6 literals arrive bracketed (URL.hostname / out.host = `[::1]`); OTel
|
|
35
|
+
// `server.address` is the bare address.
|
|
36
|
+
function stripIpv6Brackets (host) {
|
|
37
|
+
return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} ServerUrlParts
|
|
42
|
+
* @property {string} [scheme] value for `url.scheme`
|
|
43
|
+
* @property {string} [address] value for `server.address`
|
|
44
|
+
* @property {number} [port] value for `server.port`
|
|
45
|
+
* @property {string} path value for `url.path`
|
|
46
|
+
* @property {string} [query] value for `url.query` (omitted when empty)
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decompose a server request URL into the OpenTelemetry `url.*` / `server.*`
|
|
51
|
+
* parts. Structural fields (scheme, address, port, path) are read from the raw
|
|
52
|
+
* URL; the query is taken from the already-obfuscated URL so the configured
|
|
53
|
+
* query-string obfuscation is preserved.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} rawUrl full request URL (`scheme://host[:port]/path?query`)
|
|
56
|
+
* @param {string} obfuscatedUrl same URL with its query string obfuscated
|
|
57
|
+
* @returns {ServerUrlParts}
|
|
58
|
+
*/
|
|
59
|
+
function decomposeServerUrl (rawUrl, obfuscatedUrl) {
|
|
60
|
+
let scheme
|
|
61
|
+
let address
|
|
62
|
+
let port
|
|
63
|
+
let path
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(rawUrl)
|
|
67
|
+
scheme = parsed.protocol.length > 1 ? parsed.protocol.slice(0, -1) : undefined
|
|
68
|
+
// `extractURL` builds `http://undefined/...` when the Host header is absent; skip that.
|
|
69
|
+
const hostname = parsed.hostname
|
|
70
|
+
if (hostname && hostname !== 'undefined') {
|
|
71
|
+
address = stripIpv6Brackets(hostname)
|
|
72
|
+
}
|
|
73
|
+
if (parsed.port) {
|
|
74
|
+
const parsedPort = Number.parseInt(parsed.port)
|
|
75
|
+
if (parsedPort > 0) port = parsedPort
|
|
76
|
+
}
|
|
77
|
+
path = parsed.pathname || '/'
|
|
78
|
+
} catch {
|
|
79
|
+
// Malformed or relative URL: fall back to a best-effort path only.
|
|
80
|
+
path = extractPathFromUrl(rawUrl)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let query
|
|
84
|
+
const queryIndex = obfuscatedUrl.indexOf('?')
|
|
85
|
+
if (queryIndex !== -1) {
|
|
86
|
+
const rawQuery = obfuscatedUrl.slice(queryIndex + 1)
|
|
87
|
+
if (rawQuery) query = rawQuery
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { scheme, address, port, path, query }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ERROR_TYPE = 'error.type'
|
|
94
|
+
|
|
95
|
+
function toHttpScheme (scheme) {
|
|
96
|
+
if (scheme === 'ws') return 'http'
|
|
97
|
+
if (scheme === 'wss') return 'https'
|
|
98
|
+
return scheme
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Redact any userinfo embedded in a URL's authority, since `url.full` must not
|
|
103
|
+
* leak credentials: `user:pass@host` -> `REDACTED:REDACTED@host`, `user@host` ->
|
|
104
|
+
* `REDACTED@host`. Returns the URL unchanged when no userinfo is present.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} url
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function redactUrlCredentials (url) {
|
|
110
|
+
const schemeEnd = url.indexOf('://')
|
|
111
|
+
if (schemeEnd === -1) return url
|
|
112
|
+
const authorityStart = schemeEnd + 3
|
|
113
|
+
|
|
114
|
+
let authorityEnd = url.length
|
|
115
|
+
for (let i = authorityStart; i < url.length; i++) {
|
|
116
|
+
const char = url[i]
|
|
117
|
+
if (char === '/' || char === '?' || char === '#') {
|
|
118
|
+
authorityEnd = i
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// userinfo runs to the LAST '@' in the authority (WHATWG); using the first
|
|
124
|
+
// '@' would leak the remainder, e.g. `user:p@ss@host`.
|
|
125
|
+
const at = url.lastIndexOf('@', authorityEnd - 1)
|
|
126
|
+
if (at < authorityStart) return url
|
|
127
|
+
|
|
128
|
+
const redacted = url.slice(authorityStart, at).includes(':') ? 'REDACTED:REDACTED' : 'REDACTED'
|
|
129
|
+
return url.slice(0, authorityStart) + redacted + url.slice(at)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The scheme's default port, used as the `server.port` fallback for client spans
|
|
134
|
+
* (the attribute is required for clients but the explicit port is absent for
|
|
135
|
+
* default-port requests).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} [url]
|
|
138
|
+
* @returns {number | undefined}
|
|
139
|
+
*/
|
|
140
|
+
function defaultPortForUrl (url) {
|
|
141
|
+
if (url === undefined) return
|
|
142
|
+
if (url.startsWith('https:') || url.startsWith('wss:')) return 443
|
|
143
|
+
if (url.startsWith('http:') || url.startsWith('ws:')) return 80
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @typedef {object} FormattedHttpSpan
|
|
148
|
+
* @property {Record<string, string>} meta
|
|
149
|
+
* @property {Record<string, number>} metrics
|
|
150
|
+
* @property {number} error
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rewrite a formatted span's Datadog HTTP tags to OpenTelemetry HTTP
|
|
155
|
+
* semantic-convention names, in place. Called at serialization time (from
|
|
156
|
+
* `span_format`) when `DD_TRACE_OTEL_SEMANTICS_ENABLED` is set, so every HTTP
|
|
157
|
+
* integration is covered from one place. No-op for non-HTTP spans. The span
|
|
158
|
+
* keeps the Datadog tag names throughout its lifetime — only the serialized
|
|
159
|
+
* output is renamed — so runtime consumers (peer.service, AppSec, trace stats)
|
|
160
|
+
* are unaffected.
|
|
161
|
+
*
|
|
162
|
+
* @param {FormattedHttpSpan} formattedSpan
|
|
163
|
+
*/
|
|
164
|
+
function applyHttpOtelSemantics (formattedSpan) {
|
|
165
|
+
const meta = formattedSpan.meta
|
|
166
|
+
const metrics = formattedSpan.metrics
|
|
167
|
+
const method = meta['http.method']
|
|
168
|
+
const url = meta['http.url']
|
|
169
|
+
if (method === undefined && url === undefined) return
|
|
170
|
+
|
|
171
|
+
// Rebuild meta/metrics as fresh objects that omit the renamed Datadog HTTP
|
|
172
|
+
// keys. Deleting them in place demotes the formatted span to V8 dictionary
|
|
173
|
+
// mode (~40% slower than this rebuild, measured); a fresh object keeps fast
|
|
174
|
+
// properties and can't leak a renamed key as `undefined` on the OTLP path.
|
|
175
|
+
const newMeta = {}
|
|
176
|
+
for (const key of Object.keys(meta)) {
|
|
177
|
+
if (!DD_HTTP_META_KEYS.has(key)) newMeta[key] = meta[key]
|
|
178
|
+
}
|
|
179
|
+
const newMetrics = {}
|
|
180
|
+
for (const key of Object.keys(metrics)) {
|
|
181
|
+
if (key !== NETWORK_DESTINATION_PORT) newMetrics[key] = metrics[key]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const kind = meta['span.kind']
|
|
185
|
+
|
|
186
|
+
if (method !== undefined) {
|
|
187
|
+
if (KNOWN_METHODS.has(method)) {
|
|
188
|
+
newMeta[HTTP_REQUEST_METHOD] = method
|
|
189
|
+
} else {
|
|
190
|
+
// Unknown verb: bucket to `_OTHER`, preserve the raw value, and use the
|
|
191
|
+
// literal "HTTP" in the span name (the spec forbids the URL path there).
|
|
192
|
+
// Known-method names are already `{method} {route}`.
|
|
193
|
+
newMeta[HTTP_REQUEST_METHOD] = '_OTHER'
|
|
194
|
+
newMeta[HTTP_REQUEST_METHOD_ORIGINAL] = method
|
|
195
|
+
const resource = formattedSpan.resource
|
|
196
|
+
if (typeof resource === 'string') {
|
|
197
|
+
if (resource === method) {
|
|
198
|
+
formattedSpan.resource = 'HTTP'
|
|
199
|
+
} else if (resource.startsWith(`${method} `)) {
|
|
200
|
+
formattedSpan.resource = `HTTP${resource.slice(method.length)}`
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const status = meta['http.status_code']
|
|
207
|
+
let statusCode
|
|
208
|
+
if (status !== undefined) {
|
|
209
|
+
// OTel types http.response.status_code as an int, so emit it as a numeric
|
|
210
|
+
// metric (the OTLP exporter serializes meta as stringValue but metrics as
|
|
211
|
+
// intValue) — mirroring how server.port is handled below. Guard against a
|
|
212
|
+
// non-numeric status, which would otherwise write a NaN metric.
|
|
213
|
+
statusCode = Number.parseInt(status)
|
|
214
|
+
if (Number.isFinite(statusCode)) newMetrics[HTTP_RESPONSE_STATUS_CODE] = statusCode
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const userAgent = meta['http.useragent']
|
|
218
|
+
if (userAgent !== undefined) newMeta[USER_AGENT_ORIGINAL] = userAgent
|
|
219
|
+
|
|
220
|
+
const clientIp = meta['http.client_ip']
|
|
221
|
+
if (clientIp !== undefined) newMeta[CLIENT_ADDRESS] = clientIp
|
|
222
|
+
|
|
223
|
+
// http.endpoint is Datadog-only (omitted above); it has no OTel equivalent.
|
|
224
|
+
|
|
225
|
+
if (kind === 'server') {
|
|
226
|
+
// FIXME: some server frameworks (e.g. Next.js — `packages/datadog-plugin-next`)
|
|
227
|
+
// never populate `http.url`, so the OTel `url.*` / `server.*` attributes below
|
|
228
|
+
// can't be derived and are omitted for those spans. This needs a fix; the
|
|
229
|
+
// short-term option is to set `http.url` in those integrations so they emit
|
|
230
|
+
// the full server attribute set.
|
|
231
|
+
if (url !== undefined) {
|
|
232
|
+
// The query in `http.url` is already obfuscated per config, so it is preserved.
|
|
233
|
+
const { scheme, address, port, path, query } = decomposeServerUrl(url, url)
|
|
234
|
+
if (path !== undefined) newMeta[URL_PATH] = path
|
|
235
|
+
if (scheme !== undefined) newMeta[URL_SCHEME] = toHttpScheme(scheme)
|
|
236
|
+
if (query !== undefined) newMeta[URL_QUERY] = query
|
|
237
|
+
if (address !== undefined) newMeta[SERVER_ADDRESS] = address
|
|
238
|
+
if (port !== undefined) newMetrics[SERVER_PORT] = port
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
if (url !== undefined) {
|
|
242
|
+
// url.full must not carry embedded credentials.
|
|
243
|
+
newMeta[URL_FULL] = redactUrlCredentials(url)
|
|
244
|
+
}
|
|
245
|
+
const outHost = meta['out.host']
|
|
246
|
+
if (outHost !== undefined) newMeta[SERVER_ADDRESS] = stripIpv6Brackets(outHost)
|
|
247
|
+
const clientPort = metrics[NETWORK_DESTINATION_PORT]
|
|
248
|
+
if (clientPort === undefined) {
|
|
249
|
+
// server.port is required for client spans; fall back to the scheme default.
|
|
250
|
+
const defaultPort = defaultPortForUrl(url)
|
|
251
|
+
if (defaultPort !== undefined) newMetrics[SERVER_PORT] = defaultPort
|
|
252
|
+
} else {
|
|
253
|
+
newMetrics[SERVER_PORT] = clientPort
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// OTel error semantics for an error response (no-clobber on an exception-derived
|
|
258
|
+
// type): server spans are errors on 5xx only (4xx MUST be left unset per the
|
|
259
|
+
// spec); client spans on any status >= 400.
|
|
260
|
+
if (status !== undefined && newMeta[ERROR_TYPE] === undefined) {
|
|
261
|
+
const isError = kind === 'server' ? statusCode >= 500 : statusCode >= 400
|
|
262
|
+
if (isError) {
|
|
263
|
+
newMeta[ERROR_TYPE] = status
|
|
264
|
+
formattedSpan.error = 1
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
formattedSpan.meta = newMeta
|
|
269
|
+
formattedSpan.metrics = newMetrics
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
NETWORK_PEER_ADDRESS, // imported by web.js (set from req.socket, not at serialization)
|
|
274
|
+
decomposeServerUrl, // exercised directly by the helper spec
|
|
275
|
+
applyHttpOtelSemantics,
|
|
276
|
+
}
|
|
@@ -22,17 +22,40 @@ const { SAMPLING_RULE_DECISION } = require('../../constants')
|
|
|
22
22
|
const { AUTO_KEEP } = require('../../../../../ext/priority')
|
|
23
23
|
const { version: ddTraceVersion } = require('../../../../../package.json')
|
|
24
24
|
const {
|
|
25
|
+
CI_JOB_ID,
|
|
25
26
|
GIT_BRANCH,
|
|
26
27
|
GIT_COMMIT_SHA,
|
|
27
28
|
GIT_REPOSITORY_URL,
|
|
28
29
|
GIT_TAG,
|
|
30
|
+
GIT_COMMIT_AUTHOR_DATE,
|
|
29
31
|
GIT_COMMIT_AUTHOR_EMAIL,
|
|
30
32
|
GIT_COMMIT_AUTHOR_NAME,
|
|
33
|
+
GIT_COMMIT_COMMITTER_DATE,
|
|
34
|
+
GIT_COMMIT_COMMITTER_EMAIL,
|
|
35
|
+
GIT_COMMIT_COMMITTER_NAME,
|
|
31
36
|
GIT_COMMIT_MESSAGE,
|
|
37
|
+
GIT_COMMIT_HEAD_AUTHOR_DATE,
|
|
38
|
+
GIT_COMMIT_HEAD_AUTHOR_EMAIL,
|
|
39
|
+
GIT_COMMIT_HEAD_AUTHOR_NAME,
|
|
40
|
+
GIT_COMMIT_HEAD_COMMITTER_DATE,
|
|
41
|
+
GIT_COMMIT_HEAD_COMMITTER_EMAIL,
|
|
42
|
+
GIT_COMMIT_HEAD_COMMITTER_NAME,
|
|
43
|
+
GIT_COMMIT_HEAD_MESSAGE,
|
|
32
44
|
CI_WORKSPACE_PATH,
|
|
45
|
+
CI_PIPELINE_ID,
|
|
46
|
+
CI_PIPELINE_NAME,
|
|
47
|
+
CI_PIPELINE_NUMBER,
|
|
33
48
|
CI_PIPELINE_URL,
|
|
34
49
|
CI_JOB_NAME,
|
|
50
|
+
CI_JOB_URL,
|
|
51
|
+
CI_NODE_LABELS,
|
|
52
|
+
CI_NODE_NAME,
|
|
53
|
+
CI_PROVIDER_NAME,
|
|
54
|
+
CI_STAGE_NAME,
|
|
35
55
|
GIT_COMMIT_HEAD_SHA,
|
|
56
|
+
GIT_PULL_REQUEST_BASE_BRANCH,
|
|
57
|
+
GIT_PULL_REQUEST_BASE_BRANCH_HEAD_SHA,
|
|
58
|
+
GIT_PULL_REQUEST_BASE_BRANCH_SHA,
|
|
36
59
|
} = require('./tags')
|
|
37
60
|
const { getRuntimeAndOSMetadata } = require('./env')
|
|
38
61
|
const { getCIMetadata } = require('./ci')
|
|
@@ -198,6 +221,43 @@ const TEST_LEVEL_EVENT_TYPES = [
|
|
|
198
221
|
'test_module_end',
|
|
199
222
|
'test_session_end',
|
|
200
223
|
]
|
|
224
|
+
const TEST_LEVELS_METADATA = 'test_levels'
|
|
225
|
+
const TEST_LEVELS_METADATA_TAGS = [
|
|
226
|
+
CI_JOB_ID,
|
|
227
|
+
CI_JOB_NAME,
|
|
228
|
+
CI_JOB_URL,
|
|
229
|
+
CI_NODE_LABELS,
|
|
230
|
+
CI_NODE_NAME,
|
|
231
|
+
CI_PIPELINE_ID,
|
|
232
|
+
CI_PIPELINE_NAME,
|
|
233
|
+
CI_PIPELINE_NUMBER,
|
|
234
|
+
CI_PIPELINE_URL,
|
|
235
|
+
CI_PROVIDER_NAME,
|
|
236
|
+
CI_STAGE_NAME,
|
|
237
|
+
CI_WORKSPACE_PATH,
|
|
238
|
+
GIT_BRANCH,
|
|
239
|
+
GIT_COMMIT_AUTHOR_DATE,
|
|
240
|
+
GIT_COMMIT_AUTHOR_EMAIL,
|
|
241
|
+
GIT_COMMIT_AUTHOR_NAME,
|
|
242
|
+
GIT_COMMIT_COMMITTER_DATE,
|
|
243
|
+
GIT_COMMIT_COMMITTER_EMAIL,
|
|
244
|
+
GIT_COMMIT_COMMITTER_NAME,
|
|
245
|
+
GIT_COMMIT_HEAD_AUTHOR_DATE,
|
|
246
|
+
GIT_COMMIT_HEAD_AUTHOR_EMAIL,
|
|
247
|
+
GIT_COMMIT_HEAD_AUTHOR_NAME,
|
|
248
|
+
GIT_COMMIT_HEAD_COMMITTER_DATE,
|
|
249
|
+
GIT_COMMIT_HEAD_COMMITTER_EMAIL,
|
|
250
|
+
GIT_COMMIT_HEAD_COMMITTER_NAME,
|
|
251
|
+
GIT_COMMIT_HEAD_MESSAGE,
|
|
252
|
+
GIT_COMMIT_HEAD_SHA,
|
|
253
|
+
GIT_COMMIT_MESSAGE,
|
|
254
|
+
GIT_COMMIT_SHA,
|
|
255
|
+
GIT_PULL_REQUEST_BASE_BRANCH,
|
|
256
|
+
GIT_PULL_REQUEST_BASE_BRANCH_HEAD_SHA,
|
|
257
|
+
GIT_PULL_REQUEST_BASE_BRANCH_SHA,
|
|
258
|
+
GIT_REPOSITORY_URL,
|
|
259
|
+
GIT_TAG,
|
|
260
|
+
]
|
|
201
261
|
const TEST_RETRY_REASON_TYPES = {
|
|
202
262
|
efd: 'early_flake_detection',
|
|
203
263
|
atr: 'auto_test_retry',
|
|
@@ -398,6 +458,7 @@ module.exports = {
|
|
|
398
458
|
getCodeOwnersFileEntries,
|
|
399
459
|
getCodeOwnersForFilename,
|
|
400
460
|
getTestCommonTags,
|
|
461
|
+
getTestLevelsMetadataTags,
|
|
401
462
|
getTestSessionCommonTags,
|
|
402
463
|
getTestModuleCommonTags,
|
|
403
464
|
getTestSuiteCommonTags,
|
|
@@ -448,6 +509,7 @@ module.exports = {
|
|
|
448
509
|
DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX,
|
|
449
510
|
DD_CAPABILITIES_FAILED_TEST_REPLAY,
|
|
450
511
|
TEST_LEVEL_EVENT_TYPES,
|
|
512
|
+
TEST_LEVELS_METADATA,
|
|
451
513
|
TEST_RETRY_REASON_TYPES,
|
|
452
514
|
getNumFromKnownTests,
|
|
453
515
|
getFileAndLineNumberFromError,
|
|
@@ -700,6 +762,24 @@ function getTestParametersString (parametersByTestName, testName) {
|
|
|
700
762
|
}
|
|
701
763
|
}
|
|
702
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Extracts CI and Git tags that apply to every test level.
|
|
767
|
+
*
|
|
768
|
+
* @param {TestEnvironmentMetadata} testEnvironmentMetadata
|
|
769
|
+
* @returns {Record<string, string|number>}
|
|
770
|
+
*/
|
|
771
|
+
function getTestLevelsMetadataTags (testEnvironmentMetadata) {
|
|
772
|
+
const testLevelsMetadataTags = {}
|
|
773
|
+
for (let i = 0; i < TEST_LEVELS_METADATA_TAGS.length; i++) {
|
|
774
|
+
const key = TEST_LEVELS_METADATA_TAGS[i]
|
|
775
|
+
const value = testEnvironmentMetadata[key]
|
|
776
|
+
if (value !== undefined) {
|
|
777
|
+
testLevelsMetadataTags[key] = value
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return testLevelsMetadataTags
|
|
781
|
+
}
|
|
782
|
+
|
|
703
783
|
function getTestTypeFromFramework (testFramework) {
|
|
704
784
|
if (testFramework === 'playwright' || testFramework === 'cypress') {
|
|
705
785
|
return 'browser'
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { URL } = require('url')
|
|
4
4
|
|
|
5
|
+
const log = require('../../log')
|
|
6
|
+
|
|
5
7
|
const HTTP2_HEADER_AUTHORITY = ':authority'
|
|
6
8
|
const HTTP2_HEADER_SCHEME = ':scheme'
|
|
7
9
|
const HTTP2_HEADER_PATH = ':path'
|
|
@@ -60,6 +62,69 @@ function obfuscateQs (config, url) {
|
|
|
60
62
|
return `${path}?${qs}`
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const qsObfuscatorCache = new Map()
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compile the configured query-string obfuscator (a regex string, or a boolean)
|
|
69
|
+
* into the boolean / RegExp form that `obfuscateQs` consumes. The compiled regex
|
|
70
|
+
* is cached, since the configuration is stable for the process lifetime.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ queryStringObfuscation?: boolean | string }} config
|
|
73
|
+
* @returns {boolean | RegExp}
|
|
74
|
+
*/
|
|
75
|
+
function getQsObfuscator (config) {
|
|
76
|
+
const obfuscator = config.queryStringObfuscation
|
|
77
|
+
|
|
78
|
+
if (typeof obfuscator === 'boolean') return obfuscator
|
|
79
|
+
|
|
80
|
+
if (typeof obfuscator === 'string') {
|
|
81
|
+
const cached = qsObfuscatorCache.get(obfuscator)
|
|
82
|
+
if (cached !== undefined) return cached
|
|
83
|
+
|
|
84
|
+
let compiled = true
|
|
85
|
+
if (obfuscator === '') {
|
|
86
|
+
compiled = false // disable obfuscator
|
|
87
|
+
} else if (obfuscator !== '.*') { // '.*' optimizes to a full redact (true)
|
|
88
|
+
try {
|
|
89
|
+
compiled = new RegExp(obfuscator, 'gi')
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.error('Error getting qs obfuscator', err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
qsObfuscatorCache.set(obfuscator, compiled)
|
|
96
|
+
return compiled
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Object.hasOwn(config, 'queryStringObfuscation')) {
|
|
100
|
+
log.error('Expected `queryStringObfuscation` to be a regex string or boolean.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a client span's `http.url` with its query retained but obfuscated per
|
|
108
|
+
* `config.queryStringObfuscation` (OTel `url.full` is the absolute URL including
|
|
109
|
+
* the redacted query). Falls back to `strippedUrl` when there is no query.
|
|
110
|
+
* Callers gate this behind `DD_TRACE_OTEL_SEMANTICS_ENABLED`, so the default
|
|
111
|
+
* (flag off) hot path stays a plain tag assignment.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ queryStringObfuscation?: boolean | string }} config
|
|
114
|
+
* @param {string} base `scheme://host[:port]`
|
|
115
|
+
* @param {string} [pathname] raw request path, may include `?query`
|
|
116
|
+
* @param {string} strippedUrl `base` + query-stripped path (used when there is no query)
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function buildClientHttpUrl (config, base, pathname, strippedUrl) {
|
|
120
|
+
if (pathname?.includes('?')) {
|
|
121
|
+
// `config.queryStringObfuscation` is the raw config value here (client plugins
|
|
122
|
+
// don't normalize it the way the server does), so compile it first.
|
|
123
|
+
return obfuscateQs({ queryStringObfuscation: getQsObfuscator(config) }, `${base}${pathname}`)
|
|
124
|
+
}
|
|
125
|
+
return strippedUrl
|
|
126
|
+
}
|
|
127
|
+
|
|
63
128
|
/**
|
|
64
129
|
* Extract URL path from URL using regex pattern instead of Node.js URL API because:
|
|
65
130
|
*
|
|
@@ -139,7 +204,9 @@ function filterSensitiveInfoFromRepository (repositoryUrl) {
|
|
|
139
204
|
module.exports = {
|
|
140
205
|
extractURL,
|
|
141
206
|
obfuscateQs,
|
|
207
|
+
getQsObfuscator,
|
|
208
|
+
buildClientHttpUrl,
|
|
142
209
|
calculateHttpEndpoint,
|
|
143
210
|
filterSensitiveInfoFromRepository,
|
|
144
|
-
extractPathFromUrl, //
|
|
211
|
+
extractPathFromUrl, // used by http-otel-semantics decomposeServerUrl fallback (and tests)
|
|
145
212
|
}
|