diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs
index 6e04e7a41e71..6051c80536d2 100644
--- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs
+++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs
@@ -15,6 +15,7 @@
using Datadog.Trace.Configuration.ConfigurationSources.Telemetry;
using Datadog.Trace.Configuration.Telemetry;
using Datadog.Trace.DataStreamsMonitoring.TransactionTracking;
+using Datadog.Trace.FeatureFlags;
using Datadog.Trace.Logging;
using Datadog.Trace.Logging.DirectSubmission;
using Datadog.Trace.PlatformHelpers;
@@ -888,6 +889,11 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) =
Manager = new(source, this, telemetry, errorLog);
+ // The environment is deliberately not passed in: it can be changed in code after
+ // startup, so the delivery source subscribes to the manager and applies the current
+ // value per request instead of capturing one here.
+ FeatureFlags = new FeatureFlagsSettings(source, telemetry);
+
// OTLP span metrics require OTLP trace export (see TracerManagerFactory.GetAgentWriter).
// Force to false otherwise, even if explicitly requested.
if (OtelTracesSpanMetricsEnabled && !Manager.InitialExporterSettings.IsOtlpTraceExport)
@@ -1500,6 +1506,11 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) =
///
internal bool IsSpanEnrichmentEnabled { get; }
+ ///
+ /// Gets the Feature Flags settings, which select where flag configuration is delivered from.
+ ///
+ internal FeatureFlagsSettings FeatureFlags { get; }
+
///
/// Gets a value indicating whether partial flush is enabled
///
diff --git a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
index cba391e5643a..de6472239a3b 100644
--- a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
+++ b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
@@ -3157,6 +3157,85 @@ supportedConfigurations:
documentation: |-
Enables Feature Flags Provider (Experimental).
Default value is false (disabled).
+ DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS:
+ - implementation: A
+ scope: managed
+ type: int
+ default: '30000'
+ product: FeatureFlags
+ const_name: FlaggingProviderInitializationTimeoutMs
+ documentation: |-
+ Configuration key for how long, in milliseconds, provider initialization waits for the first
+ flag configuration to arrive before returning.
+ Default value is 30000 (30 seconds), matching the other tracers.
+ Initialization does not fail when the timeout expires: the provider stays not-ready, evaluations
+ return the caller's default value, and the provider becomes ready when configuration arrives.
+ DD_FEATURE_FLAGS_ENABLED:
+ - implementation: A
+ scope: managed
+ type: boolean
+ default: 'true'
+ product: FeatureFlags
+ const_name: FeatureFlagsEnabled
+ documentation: |-
+ Configuration key to enable or disable Feature Flags.
+ Default value is true (enabled).
+ Feature Flags only contact Datadog once application code initializes the provider, so enabling
+ this alone does not start requesting flag configuration.
+ This supersedes .
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE:
+ - implementation: A
+ scope: managed
+ type: string
+ default: agentless
+ product: FeatureFlags
+ const_name: FeatureFlagsConfigurationSource
+ documentation: |-
+ Configuration key for selecting where flag configuration is loaded from.
+ Supported values are agentless (direct HTTP delivery, the default),
+ remote_config (delivery through the Datadog Agent's Remote Configuration)
+ and offline, which disables Feature Flags and contacts nothing.
+ Any other value is rejected and the default applies.
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL:
+ - implementation: A
+ scope: managed
+ type: string
+ sensitive: true
+ default: null
+ product: FeatureFlags
+ const_name: FeatureFlagsConfigurationSourceAgentlessBaseUrl
+ documentation: |-
+ Configuration key for overriding the endpoint used by the agentless configuration source.
+ If unset, the endpoint is derived from and Datadog hosts it.
+ A configured URL is treated as an endpoint of your own, which changes three things:
+ the Datadog API key is not sent to it, so it is responsible for its own authentication;
+ it is requested exactly as written, so dd_env is not added and any environment or tenant
+ scope has to be part of the URL you configure;
+ and only its path is completed, when it has none or a path of /, with the standard
+ rules-based server path. Any other path is used verbatim as the exact endpoint.
+ The value may carry credentials, so it is never written to logs or telemetry.
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS:
+ - implementation: A
+ scope: managed
+ type: int
+ default: '30'
+ product: FeatureFlags
+ const_name: FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds
+ documentation: |-
+ Configuration key for how often, in seconds, the agentless configuration source polls for
+ flag configuration.
+ Default value is 30. Values outside (0, 3600] are rejected and the default is used.
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS:
+ - implementation: A
+ scope: managed
+ type: int
+ default: '5'
+ product: FeatureFlags
+ const_name: FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds
+ documentation: |-
+ Configuration key for the request timeout, in seconds, used by the agentless configuration
+ source.
+ Default value is 5. Non-positive values are rejected and the default is used.
DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED:
- implementation: A
scope: managed
diff --git a/tracer/src/Datadog.Trace/FeatureFlags/Agentless/AgentlessEndpoint.cs b/tracer/src/Datadog.Trace/FeatureFlags/Agentless/AgentlessEndpoint.cs
new file mode 100644
index 000000000000..08868b22573d
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/Agentless/AgentlessEndpoint.cs
@@ -0,0 +1,186 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Datadog.Trace.Processors;
+using Datadog.Trace.Util;
+
+namespace Datadog.Trace.FeatureFlags.Agentless;
+
+///
+/// The agentless endpoint, derived from the Datadog site or a custom base URL. A class rather
+/// than a struct so that "no endpoint" is null instead of a default instance whose
+/// non-nullable is null; it is built once per process, so the allocation is free.
+///
+internal sealed class AgentlessEndpoint
+{
+ ///
+ /// Canonical rules-based server path, appended to the managed CDN host and to custom base
+ /// URLs that only supply an origin.
+ ///
+ internal const string DefaultPath = "/api/v2/feature-flagging/config/rules-based/server";
+
+ ///
+ /// The prefix prepended to the site to form the managed CDN host.
+ ///
+ internal const string ManagedHostPrefix = "ufc-server.ff-cdn.";
+
+ ///
+ /// The query parameter carrying the environment to request configuration for. Added to the
+ /// managed endpoint only.
+ ///
+ internal const string EnvParameterName = "dd_env";
+
+ private AgentlessEndpoint(Uri uri, bool isManaged)
+ {
+ Uri = uri;
+ IsManaged = isManaged;
+ }
+
+ ///
+ /// Gets the endpoint URI, without the environment. Use to get the
+ /// URI to request.
+ ///
+ public Uri Uri { get; }
+
+ ///
+ /// Gets a value indicating whether this is the endpoint derived from the site. The API key is
+ /// only sent there: a custom endpoint reports its own authentication failure rather than
+ /// having the credential leaked to it.
+ ///
+ public bool IsManaged { get; }
+
+ ///
+ /// Builds the endpoint. Without a custom the managed Datadog CDN
+ /// endpoint is derived from the (lowercased) site, so staging and government sites resolve
+ /// with no allowlist, and is the only endpoint dd_env is added to. A custom base URL that
+ /// is an origin receives the canonical path; one that carries a path is used verbatim.
+ ///
+ /// The environment is not part of the endpoint: it can be changed in code while the application
+ /// runs, so it is applied per request by instead.
+ ///
+ ///
+ /// The Datadog site, for example datadoghq.com.
+ /// The configured endpoint override, or null.
+ /// The resulting endpoint, or null when none could be built.
+ /// Why the configured base URL was rejected. Never contains the URL, which may carry credentials.
+ /// true when an endpoint could be built.
+ public static bool TryCreate(string? site, string? baseUrl, [NotNullWhen(true)] out AgentlessEndpoint? endpoint, out string? error)
+ {
+ endpoint = null;
+ error = null;
+
+ var configured = baseUrl?.Trim();
+ if (StringUtil.IsNullOrEmpty(configured))
+ {
+ var trimmedSite = site?.Trim();
+ if (StringUtil.IsNullOrEmpty(trimmedSite))
+ {
+ error = "No Datadog site is configured";
+ return false;
+ }
+
+ // The site is concatenated into a host, so every character that can change what a URL means
+ // has to be rejected before that happens. "@" is the dangerous one: it would make the rest of
+ // the value the real host, and the API key would be sent there. "/", "?" and "#" would start a
+ // path, query or fragment, and ":" a port or a scheme. Uri.TryCreate accepts several of these,
+ // so it cannot be relied on to catch them. The other tracers reject the same set.
+ foreach (var character in trimmedSite)
+ {
+ if (char.IsWhiteSpace(character) || character is '/' or '?' or '#' or '@' or ':')
+ {
+ error = "The configured Datadog site is not valid";
+ return false;
+ }
+ }
+
+ var managedHost = ManagedHostPrefix + trimmedSite.ToLowerInvariant();
+
+ if (!Uri.TryCreate($"https://{managedHost}{DefaultPath}", UriKind.Absolute, out var managedUri))
+ {
+ error = "The configured Datadog site is not valid";
+ return false;
+ }
+
+ endpoint = new AgentlessEndpoint(managedUri, isManaged: true);
+ return true;
+ }
+
+ // Uri parsing rejects whitespace inside a host, but accepts it in a path or query, so a
+ // URL carrying it there is malformed and has to be caught explicitly.
+ foreach (var character in configured)
+ {
+ if (char.IsWhiteSpace(character))
+ {
+ error = "The configured Feature Flags agentless URL is not a valid URL";
+ return false;
+ }
+ }
+
+ if (!Uri.TryCreate(configured, UriKind.Absolute, out var custom) || StringUtil.IsNullOrEmpty(custom.Host))
+ {
+ error = "The configured Feature Flags agentless URL is not a valid absolute URL";
+ return false;
+ }
+
+ // http is accepted for a custom endpoint only: pointing at one is an operator decision.
+ if (custom.Scheme != Uri.UriSchemeHttps && custom.Scheme != Uri.UriSchemeHttp)
+ {
+ error = "The configured Feature Flags agentless URL must use HTTP or HTTPS";
+ return false;
+ }
+
+ if (custom.AbsolutePath is "" or "/")
+ {
+ custom = new UriBuilder(custom) { Path = DefaultPath }.Uri;
+ }
+
+ endpoint = new AgentlessEndpoint(custom, isManaged: false);
+ return true;
+ }
+
+ ///
+ /// Returns the URI to request configuration for . The environment is
+ /// added as a query parameter rather than baked into the endpoint, because it can change while
+ /// the application runs.
+ ///
+ /// A configured base URL is opaque: it is requested exactly as the operator wrote it, since it
+ /// may hold credentials, routing, or a scope of its own. Only the managed endpoint carries
+ /// dd_env, which matches the other tracers.
+ ///
+ ///
+ /// The current environment, or null when none is configured.
+ /// The URI to request.
+ public Uri BuildRequestUri(string? env)
+ {
+ if (!IsManaged)
+ {
+ return Uri;
+ }
+
+ // Normalized the same way the tracer normalizes it before tagging spans, so that flag
+ // targeting and span tags agree on what the environment is. It also bounds the value at 200
+ // characters, which keeps a misconfigured environment from producing an unusable URL.
+ // A value that normalizes to nothing is treated as no environment at all.
+ var normalized = TraceUtil.NormalizeTag(env);
+ if (StringUtil.IsNullOrEmpty(normalized))
+ {
+ return Uri;
+ }
+
+ var parameter = EnvParameterName + "=" + Uri.EscapeDataString(normalized);
+ var builder = new UriBuilder(Uri);
+
+ // The getter returns the query with its leading "?", while the setter prepends one of its
+ // own on .NET Framework, so the existing query is trimmed before it is extended. A URL
+ // ending in a bare "?" reports a query of "?", which the length check treats as no query.
+ var existing = builder.Query;
+ builder.Query = existing.Length > 1 ? existing.TrimStart('?') + "&" + parameter : parameter;
+ return builder.Uri;
+ }
+}
diff --git a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs
new file mode 100644
index 000000000000..9bbb0aa7234b
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs
@@ -0,0 +1,202 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.Configuration.ConfigurationSources.Telemetry;
+using Datadog.Trace.Configuration.Telemetry;
+using Datadog.Trace.Logging;
+using Datadog.Trace.Util;
+
+namespace Datadog.Trace.FeatureFlags;
+
+///
+/// Feature Flags configuration: which delivery source is selected, and how the agentless
+/// source is operated.
+///
+internal sealed class FeatureFlagsSettings
+{
+ internal const string AgentlessSourceName = "agentless";
+ internal const string RemoteConfigSourceName = "remote_config";
+ internal const string OfflineSourceName = "offline";
+
+ internal const string DefaultSite = "datadoghq.com";
+
+ internal const int DefaultPollIntervalSeconds = 30;
+ internal const int DefaultRequestTimeoutSeconds = 5;
+ // Matches the Go, Java and Node tracers, so the same slow first configuration does not give
+ // one language the caller's default value while the others still return a real one.
+ internal const int DefaultInitializationTimeoutMs = 30_000;
+
+ // One hour. An interval above this is more likely a misconfiguration (for example milliseconds
+ // passed as seconds) than an intent, and the other tracers cap it at the same value.
+ private const int MaxPollIntervalSeconds = 3600;
+
+ private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(FeatureFlagsSettings));
+
+ public FeatureFlagsSettings(IConfigurationSource? source, IConfigurationTelemetry telemetry)
+ {
+ source ??= NullConfigurationSource.Instance;
+ var config = new ConfigurationBuilder(source, telemetry);
+
+ // Read as nullable: the precedence rules distinguish "explicitly provided" from "left unset",
+ // so a default value here would erase the difference the legacy key depends on.
+ var enabled = config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsEnabled).AsBool();
+#pragma warning disable 618 // superseded, but still honoured so existing adopters keep their source
+ var legacyEnabled = config.WithKeys(ConfigurationKeys.FeatureFlags.FlaggingProviderEnabled).AsBool();
+#pragma warning restore 618
+
+ if (legacyEnabled is not null)
+ {
+#pragma warning disable 618
+ Log.Warning(
+ "{LegacyKey} is deprecated. Use {EnabledKey} and {SourceKey} instead.",
+ ConfigurationKeys.FeatureFlags.FlaggingProviderEnabled,
+ ConfigurationKeys.FeatureFlags.FeatureFlagsEnabled,
+ ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSource);
+#pragma warning restore 618
+ }
+
+ // Where configuration comes from is a separate question from whether the product runs, and
+ // the two are answered separately below, as the other tracers answer them.
+ //
+ // The source key is read once, with every other outcome expressed as its default, so
+ // configuration telemetry reports the one value we act on rather than one entry per
+ // candidate key. Shared across tracers, so the precedence is deliberate: an explicit source
+ // wins over the legacy key, the legacy key grandfathers existing adopters onto Remote
+ // Configuration, and everything else defaults to agentless.
+ // net461 has no System.ValueTuple, so a tuple pattern over both values does not compile.
+ DefaultResult defaultSource = enabled switch
+ {
+ null when legacyEnabled is not null => legacyEnabled.Value
+ ? new(FeatureFlagsSource.RemoteConfig, RemoteConfigSourceName)
+ : new(FeatureFlagsSource.Offline, OfflineSourceName),
+ _ => new(FeatureFlagsSource.Agentless, AgentlessSourceName),
+ };
+
+ Source = config
+ .WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSource)
+ .GetAs(defaultSource, validator: null, converter: ConvertSource);
+
+ // The stable kill switch turns the product off whatever the source says. Beyond that, only a
+ // delivery source has anything to run: offline delivers nothing, and an unrecognised value
+ // resolves to offline so that a typo fails closed instead of starting billed delivery.
+ Enabled = enabled != false
+ && Source is FeatureFlagsSource.Agentless or FeatureFlagsSource.RemoteConfig;
+
+ var agentlessBaseUrl = config
+ .WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessBaseUrl)
+ .AsRedactedString();
+ AgentlessBaseUrl = !StringUtil.IsNullOrWhiteSpace(agentlessBaseUrl) ? agentlessBaseUrl : null;
+
+ PollInterval = TimeSpan.FromSeconds(
+ config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds)
+ .AsInt32(DefaultPollIntervalSeconds, v => v > 0 && v <= MaxPollIntervalSeconds)
+ .Value);
+
+ RequestTimeout = TimeSpan.FromSeconds(
+ config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds)
+ .AsInt32(DefaultRequestTimeoutSeconds, v => v > 0)
+ .Value);
+
+ // DD_SITE and DD_API_KEY are not extracted on TracerSettings, so they are read here as the
+ // other product settings do (TelemetrySettings, DirectLogSubmissionSettings). DD_ENV is not
+ // read at all: it can be changed in code while the application runs, so the delivery source
+ // takes the current value per request rather than capturing one here.
+ Site = config
+ .WithKeys(ConfigurationKeys.Site)
+ .AsString(DefaultSite, static site => !StringUtil.IsNullOrWhiteSpace(site));
+ ApiKey = config.WithKeys(ConfigurationKeys.ApiKey).AsRedactedString();
+
+ var initializationTimeoutMs = config
+ .WithKeys(ConfigurationKeys.FeatureFlags.FlaggingProviderInitializationTimeoutMs)
+ .AsInt32(DefaultInitializationTimeoutMs, timeout => timeout > 0)
+ .Value;
+ InitializationTimeout = TimeSpan.FromMilliseconds(initializationTimeoutMs);
+ }
+
+ ///
+ /// Gets the resolved delivery source. means nothing is contacted.
+ ///
+ public FeatureFlagsSource Source { get; }
+
+ ///
+ /// Gets a value indicating whether Feature Flags run at all. Independent of ,
+ /// which says where configuration would come from.
+ ///
+ public bool Enabled { get; }
+
+ ///
+ /// Gets the configured override for the agentless endpoint, or null to derive it from the site.
+ ///
+ public string? AgentlessBaseUrl { get; }
+
+ ///
+ /// Gets the Datadog site the managed agentless endpoint is derived from.
+ ///
+ public string Site { get; }
+
+ ///
+ /// Gets the API key, required by the managed agentless endpoint.
+ ///
+ public string? ApiKey { get; }
+
+ ///
+ /// Gets how often the agentless source polls for configuration.
+ ///
+ public TimeSpan PollInterval { get; }
+
+ ///
+ /// Gets the per-request timeout used by the agentless source.
+ ///
+ public TimeSpan RequestTimeout { get; }
+
+ ///
+ /// Gets how long provider initialization waits for the first configuration.
+ ///
+ public TimeSpan InitializationTimeout { get; }
+
+ ///
+ /// Converts a configured source name to a . A blank value is
+ /// treated as unset and falls back to the default, which is what an absent key would have
+ /// selected anyway. An unrecognised value fails closed instead: nothing is contacted.
+ ///
+ private static ParsingResult ConvertSource(string? value)
+ {
+ if (StringUtil.IsNullOrWhiteSpace(value))
+ {
+ return ParsingResult.Failure();
+ }
+
+ var trimmed = value.Trim();
+
+ if (string.Equals(trimmed, AgentlessSourceName, StringComparison.OrdinalIgnoreCase))
+ {
+ return ParsingResult.Success(FeatureFlagsSource.Agentless);
+ }
+
+ if (string.Equals(trimmed, RemoteConfigSourceName, StringComparison.OrdinalIgnoreCase))
+ {
+ return ParsingResult.Success(FeatureFlagsSource.RemoteConfig);
+ }
+
+ // "offline" is a reserved sentinel: the provider is intentionally off.
+ if (string.Equals(trimmed, OfflineSourceName, StringComparison.OrdinalIgnoreCase))
+ {
+ return ParsingResult.Success(FeatureFlagsSource.Offline);
+ }
+
+ // A value nobody recognises fails closed rather than falling back to agentless: guessing a
+ // billed delivery path from a typo is worse than delivering nothing. Shared across tracers,
+ // and asserted by the system-tests parametric suite.
+ Log.Warning(
+ "Unsupported Feature Flags configuration source {Source}. No configuration will be delivered.",
+ value);
+
+ return ParsingResult.Success(FeatureFlagsSource.Offline);
+ }
+}
diff --git a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs
new file mode 100644
index 000000000000..3c85ae61433f
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs
@@ -0,0 +1,30 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+namespace Datadog.Trace.FeatureFlags;
+
+///
+/// Where flag configuration is loaded from.
+///
+internal enum FeatureFlagsSource
+{
+ ///
+ /// Selected by offline. Nothing is contacted and no configuration is loaded, so
+ /// evaluations return the caller's default value.
+ ///
+ Offline,
+
+ ///
+ /// Configuration is fetched over HTTP, without the Datadog Agent.
+ ///
+ Agentless,
+
+ ///
+ /// Configuration is delivered through the Datadog Agent's Remote Configuration.
+ ///
+ RemoteConfig,
+}
diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
index e3c49c08f659..288cd8f848eb 100644
--- a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
@@ -23,12 +23,66 @@ internal static class FeatureFlags
///
public const string FlaggingProviderEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED";
+ ///
+ /// Configuration key for how long, in milliseconds, provider initialization waits for the first
+ /// flag configuration to arrive before returning.
+ /// Default value is 30000 (30 seconds), matching the other tracers.
+ /// Initialization does not fail when the timeout expires: the provider stays not-ready, evaluations
+ /// return the caller's default value, and the provider becomes ready when configuration arrives.
+ ///
+ public const string FlaggingProviderInitializationTimeoutMs = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS";
+
///
/// Enables APM span enrichment with feature-flag evaluation metadata (Experimental).
/// Default value is false (disabled).
///
public const string SpanEnrichmentEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED";
+ ///
+ /// Configuration key for selecting where flag configuration is loaded from.
+ /// Supported values are agentless (direct HTTP delivery, the default),
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration)
+ /// and offline, which disables Feature Flags and contacts nothing.
+ /// Any other value is rejected and the default applies.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// If unset, the endpoint is derived from and Datadog hosts it.
+ /// A configured URL is treated as an endpoint of your own, which changes three things:
+ /// the Datadog API key is not sent to it, so it is responsible for its own authentication;
+ /// it is requested exactly as written, so dd_env is not added and any environment or tenant
+ /// scope has to be part of the URL you configure;
+ /// and only its path is completed, when it has none or a path of /, with the standard
+ /// rules-based server path. Any other path is used verbatim as the exact endpoint.
+ /// The value may carry credentials, so it is never written to logs or telemetry.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessBaseUrl = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL";
+
+ ///
+ /// Configuration key for how often, in seconds, the agentless configuration source polls for
+ /// flag configuration.
+ /// Default value is 30. Values outside (0, 3600] are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS";
+
+ ///
+ /// Configuration key for the request timeout, in seconds, used by the agentless configuration
+ /// source.
+ /// Default value is 5. Non-positive values are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS";
+
+ ///
+ /// Configuration key to enable or disable Feature Flags.
+ /// Default value is true (enabled).
+ /// Feature Flags only contact Datadog once application code initializes the provider, so enabling
+ /// this alone does not start requesting flag configuration.
+ /// This supersedes .
+ ///
+ public const string FeatureFlagsEnabled = "DD_FEATURE_FLAGS_ENABLED";
+
///
/// Enables support for collecting and exporting logs generated by the the OpenTelemetry Logs API.
/// This feature is available starting with .NET 3.1 when using Microsoft.Extensions.Logging
diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
index e3c49c08f659..288cd8f848eb 100644
--- a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
@@ -23,12 +23,66 @@ internal static class FeatureFlags
///
public const string FlaggingProviderEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED";
+ ///
+ /// Configuration key for how long, in milliseconds, provider initialization waits for the first
+ /// flag configuration to arrive before returning.
+ /// Default value is 30000 (30 seconds), matching the other tracers.
+ /// Initialization does not fail when the timeout expires: the provider stays not-ready, evaluations
+ /// return the caller's default value, and the provider becomes ready when configuration arrives.
+ ///
+ public const string FlaggingProviderInitializationTimeoutMs = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS";
+
///
/// Enables APM span enrichment with feature-flag evaluation metadata (Experimental).
/// Default value is false (disabled).
///
public const string SpanEnrichmentEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED";
+ ///
+ /// Configuration key for selecting where flag configuration is loaded from.
+ /// Supported values are agentless (direct HTTP delivery, the default),
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration)
+ /// and offline, which disables Feature Flags and contacts nothing.
+ /// Any other value is rejected and the default applies.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// If unset, the endpoint is derived from and Datadog hosts it.
+ /// A configured URL is treated as an endpoint of your own, which changes three things:
+ /// the Datadog API key is not sent to it, so it is responsible for its own authentication;
+ /// it is requested exactly as written, so dd_env is not added and any environment or tenant
+ /// scope has to be part of the URL you configure;
+ /// and only its path is completed, when it has none or a path of /, with the standard
+ /// rules-based server path. Any other path is used verbatim as the exact endpoint.
+ /// The value may carry credentials, so it is never written to logs or telemetry.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessBaseUrl = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL";
+
+ ///
+ /// Configuration key for how often, in seconds, the agentless configuration source polls for
+ /// flag configuration.
+ /// Default value is 30. Values outside (0, 3600] are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS";
+
+ ///
+ /// Configuration key for the request timeout, in seconds, used by the agentless configuration
+ /// source.
+ /// Default value is 5. Non-positive values are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS";
+
+ ///
+ /// Configuration key to enable or disable Feature Flags.
+ /// Default value is true (enabled).
+ /// Feature Flags only contact Datadog once application code initializes the provider, so enabling
+ /// this alone does not start requesting flag configuration.
+ /// This supersedes .
+ ///
+ public const string FeatureFlagsEnabled = "DD_FEATURE_FLAGS_ENABLED";
+
///
/// Enables support for collecting and exporting logs generated by the the OpenTelemetry Logs API.
/// This feature is available starting with .NET 3.1 when using Microsoft.Extensions.Logging
diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
index e3c49c08f659..288cd8f848eb 100644
--- a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
@@ -23,12 +23,66 @@ internal static class FeatureFlags
///
public const string FlaggingProviderEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED";
+ ///
+ /// Configuration key for how long, in milliseconds, provider initialization waits for the first
+ /// flag configuration to arrive before returning.
+ /// Default value is 30000 (30 seconds), matching the other tracers.
+ /// Initialization does not fail when the timeout expires: the provider stays not-ready, evaluations
+ /// return the caller's default value, and the provider becomes ready when configuration arrives.
+ ///
+ public const string FlaggingProviderInitializationTimeoutMs = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS";
+
///
/// Enables APM span enrichment with feature-flag evaluation metadata (Experimental).
/// Default value is false (disabled).
///
public const string SpanEnrichmentEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED";
+ ///
+ /// Configuration key for selecting where flag configuration is loaded from.
+ /// Supported values are agentless (direct HTTP delivery, the default),
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration)
+ /// and offline, which disables Feature Flags and contacts nothing.
+ /// Any other value is rejected and the default applies.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// If unset, the endpoint is derived from and Datadog hosts it.
+ /// A configured URL is treated as an endpoint of your own, which changes three things:
+ /// the Datadog API key is not sent to it, so it is responsible for its own authentication;
+ /// it is requested exactly as written, so dd_env is not added and any environment or tenant
+ /// scope has to be part of the URL you configure;
+ /// and only its path is completed, when it has none or a path of /, with the standard
+ /// rules-based server path. Any other path is used verbatim as the exact endpoint.
+ /// The value may carry credentials, so it is never written to logs or telemetry.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessBaseUrl = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL";
+
+ ///
+ /// Configuration key for how often, in seconds, the agentless configuration source polls for
+ /// flag configuration.
+ /// Default value is 30. Values outside (0, 3600] are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS";
+
+ ///
+ /// Configuration key for the request timeout, in seconds, used by the agentless configuration
+ /// source.
+ /// Default value is 5. Non-positive values are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS";
+
+ ///
+ /// Configuration key to enable or disable Feature Flags.
+ /// Default value is true (enabled).
+ /// Feature Flags only contact Datadog once application code initializes the provider, so enabling
+ /// this alone does not start requesting flag configuration.
+ /// This supersedes .
+ ///
+ public const string FeatureFlagsEnabled = "DD_FEATURE_FLAGS_ENABLED";
+
///
/// Enables support for collecting and exporting logs generated by the the OpenTelemetry Logs API.
/// This feature is available starting with .NET 3.1 when using Microsoft.Extensions.Logging
diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
index e3c49c08f659..288cd8f848eb 100644
--- a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
+++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.FeatureFlags.g.cs
@@ -23,12 +23,66 @@ internal static class FeatureFlags
///
public const string FlaggingProviderEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED";
+ ///
+ /// Configuration key for how long, in milliseconds, provider initialization waits for the first
+ /// flag configuration to arrive before returning.
+ /// Default value is 30000 (30 seconds), matching the other tracers.
+ /// Initialization does not fail when the timeout expires: the provider stays not-ready, evaluations
+ /// return the caller's default value, and the provider becomes ready when configuration arrives.
+ ///
+ public const string FlaggingProviderInitializationTimeoutMs = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS";
+
///
/// Enables APM span enrichment with feature-flag evaluation metadata (Experimental).
/// Default value is false (disabled).
///
public const string SpanEnrichmentEnabled = "DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED";
+ ///
+ /// Configuration key for selecting where flag configuration is loaded from.
+ /// Supported values are agentless (direct HTTP delivery, the default),
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration)
+ /// and offline, which disables Feature Flags and contacts nothing.
+ /// Any other value is rejected and the default applies.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// If unset, the endpoint is derived from and Datadog hosts it.
+ /// A configured URL is treated as an endpoint of your own, which changes three things:
+ /// the Datadog API key is not sent to it, so it is responsible for its own authentication;
+ /// it is requested exactly as written, so dd_env is not added and any environment or tenant
+ /// scope has to be part of the URL you configure;
+ /// and only its path is completed, when it has none or a path of /, with the standard
+ /// rules-based server path. Any other path is used verbatim as the exact endpoint.
+ /// The value may carry credentials, so it is never written to logs or telemetry.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessBaseUrl = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL";
+
+ ///
+ /// Configuration key for how often, in seconds, the agentless configuration source polls for
+ /// flag configuration.
+ /// Default value is 30. Values outside (0, 3600] are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS";
+
+ ///
+ /// Configuration key for the request timeout, in seconds, used by the agentless configuration
+ /// source.
+ /// Default value is 5. Non-positive values are rejected and the default is used.
+ ///
+ public const string FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS";
+
+ ///
+ /// Configuration key to enable or disable Feature Flags.
+ /// Default value is true (enabled).
+ /// Feature Flags only contact Datadog once application code initializes the provider, so enabling
+ /// this alone does not start requesting flag configuration.
+ /// This supersedes .
+ ///
+ public const string FeatureFlagsEnabled = "DD_FEATURE_FLAGS_ENABLED";
+
///
/// Enables support for collecting and exporting logs generated by the the OpenTelemetry Logs API.
/// This feature is available starting with .NET 3.1 when using Microsoft.Extensions.Logging
diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/AgentlessEndpointTests.cs b/tracer/test/Datadog.Trace.Tests/FeatureFlags/AgentlessEndpointTests.cs
new file mode 100644
index 000000000000..6e024bfe7456
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/AgentlessEndpointTests.cs
@@ -0,0 +1,173 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using Datadog.Trace.FeatureFlags.Agentless;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.FeatureFlags;
+
+public class AgentlessEndpointTests
+{
+ private const string DefaultPath = "/api/v2/feature-flagging/config/rules-based/server";
+
+ [Theory]
+ [InlineData("datadoghq.com", "https://ufc-server.ff-cdn.datadoghq.com" + DefaultPath)]
+ [InlineData("DATADOGHQ.COM", "https://ufc-server.ff-cdn.datadoghq.com" + DefaultPath)] // site is lowercased
+ [InlineData("datad0g.com", "https://ufc-server.ff-cdn.datad0g.com" + DefaultPath)] // staging
+ [InlineData("ddog-gov.com", "https://ufc-server.ff-cdn.ddog-gov.com" + DefaultPath)] // govcloud
+ public void DerivesManagedEndpointFromSite(string site, string expected)
+ {
+ var endpoint = Create(site);
+
+ endpoint.IsManaged.Should().BeTrue();
+ endpoint.Uri.ToString().Should().Be(expected);
+ }
+
+ [Fact]
+ public void EndpointItselfCarriesNoEnvironment()
+ => Create("datadoghq.com").Uri.Query.Should().BeEmpty();
+
+ [Fact]
+ public void AddsDdEnvWhenEnvIsConfigured()
+ => Create("datadoghq.com").BuildRequestUri("production").Query.Should().Be("?dd_env=production");
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ // The tracer's tag normalization requires a leading letter, so a value that is entirely digits
+ // normalizes away. Spans are tagged the same way, so no environment is reported either.
+ [InlineData("2024")]
+ public void DoesNotAddDdEnvWhenEnvIsNotConfigured(string? env)
+ => Create("datadoghq.com").BuildRequestUri(env).Query.Should().BeEmpty();
+
+ [Theory]
+ // Normalized exactly as the tracer normalizes the environment before tagging spans: lowercased,
+ // with runs of unsupported characters collapsed to a single underscore.
+ [InlineData("Production", "production")]
+ [InlineData("Prod EU", "prod_eu")]
+ [InlineData(" staging ", "staging")]
+ [InlineData("my env&test", "my_env_test")]
+ public void NormalizesDdEnvValue(string env, string expected)
+ => Create("datadoghq.com").BuildRequestUri(env).Query.Should().Be("?dd_env=" + expected);
+
+ [Fact]
+ public void EscapesDdEnvValue()
+ // "/" survives normalization but cannot be carried unescaped in a query value.
+ => Create("datadoghq.com").BuildRequestUri("team/a").Query.Should().Be("?dd_env=team%2Fa");
+
+ [Fact]
+ public void TruncatesAnOverlongDdEnvValue()
+ => Create("datadoghq.com").BuildRequestUri(new string('a', 500))
+ .Query.Should().Be("?dd_env=" + new string('a', 200));
+
+ [Theory]
+ // A configured base URL is opaque: the operator may have scoped it themselves, and it may carry
+ // credentials or routing, so it is requested exactly as written. Java, JS, Go and Python all
+ // treat a custom endpoint the same way.
+ [InlineData("https://flags.example.com/ufc")]
+ [InlineData("https://flags.example.com/ufc?token=abc")]
+ [InlineData("https://flags.example.com/ufc?dd_env=staging")]
+ [InlineData("https://flags.example.com/ufc?token=abc&dd_env=staging")]
+ public void DoesNotAddDdEnvToACustomEndpoint(string baseUrl)
+ {
+ var endpoint = Create("datadoghq.com", baseUrl);
+
+ endpoint.BuildRequestUri("production").Should().Be(endpoint.Uri);
+ }
+
+ [Theory]
+ [InlineData("https://flags.example.com", "https://flags.example.com" + DefaultPath)]
+ [InlineData("https://flags.example.com/", "https://flags.example.com" + DefaultPath)]
+ [InlineData("https://flags.example.com/ufc", "https://flags.example.com/ufc")]
+ [InlineData("https://flags.example.com/ufc?custom=query", "https://flags.example.com/ufc?custom=query")]
+ public void CustomEndpointReceivesCanonicalPathForOriginOnly(string baseUrl, string expected)
+ {
+ var endpoint = Create("datadoghq.com", baseUrl: baseUrl);
+
+ endpoint.IsManaged.Should().BeFalse();
+ endpoint.Uri.ToString().Should().Be(expected);
+ }
+
+ [Theory]
+ [InlineData("http://localhost:8080/ufc")] // http accepted for custom endpoints
+ public void CustomEndpointAcceptsHttp(string baseUrl)
+ => Create("datadoghq.com", baseUrl: baseUrl).IsManaged.Should().BeFalse();
+
+ [Theory]
+ [InlineData("ftp://flags.example.com", "The configured Feature Flags agentless URL must use HTTP or HTTPS")]
+ [InlineData("notaurl", "The configured Feature Flags agentless URL is not a valid absolute URL")]
+ [InlineData("https://flags.example.com bad", "The configured Feature Flags agentless URL is not a valid URL")] // internal whitespace
+ public void RejectsInvalidBaseUrl(string baseUrl, string expectedError)
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", baseUrl: baseUrl, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be(expectedError);
+ endpoint.Should().BeNull();
+ }
+
+ [Fact]
+ public void RejectsEmptySiteWithoutBaseUrl()
+ {
+ AgentlessEndpoint.TryCreate(site: null, baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("No Datadog site is configured");
+ endpoint.Should().BeNull();
+ }
+
+ [Fact]
+ public void RejectsWhitespaceOnlySiteWithoutBaseUrl()
+ {
+ AgentlessEndpoint.TryCreate(" ", baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("No Datadog site is configured");
+ endpoint.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData("https://datadoghq.com")] // user accidentally includes the scheme
+ [InlineData("data dog hq.com")] // internal spaces
+ [InlineData("datadoghq.com:99999")] // invalid port
+ // "@" would end the userinfo and make the remainder the real host, so the request, and with it
+ // the API key, would go to a host the operator never named.
+ [InlineData("datadoghq.com@attacker.example")]
+ [InlineData("datadoghq.com/../evil")] // a path escapes the host
+ [InlineData("datadoghq.com?x=1")] // a query escapes the host
+ [InlineData("datadoghq.com#f")] // a fragment escapes the host
+ public void RejectsMalformedSiteWithoutThrowing(string site)
+ {
+ AgentlessEndpoint.TryCreate(site, baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("The configured Datadog site is not valid");
+ endpoint.Should().BeNull();
+ }
+
+ [Fact]
+ public void ErrorNeverContainsUrl()
+ {
+ // A URL may carry credentials, so the error must never echo it.
+ AgentlessEndpoint.TryCreate("datadoghq.com", baseUrl: "https://user:pass@flags.example.com bad", out _, out var error)
+ .Should().BeFalse();
+ error.Should().NotContain("user");
+ error.Should().NotContain("pass");
+ }
+
+ // Builds an endpoint that is expected to be valid, and returns it as non-nullable so the
+ // assertions can read it directly. Throwing rather than asserting keeps the compiler's nullable
+ // analysis satisfied without a null-forgiving operator, which would let an assertion be
+ // silently skipped if the endpoint were ever null.
+ private static AgentlessEndpoint Create(string? site, string? baseUrl = null)
+ {
+ AgentlessEndpoint.TryCreate(site, baseUrl, out var endpoint, out var error)
+ .Should().BeTrue();
+ error.Should().BeNull();
+
+ return endpoint ?? throw new InvalidOperationException("TryCreate reported success without producing an endpoint.");
+ }
+}
diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsSettingsTests.cs b/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsSettingsTests.cs
new file mode 100644
index 000000000000..79d67eedb464
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsSettingsTests.cs
@@ -0,0 +1,209 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using System.Collections.Specialized;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.Configuration.Telemetry;
+using Datadog.Trace.FeatureFlags;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.FeatureFlags;
+
+public class FeatureFlagsSettingsTests
+{
+ // The source-selection contract is shared across tracers, so these cases mirror the
+ // system-tests parametric suite (tests/parametric/test_ffe/test_configuration_sources.py).
+ // Where configuration would come from and whether Feature Flags run are asserted separately:
+ // the kill switch turns the product off without changing the source it would have used.
+ [Theory]
+ // Nothing configured: agentless is the default.
+ [InlineData(null, null, null, FeatureFlagsSource.Agentless, true)]
+ // The stable kill switch wins over everything, including a legacy opt-in and an explicit source.
+ [InlineData("false", null, null, FeatureFlagsSource.Agentless, false)]
+ [InlineData("false", null, "true", FeatureFlagsSource.Agentless, false)]
+ [InlineData("false", "agentless", null, FeatureFlagsSource.Agentless, false)]
+ [InlineData("false", "remote_config", null, FeatureFlagsSource.RemoteConfig, false)]
+ // Enabling explicitly does not imply the historical Remote Configuration source.
+ [InlineData("true", null, null, FeatureFlagsSource.Agentless, true)]
+ // An explicit source wins over the legacy key, in both directions.
+ [InlineData(null, "agentless", "true", FeatureFlagsSource.Agentless, true)]
+ [InlineData(null, "remote_config", "false", FeatureFlagsSource.RemoteConfig, true)]
+ // The legacy key grandfathers existing adopters, who opted in when RC was the only source.
+ [InlineData(null, null, "true", FeatureFlagsSource.RemoteConfig, true)]
+ [InlineData(null, null, "false", FeatureFlagsSource.Offline, false)]
+ // An explicit new-key value takes precedence over the legacy key, so a stale legacy disable
+ // does not silently keep Feature Flags off during migration.
+ [InlineData("true", null, "false", FeatureFlagsSource.Agentless, true)]
+ [InlineData("true", null, "true", FeatureFlagsSource.Agentless, true)]
+ // An unrecognised source fails closed, and does so before the legacy key is considered: starting
+ // billed delivery off a typo is worse than delivering nothing. Java and JS resolve it the same
+ // way, and the system-tests parametric suite asserts no request is made.
+ [InlineData(null, "invalid", null, FeatureFlagsSource.Offline, false)]
+ [InlineData(null, "invalid", "true", FeatureFlagsSource.Offline, false)]
+ // "offline" is a recognised source that delivers nothing, so there is nothing to run yet.
+ [InlineData(null, "offline", null, FeatureFlagsSource.Offline, false)]
+ [InlineData(null, "offline", "true", FeatureFlagsSource.Offline, false)]
+ public void ResolvesSource(string? enabled, string? source, string? legacyEnabled, object expected, bool expectedEnabled)
+ {
+ var settings = CreateSettings(enabled, source, legacyEnabled);
+
+ settings.Source.Should().Be((FeatureFlagsSource)expected);
+ settings.Enabled.Should().Be(expectedEnabled);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void TreatsBlankSourceAsUnset(string source)
+ {
+ CreateSettings(enabled: null, source: source, legacyEnabled: null)
+ .Source.Should().Be(FeatureFlagsSource.Agentless);
+
+ // Being semantically unset, a blank source still lets the legacy key grandfather RC.
+ CreateSettings(enabled: null, source: source, legacyEnabled: "true")
+ .Source.Should().Be(FeatureFlagsSource.RemoteConfig);
+ }
+
+ [Theory]
+ [InlineData("AGENTLESS", FeatureFlagsSource.Agentless)]
+ [InlineData(" Remote_Config ", FeatureFlagsSource.RemoteConfig)]
+ public void NormalizesSourceCasingAndWhitespace(string source, object expected)
+ => CreateSettings(enabled: null, source: source, legacyEnabled: null).Source.Should().Be((FeatureFlagsSource)expected);
+
+ [Fact]
+ public void UsesDocumentedDefaults()
+ {
+ var settings = CreateSettings(null, null, null);
+
+ settings.PollInterval.Should().Be(TimeSpan.FromSeconds(30));
+ settings.RequestTimeout.Should().Be(TimeSpan.FromSeconds(5));
+ settings.InitializationTimeout.Should().Be(TimeSpan.FromMilliseconds(30_000));
+ settings.AgentlessBaseUrl.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData("60", 60)]
+ [InlineData("3600", 3600)]
+ // Out of range values are rejected in favour of the default: a non-positive interval would
+ // turn polling into a tight loop, and an implausibly large one is a misconfiguration.
+ [InlineData("0", 30)]
+ [InlineData("-1", 30)]
+ [InlineData("3601", 30)]
+ [InlineData("not-a-number", 30)]
+ public void ReadsPollInterval(string configured, int expectedSeconds)
+ {
+ var settings = CreateSettings(
+ null,
+ null,
+ null,
+ (ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds, configured));
+
+ settings.PollInterval.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
+ }
+
+ [Theory]
+ [InlineData("1", 1)]
+ [InlineData("0", 5)]
+ [InlineData("-2", 5)]
+ public void ReadsRequestTimeout(string configured, int expectedSeconds)
+ {
+ var settings = CreateSettings(
+ null,
+ null,
+ null,
+ (ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds, configured));
+
+ settings.RequestTimeout.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
+ }
+
+ [Theory]
+ [InlineData("1000", 1000)]
+ [InlineData("0", 30_000)]
+ [InlineData("-1", 30_000)]
+ public void ReadsInitializationTimeout(string configured, int expectedMs)
+ {
+ var settings = CreateSettings(
+ null,
+ null,
+ null,
+ (ConfigurationKeys.FeatureFlags.FlaggingProviderInitializationTimeoutMs, configured));
+
+ settings.InitializationTimeout.Should().Be(TimeSpan.FromMilliseconds(expectedMs));
+ }
+
+ [Theory]
+ [InlineData("https://flags.example.com/ufc", "https://flags.example.com/ufc")]
+ [InlineData("", null)]
+ [InlineData(" ", null)]
+ public void ReadsAgentlessBaseUrl(string configured, string? expected)
+ {
+ var settings = CreateSettings(
+ null,
+ null,
+ null,
+ (ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessBaseUrl, configured));
+
+ settings.AgentlessBaseUrl.Should().Be(expected);
+ }
+
+ [Fact]
+ public void ReadsSiteAndApiKeyFromTheConfigurationSource()
+ {
+ var settings = CreateSettings(
+ null,
+ null,
+ null,
+ (ConfigurationKeys.Site, "datadoghq.eu"),
+ (ConfigurationKeys.ApiKey, "an-api-key"));
+
+ settings.Site.Should().Be("datadoghq.eu");
+ settings.ApiKey.Should().Be("an-api-key");
+ }
+
+ [Theory]
+ // A blank site is rejected in favour of the default, so the managed endpoint stays resolvable.
+ [InlineData("")]
+ [InlineData(" ")]
+ public void FallsBackToTheDefaultSite(string configured)
+ => CreateSettings(null, null, null, (ConfigurationKeys.Site, configured))
+ .Site.Should().Be(FeatureFlagsSettings.DefaultSite);
+
+ private static FeatureFlagsSettings CreateSettings(
+ string? enabled,
+ string? source,
+ string? legacyEnabled,
+ params (string Key, string Value)[] extra)
+ {
+ var collection = new NameValueCollection();
+
+ if (enabled is not null)
+ {
+ collection[ConfigurationKeys.FeatureFlags.FeatureFlagsEnabled] = enabled;
+ }
+
+ if (source is not null)
+ {
+ collection[ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSource] = source;
+ }
+
+ if (legacyEnabled is not null)
+ {
+#pragma warning disable 618 // superseded, but still honoured for existing adopters
+ collection[ConfigurationKeys.FeatureFlags.FlaggingProviderEnabled] = legacyEnabled;
+#pragma warning restore 618
+ }
+
+ foreach (var (key, value) in extra)
+ {
+ collection[key] = value;
+ }
+
+ return new FeatureFlagsSettings(new NameValueConfigurationSource(collection), NullConfigurationTelemetry.Instance);
+ }
+}