diff --git a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
index 3342c5173299..cef99aae2bcf 100644
--- a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
+++ b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml
@@ -3147,6 +3147,78 @@ supportedConfigurations:
documentation: |-
Enables Feature Flags Provider (Experimental).
Default value is false (disabled).
+ DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS:
+ - implementation: B
+ scope: managed
+ type: int
+ default: '10000'
+ 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 10000 (10 seconds).
+ 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) and
+ remote_config (delivery through the Datadog Agent's Remote Configuration).
+ Any other value disables Feature Flags, which fails closed and contacts nothing.
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL:
+ - implementation: A
+ scope: managed
+ type: string
+ default: null
+ product: FeatureFlags
+ const_name: FeatureFlagsConfigurationSourceAgentlessBaseUrl
+ documentation: |-
+ Configuration key for overriding the endpoint used by the agentless configuration source.
+ When the URL has no path, or a path of /, the standard rules-based server path is appended;
+ any other path is used verbatim as the exact endpoint.
+ If unset, the endpoint is derived from .
+ 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..e0f60f1c4e9d
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/Agentless/AgentlessEndpoint.cs
@@ -0,0 +1,141 @@
+//
+// 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.Util;
+
+namespace Datadog.Trace.FeatureFlags.Agentless;
+
+///
+/// The agentless endpoint, derived from the Datadog site or a custom base URL.
+///
+internal readonly struct 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.";
+
+ private AgentlessEndpoint(Uri uri, bool isManaged)
+ {
+ Uri = uri;
+ IsManaged = isManaged;
+ }
+
+ ///
+ /// Gets the endpoint URI.
+ ///
+ 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 guessed onto 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 dd_env is added only when an environment is configured.
+ /// A custom base URL that is an origin receives the canonical path; one that carries a path
+ /// is used verbatim.
+ ///
+ /// The Datadog site, for example datadoghq.com.
+ /// The configured environment, or null.
+ /// The configured endpoint override, or null.
+ /// The resulting endpoint.
+ /// 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? env, string? baseUrl, out AgentlessEndpoint endpoint, out string? error)
+ {
+ endpoint = default;
+ 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;
+ }
+
+ var managedHost = ManagedHostPrefix + trimmedSite!.ToLowerInvariant();
+ if (managedHost.Contains("://") || HasWhitespace(managedHost))
+ {
+ error = "The configured Datadog site is not valid";
+ return false;
+ }
+
+ if (!Uri.TryCreate($"https://{managedHost}{DefaultPath}", UriKind.Absolute, out var managedUri))
+ {
+ error = "The configured Datadog site is not valid";
+ return false;
+ }
+
+ if (!StringUtil.IsNullOrEmpty(env))
+ {
+ managedUri = new UriBuilder(managedUri) { Query = "dd_env=" + Uri.EscapeDataString(env!) }.Uri;
+ }
+
+ endpoint = new AgentlessEndpoint(managedUri, isManaged: true);
+ return true;
+ }
+
+ // A URL with internal whitespace is malformed, and Uri parsing is lenient enough to accept it.
+ 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;
+ }
+
+ private static bool HasWhitespace(string value)
+ {
+ foreach (var c in value)
+ {
+ if (char.IsWhiteSpace(c))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs
new file mode 100644
index 000000000000..e25c97fd0dfa
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSettings.cs
@@ -0,0 +1,229 @@
+//
+// 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.Telemetry;
+using Datadog.Trace.Logging;
+using Datadog.Trace.Telemetry;
+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;
+ internal const int DefaultInitializationTimeoutMs = 10_000;
+
+ // An interval above this is indistinguishable from "never poll" and is more likely a
+ // misconfiguration (for example milliseconds passed as seconds) than an intent.
+ 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
+ var configuredSource = config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSource).AsString();
+
+ 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
+ }
+
+ Source = ResolveSource(enabled, configuredSource, legacyEnabled);
+
+ var agentlessBaseUrl = config
+ .WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessBaseUrl)
+ .AsRedactedString();
+ AgentlessBaseUrl = !StringUtil.IsNullOrEmpty(agentlessBaseUrl?.Trim()) ? agentlessBaseUrl : null;
+
+ PollInterval = TimeSpan.FromSeconds(
+ InRangeOrDefault(
+ config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds).AsInt32(),
+ ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessPollIntervalSeconds,
+ DefaultPollIntervalSeconds,
+ MaxPollIntervalSeconds));
+
+ RequestTimeout = TimeSpan.FromSeconds(
+ InRangeOrDefault(
+ config.WithKeys(ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds).AsInt32(),
+ ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSourceAgentlessRequestTimeoutSeconds,
+ DefaultRequestTimeoutSeconds,
+ maximumSeconds: null));
+
+ Site = config
+ .WithKeys(ConfigurationKeys.Site)
+ .AsString(DefaultSite, site => !StringUtil.IsNullOrEmpty(site?.Trim()));
+
+ Env = config.WithKeys(ConfigurationKeys.Environment).AsString();
+
+ 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 are enabled at all.
+ ///
+ public bool Enabled => Source != FeatureFlagsSource.Disabled;
+
+ ///
+ /// 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 configured environment, sent to the agentless endpoint as dd_env.
+ ///
+ public string? Env { 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; }
+
+ public static FeatureFlagsSettings FromDefaultSource()
+ => new(GlobalConfigurationSource.Instance, TelemetryFactory.Config);
+
+ ///
+ /// Resolves the delivery source. Shared across tracers, so the ordering is deliberate:
+ /// the stable kill switch wins over everything, an explicit source wins over the legacy key
+ /// (and fails closed when unrecognised), the legacy key grandfathers existing adopters onto
+ /// Remote Configuration, and everything else defaults to agentless.
+ ///
+ internal static FeatureFlagsSource ResolveSource(bool? enabled, string? configuredSource, bool? legacyEnabled)
+ {
+ var normalizedSource = NormalizeSource(configuredSource);
+
+ if (enabled == false)
+ {
+ return FeatureFlagsSource.Disabled;
+ }
+
+ if (normalizedSource is not null)
+ {
+ switch (normalizedSource)
+ {
+ case AgentlessSourceName:
+ return FeatureFlagsSource.Agentless;
+ case RemoteConfigSourceName:
+ return FeatureFlagsSource.RemoteConfig;
+ case OfflineSourceName:
+ // Reserved fail-closed sentinel: the provider is intentionally off, so no warning.
+ return FeatureFlagsSource.Disabled;
+ default:
+ Log.Warning(
+ "Unsupported {SourceKey} value '{Source}'. Feature Flags are disabled.",
+ ConfigurationKeys.FeatureFlags.FeatureFlagsConfigurationSource,
+ normalizedSource);
+ return FeatureFlagsSource.Disabled;
+ }
+ }
+
+ // The legacy key only grandfathers adopters who have not migrated: an explicit new-key
+ // value (true or false) takes precedence, so the legacy key is consulted only when the
+ // new key was left unset.
+ if (enabled is null && legacyEnabled is not null)
+ {
+ return legacyEnabled.Value ? FeatureFlagsSource.RemoteConfig : FeatureFlagsSource.Disabled;
+ }
+
+ return FeatureFlagsSource.Agentless;
+ }
+
+ ///
+ /// An empty or whitespace-only source is semantically unset, not an unrecognised value.
+ ///
+ private static string? NormalizeSource(string? configuredSource)
+ {
+ if (configuredSource is null)
+ {
+ return null;
+ }
+
+ var normalized = configuredSource.Trim().ToLowerInvariant();
+ return normalized.Length == 0 ? null : normalized;
+ }
+
+ internal static int InRangeOrDefault(int? configured, string key, int defaultSeconds, int? maximumSeconds)
+ {
+ if (configured is null)
+ {
+ return defaultSeconds;
+ }
+
+ var value = configured.Value;
+ if (value <= 0 || (maximumSeconds is { } maximum && value > maximum))
+ {
+ // A non-positive interval would turn polling into a tight loop against the endpoint,
+ // so an out-of-range value is rejected rather than honoured.
+ Log.Warning(
+ "Invalid value {Key}={Value}. Using {Default} seconds instead.",
+ key,
+ value,
+ defaultSeconds);
+ return defaultSeconds;
+ }
+
+ return value;
+ }
+}
diff --git a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs
new file mode 100644
index 000000000000..dd50047ede42
--- /dev/null
+++ b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsSource.cs
@@ -0,0 +1,29 @@
+//
+// 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
+{
+ ///
+ /// Feature Flags are disabled: no configuration is loaded, and neither delivery path is contacted.
+ ///
+ Disabled,
+
+ ///
+ /// 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..c0bc6ef9a199 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,60 @@ 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 10000 (10 seconds).
+ /// 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) and
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration).
+ /// Any other value disables Feature Flags, which fails closed and contacts nothing.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// When the URL has no path, or a path of /, the standard rules-based server path is appended;
+ /// any other path is used verbatim as the exact endpoint.
+ /// If unset, the endpoint is derived from .
+ ///
+ 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..c0bc6ef9a199 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,60 @@ 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 10000 (10 seconds).
+ /// 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) and
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration).
+ /// Any other value disables Feature Flags, which fails closed and contacts nothing.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// When the URL has no path, or a path of /, the standard rules-based server path is appended;
+ /// any other path is used verbatim as the exact endpoint.
+ /// If unset, the endpoint is derived from .
+ ///
+ 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..c0bc6ef9a199 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,60 @@ 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 10000 (10 seconds).
+ /// 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) and
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration).
+ /// Any other value disables Feature Flags, which fails closed and contacts nothing.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// When the URL has no path, or a path of /, the standard rules-based server path is appended;
+ /// any other path is used verbatim as the exact endpoint.
+ /// If unset, the endpoint is derived from .
+ ///
+ 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..c0bc6ef9a199 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,60 @@ 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 10000 (10 seconds).
+ /// 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) and
+ /// remote_config (delivery through the Datadog Agent's Remote Configuration).
+ /// Any other value disables Feature Flags, which fails closed and contacts nothing.
+ ///
+ public const string FeatureFlagsConfigurationSource = "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE";
+
+ ///
+ /// Configuration key for overriding the endpoint used by the agentless configuration source.
+ /// When the URL has no path, or a path of /, the standard rules-based server path is appended;
+ /// any other path is used verbatim as the exact endpoint.
+ /// If unset, the endpoint is derived from .
+ ///
+ 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..3a737c8937ff
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/AgentlessEndpointTests.cs
@@ -0,0 +1,127 @@
+//
+// 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)
+ {
+ AgentlessEndpoint.TryCreate(site, env: null, baseUrl: null, out var endpoint, out var error)
+ .Should().BeTrue();
+ error.Should().BeNull();
+ endpoint.IsManaged.Should().BeTrue();
+ endpoint.Uri.ToString().Should().Be(expected);
+ }
+
+ [Fact]
+ public void AddsDdEnvWhenEnvIsConfigured()
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: "production", baseUrl: null, out var endpoint, out _)
+ .Should().BeTrue();
+ endpoint.Uri.Query.Should().Be("?dd_env=production");
+ }
+
+ [Fact]
+ public void DoesNotAddDdEnvWhenEnvIsNull()
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: null, baseUrl: null, out var endpoint, out _)
+ .Should().BeTrue();
+ endpoint.Uri.Query.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void EscapesDdEnvValue()
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: "my env&test", baseUrl: null, out var endpoint, out _)
+ .Should().BeTrue();
+ endpoint.Uri.Query.Should().Be("?dd_env=my%20env%26test");
+ }
+
+ [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)
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: null, baseUrl: baseUrl, out var endpoint, out var error)
+ .Should().BeTrue();
+ error.Should().BeNull();
+ 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)
+ {
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: null, baseUrl: baseUrl, out var endpoint, out var error)
+ .Should().BeTrue();
+ endpoint.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", env: null, baseUrl: baseUrl, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be(expectedError);
+ }
+
+ [Fact]
+ public void RejectsEmptySiteWithoutBaseUrl()
+ {
+ AgentlessEndpoint.TryCreate(site: null, env: null, baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("No Datadog site is configured");
+ }
+
+ [Fact]
+ public void RejectsWhitespaceOnlySiteWithoutBaseUrl()
+ {
+ AgentlessEndpoint.TryCreate(" ", env: null, baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("No Datadog site is configured");
+ }
+
+ [Theory]
+ [InlineData("https://datadoghq.com")] // user accidentally includes the scheme
+ [InlineData("data dog hq.com")] // internal spaces
+ [InlineData("datadoghq.com:99999")] // invalid port
+ public void RejectsMalformedSiteWithoutThrowing(string site)
+ {
+ AgentlessEndpoint.TryCreate(site, env: null, baseUrl: null, out var endpoint, out var error)
+ .Should().BeFalse();
+ error.Should().Be("The configured Datadog site is not valid");
+ }
+
+ [Fact]
+ public void ErrorNeverContainsUrl()
+ {
+ // A URL may carry credentials, so the error must never echo it.
+ AgentlessEndpoint.TryCreate("datadoghq.com", env: null, baseUrl: "https://user:pass@flags.example.com bad", out _, out var error)
+ .Should().BeFalse();
+ error.Should().NotContain("user");
+ error.Should().NotContain("pass");
+ }
+}
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..ec8be148a134
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsSettingsTests.cs
@@ -0,0 +1,184 @@
+//
+// 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).
+ [Theory]
+ // Nothing configured: agentless is the default.
+ [InlineData(null, null, null, FeatureFlagsSource.Agentless)]
+ // The stable kill switch wins over everything, including a legacy opt-in and an explicit source.
+ [InlineData("false", null, null, FeatureFlagsSource.Disabled)]
+ [InlineData("false", null, "true", FeatureFlagsSource.Disabled)]
+ [InlineData("false", "agentless", null, FeatureFlagsSource.Disabled)]
+ [InlineData("false", "remote_config", null, FeatureFlagsSource.Disabled)]
+ // Enabling explicitly does not imply the historical Remote Configuration source.
+ [InlineData("true", null, null, FeatureFlagsSource.Agentless)]
+ // An explicit source wins over the legacy key, in both directions.
+ [InlineData(null, "agentless", "true", FeatureFlagsSource.Agentless)]
+ [InlineData(null, "remote_config", "false", FeatureFlagsSource.RemoteConfig)]
+ // The legacy key grandfathers existing adopters, who opted in when RC was the only source.
+ [InlineData(null, null, "true", FeatureFlagsSource.RemoteConfig)]
+ [InlineData(null, null, "false", FeatureFlagsSource.Disabled)]
+ // 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)]
+ [InlineData("true", null, "true", FeatureFlagsSource.Agentless)]
+ // An unrecognised source fails closed rather than guessing a billed delivery path.
+ [InlineData(null, "invalid", null, FeatureFlagsSource.Disabled)]
+ [InlineData(null, "invalid", "true", FeatureFlagsSource.Disabled)]
+ // "offline" is a reserved, recognised fail-closed sentinel (not an unrecognised value).
+ [InlineData(null, "offline", null, FeatureFlagsSource.Disabled)]
+ [InlineData(null, "offline", "true", FeatureFlagsSource.Disabled)]
+ public void ResolvesSource(string? enabled, string? source, string? legacyEnabled, object expected)
+ {
+ var expectedSource = (FeatureFlagsSource)expected;
+ var settings = CreateSettings(enabled, source, legacyEnabled);
+
+ settings.Source.Should().Be(expectedSource);
+ settings.Enabled.Should().Be(expectedSource != FeatureFlagsSource.Disabled);
+ }
+
+ [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(10_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", 10_000)]
+ [InlineData("-1", 10_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);
+ }
+
+ 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);
+ }
+}