diff --git a/contract-tests/Representations.cs b/contract-tests/Representations.cs
index 56199496..1836c4dd 100644
--- a/contract-tests/Representations.cs
+++ b/contract-tests/Representations.cs
@@ -77,6 +77,7 @@ public class SdkClientSideParams
public Context? InitialContext { get; set; }
public User InitialUser { get; set; }
public bool? UseReport { get; set; }
+ public bool? IncludeEnvironmentAttributes { get; set; }
}
public class CommandParams
diff --git a/contract-tests/SdkClientEntity.cs b/contract-tests/SdkClientEntity.cs
index 81317bef..ec54e4b2 100644
--- a/contract-tests/SdkClientEntity.cs
+++ b/contract-tests/SdkClientEntity.cs
@@ -266,7 +266,11 @@ private ContextBuildResponse DoContextConvert(ContextConvertParams p)
private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapter logAdapter, string tag)
{
- var builder = Configuration.Builder(sdkParams.Credential);
+ var autoEnvAttributes = (sdkParams.ClientSide.IncludeEnvironmentAttributes ?? false)
+ ? ConfigurationBuilder.AutoEnvAttributes.Enabled
+ : ConfigurationBuilder.AutoEnvAttributes.Disabled;
+
+ var builder = Configuration.Builder(sdkParams.Credential, autoEnvAttributes);
builder.Logging(Components.Logging(logAdapter).BaseLoggerName(tag + ".SDK"));
diff --git a/contract-tests/TestService.cs b/contract-tests/TestService.cs
index c485fc8b..59940839 100644
--- a/contract-tests/TestService.cs
+++ b/contract-tests/TestService.cs
@@ -37,7 +37,8 @@ public class Webapp
"singleton",
"strongly-typed",
"user-type",
- "tags"
+ "tags",
+ "auto-env-attributes"
};
public readonly Handler Handler;
@@ -54,7 +55,7 @@ public Webapp(EventWaitHandle quitSignal)
_quitSignal = quitSignal;
_version = LdClient.Version.ToString();
-
+
var service = new SimpleJsonService();
Handler = service.Handler;
diff --git a/src/LaunchDarkly.ClientSdk/Configuration.cs b/src/LaunchDarkly.ClientSdk/Configuration.cs
index 319cf400..9b457674 100644
--- a/src/LaunchDarkly.ClientSdk/Configuration.cs
+++ b/src/LaunchDarkly.ClientSdk/Configuration.cs
@@ -1,21 +1,34 @@
using System;
+using System.Collections.Immutable;
using LaunchDarkly.Sdk.Client.Integrations;
using LaunchDarkly.Sdk.Client.Interfaces;
using LaunchDarkly.Sdk.Client.Internal.Interfaces;
+using LaunchDarkly.Sdk.Client.PlatformSpecific;
using LaunchDarkly.Sdk.Client.Subsystems;
namespace LaunchDarkly.Sdk.Client
{
///
- /// Configuration options for .
+ /// Configuration options for .
///
///
/// Instances of are immutable once created. They can be created with the factory method
- /// , or using a builder pattern with
- /// or .
+ /// , or using a builder pattern
+ /// with or .
///
public sealed class Configuration
{
+ ///
+ /// ApplicationInfo configuration which contains info about the application the SDK is running in.
+ ///
+ public ApplicationInfoBuilder ApplicationInfo { get; }
+
+ ///
+ /// True if Auto Environment Attributes functionality is enabled. When enabled, the SDK will automatically
+ /// provide data about the environment where the application is running.
+ ///
+ public bool AutoEnvAttributes { get; }
+
///
/// Default value for and
/// .
@@ -83,13 +96,13 @@ public sealed class Configuration
///
/// HTTP configuration properties for the SDK.
///
- ///
+ ///
public HttpConfigurationBuilder HttpConfigurationBuilder { get; }
///
/// Logging configuration properties for the SDK.
///
- ///
+ ///
public LoggingConfigurationBuilder LoggingConfigurationBuilder { get; }
///
@@ -109,7 +122,7 @@ public sealed class Configuration
///
/// Persistent storage configuration properties for the SDK.
///
- ///
+ ///
public PersistenceConfigurationBuilder PersistenceConfigurationBuilder { get; }
///
@@ -118,19 +131,21 @@ public sealed class Configuration
///
public ServiceEndpoints ServiceEndpoints { get; }
- ///
- /// ApplicationInfo configuration which contains info about the application the SDK is running in.
- ///
- public ApplicationInfoBuilder ApplicationInfo { get; }
-
///
/// Creates a configuration with all parameters set to the default.
///
/// the SDK key for your LaunchDarkly environment
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// a instance
- public static Configuration Default(string mobileKey)
+ public static Configuration Default(string mobileKey, ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes)
{
- return Builder(mobileKey).Build();
+ return Builder(mobileKey, autoEnvAttributes).Build();
}
///
@@ -151,14 +166,23 @@ public static Configuration Default(string mobileKey)
///
///
/// the mobile SDK key for your LaunchDarkly environment
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// a builder object
- public static ConfigurationBuilder Builder(string mobileKey)
+ public static ConfigurationBuilder Builder(string mobileKey,
+ ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes)
{
if (String.IsNullOrEmpty(mobileKey))
{
throw new ArgumentOutOfRangeException(nameof(mobileKey), "key is required");
}
- return new ConfigurationBuilder(mobileKey);
+
+ return new ConfigurationBuilder(mobileKey, autoEnvAttributes);
}
///
@@ -173,6 +197,8 @@ public static ConfigurationBuilder Builder(Configuration fromConfiguration)
internal Configuration(ConfigurationBuilder builder)
{
+ ApplicationInfo = builder._applicationInfo;
+ AutoEnvAttributes = builder._autoEnvAttributes;
DataSource = builder._dataSource;
DiagnosticOptOut = builder._diagnosticOptOut;
EnableBackgroundUpdating = builder._enableBackgroundUpdating;
@@ -185,7 +211,6 @@ internal Configuration(ConfigurationBuilder builder)
Offline = builder._offline;
PersistenceConfigurationBuilder = builder._persistenceConfigurationBuilder;
ServiceEndpoints = (builder._serviceEndpointsBuilder ?? Components.ServiceEndpoints()).Build();
- ApplicationInfo = builder._applicationInfo;
BackgroundModeManager = builder._backgroundModeManager;
ConnectivityStateManager = builder._connectivityStateManager;
}
diff --git a/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs
index d7d05a45..7a3ed36e 100644
--- a/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs
+++ b/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs
@@ -11,7 +11,7 @@ namespace LaunchDarkly.Sdk.Client
///
///
///
- /// Obtain an instance of this class by calling .
+ /// Obtain an instance of this class by calling .
///
///
/// All of the builder methods for setting a configuration property return a reference to the same builder, so they can be
@@ -25,12 +25,39 @@ namespace LaunchDarkly.Sdk.Client
///
public sealed class ConfigurationBuilder
{
+ ///
+ /// Enable / disable options for Auto Environment Attributes functionality. When enabled, the SDK will automatically
+ /// provide data about the environment where the application is running. This data makes it simpler to target
+ /// your mobile customers based on application name or version, or on device characteristics including manufacturer,
+ /// model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. See
+ /// our documentation
+ /// for more details.
+ /// For example, consider a “dark mode” feature being added to an app. Versions 10 through 14 contain early,
+ /// incomplete versions of the feature. These versions are available to all customers, but the “dark mode” feature is only
+ /// enabled for testers. With version 15, the feature is considered complete. With Auto Environment Attributes enabled,
+ /// you can use targeting rules to enable "dark mode" for all customers who are using version 15 or greater, and ensure
+ /// that customers on previous versions don't use the earlier, unfinished version of the feature.
+ ///
+ public enum AutoEnvAttributes
+ {
+ ///
+ /// Enables the Auto EnvironmentAttributes functionality.
+ ///
+ Enabled,
+
+ ///
+ /// Disables the Auto EnvironmentAttributes functionality.
+ ///
+ Disabled
+ }
+
// This exists so that we can distinguish between leaving the HttpMessageHandler property unchanged
// and explicitly setting it to null. If the property value is the exact same instance as this, we
// will replace it with a platform-specific implementation.
internal static readonly HttpMessageHandler DefaultHttpMessageHandlerInstance = new HttpClientHandler();
internal ApplicationInfoBuilder _applicationInfo;
+ internal bool _autoEnvAttributes = false;
internal IComponentConfigurer _dataSource = null;
internal bool _diagnosticOptOut = false;
internal bool _enableBackgroundUpdating = true;
@@ -48,13 +75,16 @@ public sealed class ConfigurationBuilder
internal IBackgroundModeManager _backgroundModeManager;
internal IConnectivityStateManager _connectivityStateManager;
- internal ConfigurationBuilder(string mobileKey)
+ internal ConfigurationBuilder(string mobileKey, AutoEnvAttributes autoEnvAttributes)
{
_mobileKey = mobileKey;
+ _autoEnvAttributes = autoEnvAttributes == AutoEnvAttributes.Enabled; // map enum to boolean
}
internal ConfigurationBuilder(Configuration copyFrom)
{
+ _applicationInfo = copyFrom.ApplicationInfo;
+ _autoEnvAttributes = copyFrom.AutoEnvAttributes;
_dataSource = copyFrom.DataSource;
_diagnosticOptOut = copyFrom.DiagnosticOptOut;
_enableBackgroundUpdating = copyFrom.EnableBackgroundUpdating;
@@ -66,7 +96,6 @@ internal ConfigurationBuilder(Configuration copyFrom)
_offline = copyFrom.Offline;
_persistenceConfigurationBuilder = copyFrom.PersistenceConfigurationBuilder;
_serviceEndpointsBuilder = new ServiceEndpointsBuilder(copyFrom.ServiceEndpoints);
- _applicationInfo = copyFrom.ApplicationInfo;
}
///
@@ -78,7 +107,7 @@ public Configuration Build()
{
return new Configuration(this);
}
-
+
///
/// Sets the SDK's application metadata, which may be used in the LaunchDarkly analytics or other product
/// features. This object is normally a configuration builder obtained from ,
@@ -92,6 +121,23 @@ public ConfigurationBuilder ApplicationInfo(ApplicationInfoBuilder applicationIn
return this;
}
+ ///
+ /// Specifies whether the SDK will use Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
+ ///
+ /// Enable / disable Auto Environment Attributes functionality.
+ /// the same builder
+ public ConfigurationBuilder AutoEnvironmentAttributes(AutoEnvAttributes autoEnvAttributes)
+ {
+ _autoEnvAttributes = autoEnvAttributes == AutoEnvAttributes.Enabled; // map enum to boolean
+ return this;
+ }
+
///
/// Sets the implementation of the component that receives feature flag data from LaunchDarkly,
/// using a factory object.
@@ -205,7 +251,7 @@ public ConfigurationBuilder Events(IComponentConfigurer eventsC
///
///
/// If enabled, this option changes the SDK's behavior whenever the (as given to
- /// methods like or
+ /// methods like or
/// ) has an
/// property of , as follows:
///
diff --git a/src/LaunchDarkly.ClientSdk/Integrations/HttpConfigurationBuilder.cs b/src/LaunchDarkly.ClientSdk/Integrations/HttpConfigurationBuilder.cs
index ce8458af..4f138db4 100644
--- a/src/LaunchDarkly.ClientSdk/Integrations/HttpConfigurationBuilder.cs
+++ b/src/LaunchDarkly.ClientSdk/Integrations/HttpConfigurationBuilder.cs
@@ -237,7 +237,7 @@ public HttpConfigurationBuilder Wrapper(string wrapperName, string wrapperVersio
/// Key for authenticating with LD service
/// Application Info for this application environment
/// an
- public HttpConfiguration CreateHttpConfiguration(string authKey, ApplicationInfo applicationInfo) =>
+ public HttpConfiguration CreateHttpConfiguration(string authKey, ApplicationInfo? applicationInfo) =>
new HttpConfiguration(
MakeHttpProperties(authKey, applicationInfo),
_messageHandler,
@@ -255,7 +255,7 @@ public LdValue DescribeConfiguration(LdClientContext context) =>
// which is more correct, but we can't really set ReadTimeout in this SDK
.Build();
- private HttpProperties MakeHttpProperties(string authToken, ApplicationInfo applicationInfo)
+ private HttpProperties MakeHttpProperties(string authToken, ApplicationInfo? applicationInfo)
{
Func handlerFn;
if (_messageHandler is null)
@@ -273,9 +273,13 @@ private HttpProperties MakeHttpProperties(string authToken, ApplicationInfo appl
.WithHttpMessageHandlerFactory(handlerFn)
.WithProxy(_proxy)
.WithUserAgent(SdkPackage.UserAgent)
- .WithApplicationTags(applicationInfo)
.WithWrapper(_wrapperName, _wrapperVersion);
+ if (applicationInfo.HasValue)
+ {
+ httpProperties = httpProperties.WithApplicationTags(applicationInfo.Value);
+ }
+
foreach (var kv in _customHeaders)
{
httpProperties = httpProperties.WithHeader(kv.Key, kv.Value);
diff --git a/src/LaunchDarkly.ClientSdk/Integrations/PersistenceConfigurationBuilder.cs b/src/LaunchDarkly.ClientSdk/Integrations/PersistenceConfigurationBuilder.cs
index 972c8cf4..1d88a15f 100644
--- a/src/LaunchDarkly.ClientSdk/Integrations/PersistenceConfigurationBuilder.cs
+++ b/src/LaunchDarkly.ClientSdk/Integrations/PersistenceConfigurationBuilder.cs
@@ -84,7 +84,7 @@ public PersistenceConfigurationBuilder Storage(IComponentConfigurer
///
///
- ///
+ /// the builder
public PersistenceConfigurationBuilder MaxCachedContexts(int maxCachedContexts)
{
_maxCachedContexts = maxCachedContexts;
diff --git a/src/LaunchDarkly.ClientSdk/Internal/ContextDecorator.cs b/src/LaunchDarkly.ClientSdk/Internal/AnonymousKeyContextDecorator.cs
similarity index 96%
rename from src/LaunchDarkly.ClientSdk/Internal/ContextDecorator.cs
rename to src/LaunchDarkly.ClientSdk/Internal/AnonymousKeyContextDecorator.cs
index 69d256cc..008a8bed 100644
--- a/src/LaunchDarkly.ClientSdk/Internal/ContextDecorator.cs
+++ b/src/LaunchDarkly.ClientSdk/Internal/AnonymousKeyContextDecorator.cs
@@ -5,7 +5,7 @@
namespace LaunchDarkly.Sdk.Client.Internal
{
- internal class ContextDecorator
+ internal class AnonymousKeyContextDecorator
{
private readonly PersistentDataStoreWrapper _store;
private readonly bool _generateAnonymousKeys;
@@ -13,7 +13,7 @@ internal class ContextDecorator
private Dictionary _cachedGeneratedKey = new Dictionary();
private object _generatedKeyLock = new object();
- public ContextDecorator(
+ public AnonymousKeyContextDecorator(
PersistentDataStoreWrapper store,
bool generateAnonymousKeys
)
diff --git a/src/LaunchDarkly.ClientSdk/Internal/AutoEnvContextDecorator.cs b/src/LaunchDarkly.ClientSdk/Internal/AutoEnvContextDecorator.cs
new file mode 100644
index 00000000..c41670aa
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/Internal/AutoEnvContextDecorator.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using LaunchDarkly.Logging;
+using LaunchDarkly.Sdk.EnvReporting;
+using LaunchDarkly.Sdk.Client.Internal.DataStores;
+
+namespace LaunchDarkly.Sdk.Client.Internal
+{
+ ///
+ /// This class can decorate a context by adding additional contexts to it using auto environment attributes
+ /// provided by .
+ ///
+ internal class AutoEnvContextDecorator
+ {
+ internal const string LdApplicationKind = "ld_application";
+ internal const string LdDeviceKind = "ld_device";
+ internal const string AttrId = "id";
+ internal const string AttrName = "name";
+ internal const string AttrVersion = "version";
+ internal const string AttrVersionName = "versionName";
+ internal const string AttrManufacturer = "manufacturer";
+ internal const string AttrModel = "model";
+ internal const string AttrLocale = "locale";
+ internal const string AttrOs = "os";
+ internal const string AttrFamily = "family";
+ internal const string EnvAttributesVersion = "envAttributesVersion";
+ internal const string SpecVersion = "1.0";
+
+ private readonly PersistentDataStoreWrapper _persistentData;
+ private readonly IEnvironmentReporter _environmentReporter;
+ private readonly Logger _logger;
+
+ ///
+ /// Creates a .
+ ///
+ /// the data source that will be used for retrieving/saving information related
+ /// to the generated contexts. Example data includes the stable key of the ld_device context kind.
+ /// the environment reporter that will be used to source the
+ /// environment attributes
+ /// the logger
+ public AutoEnvContextDecorator(
+ PersistentDataStoreWrapper persistentData,
+ IEnvironmentReporter environmentReporter,
+ Logger logger)
+ {
+ _persistentData = persistentData;
+ _environmentReporter = environmentReporter;
+ _logger = logger;
+ }
+
+ ///
+ /// Decorates the provided context with additional contexts containing environment attributes.
+ ///
+ /// the context to be decorated
+ /// the decorated context
+ public Context DecorateContext(Context context)
+ {
+ var builder = Context.MultiBuilder();
+ builder.Add(context);
+
+ foreach (var recipe in MakeRecipeList())
+ {
+ if (!context.TryGetContextByKind(recipe.Kind, out _))
+ {
+ // only add contexts for recipe Kinds not already in context to avoid overwriting data.
+ recipe.TryWrite(builder);
+ }
+ else
+ {
+ _logger.Warn("Unable to automatically add environment attributes for kind:{0}. {1} already exists.",
+ recipe.Kind, recipe.Kind);
+ }
+ }
+
+ return builder.Build();
+ }
+
+ private readonly struct ContextRecipe
+ {
+ public ContextKind Kind { get; }
+ private Func KeyCallable { get; }
+ private List RecipeNodes { get; }
+
+ public ContextRecipe(ContextKind kind, Func keyCallable, List recipeNodes)
+ {
+ Kind = kind;
+ KeyCallable = keyCallable;
+ RecipeNodes = recipeNodes;
+ }
+
+ public void TryWrite(ContextMultiBuilder multiBuilder)
+ {
+ var contextBuilder = Context.Builder(Kind, KeyCallable.Invoke());
+ var adaptedBuilder = new ContextBuilderAdapter(contextBuilder);
+ if (RecipeNodes.Aggregate(false, (wrote, node) => wrote | node.TryWrite(adaptedBuilder)))
+ {
+ contextBuilder.Set(EnvAttributesVersion, SpecVersion);
+ multiBuilder.Add(contextBuilder.Build());
+ }
+ }
+ }
+
+ private List MakeRecipeList()
+ {
+ var ldApplicationKind = ContextKind.Of(LdApplicationKind);
+ var applicationNodes = new List
+ {
+ new Node(AttrId, LdValue.Of(_environmentReporter.ApplicationInfo?.ApplicationId)),
+ new Node(AttrName,
+ LdValue.Of(_environmentReporter.ApplicationInfo?.ApplicationName)),
+ new Node(AttrVersion,
+ LdValue.Of(_environmentReporter.ApplicationInfo?.ApplicationVersion)),
+ new Node(AttrVersionName,
+ LdValue.Of(_environmentReporter.ApplicationInfo?.ApplicationVersionName)),
+ new Node(AttrLocale, LdValue.Of(_environmentReporter.Locale)),
+ };
+
+ var ldDeviceKind = ContextKind.Of(LdDeviceKind);
+ var deviceNodes = new List
+ {
+ new Node(AttrManufacturer,
+ LdValue.Of(_environmentReporter.DeviceInfo?.Manufacturer)),
+ new Node(AttrModel, LdValue.Of(_environmentReporter.DeviceInfo?.Model)),
+ new Node(AttrOs, new List
+ {
+ new Node(AttrFamily, LdValue.Of(_environmentReporter.OsInfo?.Family)),
+ new Node(AttrName, LdValue.Of(_environmentReporter.OsInfo?.Name)),
+ new Node(AttrVersion, LdValue.Of(_environmentReporter.OsInfo?.Version)),
+ })
+ };
+
+ return new List
+ {
+ new ContextRecipe(
+ ldApplicationKind,
+ () => Base64.UrlSafeSha256Hash(
+ _environmentReporter.ApplicationInfo?.ApplicationId ?? ""
+ ),
+ applicationNodes
+ ),
+ new ContextRecipe(
+ ldDeviceKind,
+ () => GetOrCreateAutoContextKey(_persistentData, ldDeviceKind),
+ deviceNodes
+ )
+ };
+ }
+
+ private string GetOrCreateAutoContextKey(PersistentDataStoreWrapper store, ContextKind contextKind)
+ {
+ var uniqueId = store.GetGeneratedContextKey(contextKind);
+ if (uniqueId is null)
+ {
+ uniqueId = Guid.NewGuid().ToString();
+ store.SetGeneratedContextKey(contextKind, uniqueId);
+ }
+ return uniqueId;
+ }
+
+ private interface ISettableMap
+ {
+ void Set(string attributeName, LdValue value);
+ }
+
+ private class Node
+ {
+ private readonly string _key;
+ private readonly LdValue? _value;
+ private readonly List _children;
+
+ public Node(string key, List children)
+ {
+ _key = key;
+ _children = children;
+ }
+
+ public Node(string key, LdValue value)
+ {
+ _key = key;
+ _value = value;
+ }
+
+ public bool TryWrite(ISettableMap settableMap)
+ {
+ if (_value.HasValue && !_value.Value.IsNull)
+ {
+ settableMap.Set(_key, _value.Value);
+ return true;
+ }
+
+ if (_children == null) return false;
+
+ var objBuilder = LdValue.BuildObject();
+ var adaptedBuilder = new ObjectBuilderAdapter(objBuilder);
+
+ if (!_children.Aggregate(false, (wrote, node) => wrote | node.TryWrite(adaptedBuilder))) return false;
+
+ settableMap.Set(_key, objBuilder.Build());
+ return true;
+ }
+ }
+
+
+ private class ObjectBuilderAdapter : ISettableMap
+ {
+ private readonly LdValue.ObjectBuilder _underlyingBuilder;
+
+ public ObjectBuilderAdapter(LdValue.ObjectBuilder builder)
+ {
+ _underlyingBuilder = builder;
+ }
+
+ public void Set(string attributeName, LdValue value)
+ {
+ _underlyingBuilder.Set(attributeName, value);
+ }
+ }
+
+ private class ContextBuilderAdapter : ISettableMap
+ {
+ private readonly ContextBuilder _underlyingBuilder;
+
+ public ContextBuilderAdapter(ContextBuilder builder)
+ {
+ _underlyingBuilder = builder;
+ }
+
+ public void Set(string attributeName, LdValue value)
+ {
+ _underlyingBuilder.Set(attributeName, value);
+ }
+ }
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/Internal/SdkPackage.cs b/src/LaunchDarkly.ClientSdk/Internal/SdkPackage.cs
index e6749c14..2a9fa3f4 100644
--- a/src/LaunchDarkly.ClientSdk/Internal/SdkPackage.cs
+++ b/src/LaunchDarkly.ClientSdk/Internal/SdkPackage.cs
@@ -2,7 +2,6 @@
namespace LaunchDarkly.Sdk.Client.Internal
{
-
///
/// Defines common information about the SDK itself for usage
/// in various components.
diff --git a/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj b/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj
index 9228e2ca..e99aea05 100644
--- a/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj
+++ b/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj
@@ -37,13 +37,12 @@
-
+
-
+
-
@@ -81,6 +80,15 @@
+
+
+
+
+
+
+
+
+
../../LaunchDarkly.ClientSdk.snk
true
diff --git a/src/LaunchDarkly.ClientSdk/LdClient.cs b/src/LaunchDarkly.ClientSdk/LdClient.cs
index 147ce5f9..6034dfa2 100644
--- a/src/LaunchDarkly.ClientSdk/LdClient.cs
+++ b/src/LaunchDarkly.ClientSdk/LdClient.cs
@@ -25,8 +25,8 @@ namespace LaunchDarkly.Sdk.Client
///
///
/// Like all client-side LaunchDarkly SDKs, the LdClient always has a single current .
- /// You specify this context at initialization time, and you can change it later with
- /// or . All subsequent calls to evaluation methods like
+ /// You specify this context at initialization time, and you can change it later with
+ /// or . All subsequent calls to evaluation methods like
/// refer to the flag values for the current context.
///
///
@@ -59,7 +59,8 @@ public sealed class LdClient : ILdClient
readonly IEventProcessor _eventProcessor;
readonly IFlagTracker _flagTracker;
readonly TaskExecutor _taskExecutor;
- readonly ContextDecorator _contextDecorator;
+ readonly AnonymousKeyContextDecorator _anonymousKeyContextDecorator;
+ private readonly AutoEnvContextDecorator _autoEnvContextDecorator;
private readonly Logger _log;
@@ -72,8 +73,8 @@ public sealed class LdClient : ILdClient
/// create a new client instance unless you first call on this one.
///
///
- /// Use the static factory methods or
- /// to set this instance.
+ /// Use the static factory methods or
+ /// to set this instance.
///
public static LdClient Instance => _instance;
@@ -91,9 +92,9 @@ public sealed class LdClient : ILdClient
/// The current evaluation context for all SDK operations.
///
///
- /// This is initially the context specified for or
- /// , but can be changed later with
- /// or .
+ /// This is initially the context specified for or
+ /// , but can be changed later with
+ /// or .
///
public Context Context => LockUtils.WithReadLock(_stateLock, () => _context);
@@ -135,17 +136,19 @@ public sealed class LdClient : ILdClient
// private constructor prevents initialization of this class
// without using WithConfigAnduser(config, user)
- LdClient() { }
+ LdClient()
+ {
+ }
LdClient(Configuration configuration, Context initialContext, TimeSpan startWaitTime)
{
_config = configuration ?? throw new ArgumentNullException(nameof(configuration));
- var baseContext = new LdClientContext(configuration, initialContext, this);
+ var baseContext = new LdClientContext(_config, initialContext, this);
- var diagnosticStore = _config.DiagnosticOptOut ? null :
- new ClientDiagnosticStore(baseContext, _config, startWaitTime);
- var diagnosticDisabler = _config.DiagnosticOptOut ? null :
- new DiagnosticDisablerImpl();
+ var diagnosticStore = _config.DiagnosticOptOut
+ ? null
+ : new ClientDiagnosticStore(baseContext, _config, startWaitTime);
+ var diagnosticDisabler = _config.DiagnosticOptOut ? null : new DiagnosticDisablerImpl();
_clientContext = baseContext.WithDiagnostics(diagnosticDisabler, diagnosticStore);
_log = _clientContext.BaseLogger;
@@ -153,16 +156,26 @@ public sealed class LdClient : ILdClient
_log.Info("Starting LaunchDarkly Client {0}", Version);
- var persistenceConfiguration = (configuration.PersistenceConfigurationBuilder ?? Components.Persistence())
+ var persistenceConfiguration = (_config.PersistenceConfigurationBuilder ?? Components.Persistence())
.Build(_clientContext);
_dataStore = new FlagDataManager(
- configuration.MobileKey,
+ _config.MobileKey,
persistenceConfiguration,
_log.SubLogger(LogNames.DataStoreSubLog)
- );
+ );
- _contextDecorator = new ContextDecorator(_dataStore.PersistentStore, configuration.GenerateAnonymousKeys);
- _context = _contextDecorator.DecorateContext(initialContext);
+ _anonymousKeyContextDecorator =
+ new AnonymousKeyContextDecorator(_dataStore.PersistentStore, _config.GenerateAnonymousKeys);
+ var decoratedContext = _anonymousKeyContextDecorator.DecorateContext(initialContext);
+
+ if (_config.AutoEnvAttributes)
+ {
+ _autoEnvContextDecorator = new AutoEnvContextDecorator(_dataStore.PersistentStore,
+ _clientContext.EnvironmentReporter, _log);
+ decoratedContext = _autoEnvContextDecorator.DecorateContext(decoratedContext);
+ }
+
+ _context = decoratedContext;
// If we had cached data for the new context, set the current in-memory flag data state to use
// that data, so that any Variation calls made before Identify has completed will use the
@@ -171,30 +184,31 @@ public sealed class LdClient : ILdClient
if (cachedData != null)
{
_log.Debug("Cached flag data is available for this context");
- _dataStore.Init(_context, cachedData.Value, false); // false means "don't rewrite the flags to persistent storage"
+ _dataStore.Init(_context, cachedData.Value,
+ false); // false means "don't rewrite the flags to persistent storage"
}
var dataSourceUpdateSink = new DataSourceUpdateSinkImpl(
_dataStore,
- configuration.Offline,
+ _config.Offline,
_taskExecutor,
_log.SubLogger(LogNames.DataSourceSubLog)
- );
+ );
_dataSourceUpdateSink = dataSourceUpdateSink;
_dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceUpdateSink);
_flagTracker = new FlagTrackerImpl(dataSourceUpdateSink);
- var dataSourceFactory = configuration.DataSource ?? Components.StreamingDataSource();
+ var dataSourceFactory = _config.DataSource ?? Components.StreamingDataSource();
- _connectivityStateManager = Factory.CreateConnectivityStateManager(configuration);
+ _connectivityStateManager = Factory.CreateConnectivityStateManager(_config);
var isConnected = _connectivityStateManager.IsConnected;
- diagnosticDisabler?.SetDisabled(!isConnected || configuration.Offline);
+ diagnosticDisabler?.SetDisabled(!isConnected || _config.Offline);
- _eventProcessor = (configuration.Events ?? Components.SendEvents())
+ _eventProcessor = (_config.Events ?? Components.SendEvents())
.Build(_clientContext);
- _eventProcessor.SetOffline(configuration.Offline || !isConnected);
+ _eventProcessor.SetOffline(_config.Offline || !isConnected);
_connectionManager = new ConnectionManager(
_clientContext,
@@ -202,13 +216,13 @@ public sealed class LdClient : ILdClient
_dataSourceUpdateSink,
_eventProcessor,
diagnosticDisabler,
- configuration.EnableBackgroundUpdating,
+ _config.EnableBackgroundUpdating,
_context,
_log
- );
- _connectionManager.SetForceOffline(configuration.Offline);
+ );
+ _connectionManager.SetForceOffline(_config.Offline);
_connectionManager.SetNetworkEnabled(isConnected);
- if (configuration.Offline)
+ if (_config.Offline)
{
_log.Info("Starting LaunchDarkly client in offline mode");
}
@@ -216,12 +230,12 @@ public sealed class LdClient : ILdClient
_connectivityStateManager.ConnectionChanged += networkAvailable =>
{
_log.Debug("Setting online to {0} due to a connectivity change event", networkAvailable);
- _ = _connectionManager.SetNetworkEnabled(networkAvailable); // do not await the result
+ _ = _connectionManager.SetNetworkEnabled(networkAvailable); // do not await the result
};
-
+
// Send an initial identify event, but only if we weren't explicitly set to be offline
- if (!configuration.Offline)
+ if (!_config.Offline)
{
_eventProcessor.RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent
{
@@ -260,9 +274,10 @@ async Task StartAsync()
/// an property of .
///
///
- /// If you would rather this happen asynchronously, use . To
+ /// If you would rather this happen asynchronously, use
+ /// . To
/// specify additional configuration options rather than just the mobile key, use
- /// or .
+ /// or .
///
///
/// You must use one of these static factory methods to instantiate the single instance of LdClient
@@ -270,16 +285,24 @@ async Task StartAsync()
///
///
/// the mobile key given to you by LaunchDarkly
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// the initial evaluation context; see for more
/// about setting the context and optionally requesting a unique key for it
/// the maximum length of time to wait for the client to initialize
/// the singleton instance
- ///
- ///
- ///
- public static LdClient Init(string mobileKey, Context initialContext, TimeSpan maxWaitTime)
+ ///
+ ///
+ ///
+ public static LdClient Init(string mobileKey, ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes,
+ Context initialContext, TimeSpan maxWaitTime)
{
- var config = Configuration.Default(mobileKey);
+ var config = Configuration.Default(mobileKey, autoEnvAttributes);
return Init(config, initialContext, maxWaitTime);
}
@@ -288,19 +311,28 @@ public static LdClient Init(string mobileKey, Context initialContext, TimeSpan m
/// Creates a new singleton instance and attempts to initialize feature flags.
///
///
- /// This is equivalent to , but using the
+ /// This is equivalent to
+ /// , but using the
/// type instead of .
///
/// the mobile key given to you by LaunchDarkly
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// the initial user attributes; see for more
/// about setting the context and optionally requesting a unique key for it
/// the maximum length of time to wait for the client to initialize
/// the singleton instance
///
- ///
- ///
- public static LdClient Init(string mobileKey, User initialUser, TimeSpan maxWaitTime) =>
- Init(mobileKey, Context.FromUser(initialUser), maxWaitTime);
+ ///
+ ///
+ public static LdClient Init(string mobileKey, ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes,
+ User initialUser, TimeSpan maxWaitTime) =>
+ Init(mobileKey, autoEnvAttributes, Context.FromUser(initialUser), maxWaitTime);
///
/// Creates a new singleton instance and attempts to initialize feature flags
@@ -312,9 +344,10 @@ public static LdClient Init(string mobileKey, User initialUser, TimeSpan maxWait
/// the LaunchDarkly service is returned (or immediately if it is in offline mode).
///
///
- /// If you would rather this happen synchronously, use . To
+ /// If you would rather this happen synchronously, use
+ /// . To
/// specify additional configuration options rather than just the mobile key, you can use
- /// or .
+ /// or .
///
///
/// You must use one of these static factory methods to instantiate the single instance of LdClient
@@ -322,12 +355,20 @@ public static LdClient Init(string mobileKey, User initialUser, TimeSpan maxWait
///
///
/// the mobile key given to you by LaunchDarkly
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// the initial evaluation context; see for more
/// about setting the context and optionally requesting a unique key for it
/// a Task that resolves to the singleton LdClient instance
- public static async Task InitAsync(string mobileKey, Context initialContext)
+ public static async Task InitAsync(string mobileKey,
+ ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes, Context initialContext)
{
- var config = Configuration.Default(mobileKey);
+ var config = Configuration.Default(mobileKey, autoEnvAttributes);
return await InitAsync(config, initialContext);
}
@@ -337,17 +378,26 @@ public static async Task InitAsync(string mobileKey, Context initialCo
/// asynchronously.
///
///
- /// This is equivalent to , but using the
+ /// This is equivalent to
+ /// , but using the
/// type instead of .
///
/// the mobile key given to you by LaunchDarkly
+ /// Enable / disable Auto Environment Attributes functionality. When enabled,
+ /// the SDK will automatically provide data about the environment where the application is running.
+ /// This data makes it simpler to target your mobile customers based on application name or version, or on
+ /// device characteristics including manufacturer, model, operating system, locale, and so on. We recommend
+ /// enabling this when you configure the SDK. See
+ /// our documentation for
+ /// more details.
/// the initial user attributes
/// a Task that resolves to the singleton LdClient instance
- public static Task InitAsync(string mobileKey, User initialUser) =>
- InitAsync(mobileKey, Context.FromUser(initialUser));
+ public static Task InitAsync(string mobileKey,
+ ConfigurationBuilder.AutoEnvAttributes autoEnvAttributes, User initialUser) =>
+ InitAsync(mobileKey, autoEnvAttributes, Context.FromUser(initialUser));
///
- /// Creates and returns a new LdClient singleton instance, then starts the workflow for
+ /// Creates and returns a new LdClient singleton instance, then starts the workflow for
/// fetching Feature Flags.
///
///
@@ -358,9 +408,10 @@ public static Task InitAsync(string mobileKey, User initialUser) =>
/// an property of .
///
///
- /// If you would rather this happen asynchronously, use .
+ /// If you would rather this happen asynchronously, use .
/// If you do not need to specify configuration options other than the mobile key, you can use
- /// or .
+ /// or
+ /// .
///
///
/// You must use one of these static factory methods to instantiate the single instance of LdClient
@@ -375,8 +426,8 @@ public static Task InitAsync(string mobileKey, User initialUser) =>
/// an uninitialized state
/// the singleton LdClient instance
///
- ///
- ///
+ ///
+ ///
public static LdClient Init(Configuration config, Context initialContext, TimeSpan maxWaitTime)
{
if (maxWaitTime.Ticks < 0 && maxWaitTime != Timeout.InfiniteTimeSpan)
@@ -390,11 +441,11 @@ public static LdClient Init(Configuration config, Context initialContext, TimeSp
}
///
- /// Creates and returns a new LdClient singleton instance, then starts the workflow for
+ /// Creates and returns a new LdClient singleton instance, then starts the workflow for
/// fetching Feature Flags.
///
///
- /// This is equivalent to , but using the
+ /// This is equivalent to , but using the
/// type instead of .
///
/// the client configuration
@@ -403,8 +454,8 @@ public static LdClient Init(Configuration config, Context initialContext, TimeSp
/// if this time elapses, the method will not throw an exception but will return the client in
/// an uninitialized state
/// the singleton LdClient instance
- ///
- ///
+ ///
+ ///
///
public static LdClient Init(Configuration config, User initialUser, TimeSpan maxWaitTime) =>
Init(config, Context.FromUser(initialUser), maxWaitTime);
@@ -419,9 +470,10 @@ public static LdClient Init(Configuration config, User initialUser, TimeSpan max
/// the LaunchDarkly service is returned (or immediately if it is in offline mode).
///
///
- /// If you would rather this happen synchronously, use .
+ /// If you would rather this happen synchronously, use .
/// If you do not need to specify configuration options other than the mobile key, you can use
- /// or .
+ /// or
+ /// .
///
///
/// You must use one of these static factory methods to instantiate the single instance of LdClient
@@ -433,8 +485,8 @@ public static LdClient Init(Configuration config, User initialUser, TimeSpan max
/// about setting the context and optionally requesting a unique key for it
/// a Task that resolves to the singleton LdClient instance
///
- ///
- ///
+ ///
+ ///
public static async Task InitAsync(Configuration config, Context initialContext)
{
var c = CreateInstance(config, initialContext, TimeSpan.Zero);
@@ -447,14 +499,14 @@ public static async Task InitAsync(Configuration config, Context initi
/// asynchronously.
///
///
- /// This is equivalent to , but using the
+ /// This is equivalent to , but using the
/// type instead of .
///
/// the client configuration
/// the initial user attributes
/// a Task that resolves to the singleton LdClient instance
- ///
- ///
+ ///
+ ///
///
public static Task InitAsync(Configuration config, User initialUser) =>
InitAsync(config, Context.FromUser(initialUser));
@@ -490,61 +542,71 @@ public async Task SetOfflineAsync(bool value)
///
public bool BoolVariation(string key, bool defaultValue = false)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Bool, true, _eventFactoryDefault).Value;
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Bool, true,
+ _eventFactoryDefault).Value;
}
///
public EvaluationDetail BoolVariationDetail(string key, bool defaultValue = false)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Bool, true, _eventFactoryWithReasons);
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Bool, true,
+ _eventFactoryWithReasons);
}
///
public string StringVariation(string key, string defaultValue)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.String, true, _eventFactoryDefault).Value;
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.String, true,
+ _eventFactoryDefault).Value;
}
///
public EvaluationDetail StringVariationDetail(string key, string defaultValue)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.String, true, _eventFactoryWithReasons);
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.String, true,
+ _eventFactoryWithReasons);
}
///
public float FloatVariation(string key, float defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Float, true, _eventFactoryDefault).Value;
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Float, true,
+ _eventFactoryDefault).Value;
}
///
public EvaluationDetail FloatVariationDetail(string key, float defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Float, true, _eventFactoryWithReasons);
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Float, true,
+ _eventFactoryWithReasons);
}
///
public double DoubleVariation(string key, double defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Double, true, _eventFactoryDefault).Value;
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Double, true,
+ _eventFactoryDefault).Value;
}
///
public EvaluationDetail DoubleVariationDetail(string key, double defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Double, true, _eventFactoryWithReasons);
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Double, true,
+ _eventFactoryWithReasons);
}
///
public int IntVariation(string key, int defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Int, true, _eventFactoryDefault).Value;
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Int, true, _eventFactoryDefault)
+ .Value;
}
///
public EvaluationDetail IntVariationDetail(string key, int defaultValue = 0)
{
- return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Int, true, _eventFactoryWithReasons);
+ return VariationInternal(key, LdValue.Of(defaultValue), LdValue.Convert.Int, true,
+ _eventFactoryWithReasons);
}
///
@@ -559,7 +621,8 @@ public EvaluationDetail JsonVariationDetail(string key, LdValue default
return VariationInternal(key, defaultValue, LdValue.Convert.Json, false, _eventFactoryWithReasons);
}
- EvaluationDetail VariationInternal(string featureKey, LdValue defaultJson, LdValue.Converter converter, bool checkType, EventFactory eventFactory)
+ EvaluationDetail VariationInternal(string featureKey, LdValue defaultJson, LdValue.Converter converter,
+ bool checkType, EventFactory eventFactory)
{
T defaultValue = converter.ToType(defaultJson);
@@ -572,14 +635,16 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) =>
if (!Initialized)
{
_log.Warn("LaunchDarkly client has not yet been initialized. Returning default value");
- SendEvaluationEventIfOnline(eventFactory.NewUnknownFlagEvaluationEvent(featureKey, Context, defaultJson,
+ SendEvaluationEventIfOnline(eventFactory.NewUnknownFlagEvaluationEvent(featureKey, Context,
+ defaultJson,
EvaluationErrorKind.ClientNotReady));
return errorResult(EvaluationErrorKind.ClientNotReady);
}
else
{
_log.Info("Unknown feature flag {0}; returning default value", featureKey);
- SendEvaluationEventIfOnline(eventFactory.NewUnknownFlagEvaluationEvent(featureKey, Context, defaultJson,
+ SendEvaluationEventIfOnline(eventFactory.NewUnknownFlagEvaluationEvent(featureKey, Context,
+ defaultJson,
EvaluationErrorKind.FlagNotFound));
return errorResult(EvaluationErrorKind.FlagNotFound);
}
@@ -597,23 +662,28 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) =>
if (flag.Value.IsNull)
{
valueJson = defaultJson;
- result = new EvaluationDetail(defaultValue, flag.Variation, flag.Reason ?? EvaluationReason.OffReason);
+ result = new EvaluationDetail(defaultValue, flag.Variation,
+ flag.Reason ?? EvaluationReason.OffReason);
}
else
{
if (checkType && !defaultJson.IsNull && flag.Value.Type != defaultJson.Type)
{
valueJson = defaultJson;
- result = new EvaluationDetail(defaultValue, null, EvaluationReason.ErrorReason(EvaluationErrorKind.WrongType));
+ result = new EvaluationDetail(defaultValue, null,
+ EvaluationReason.ErrorReason(EvaluationErrorKind.WrongType));
}
else
{
valueJson = flag.Value;
- result = new EvaluationDetail(converter.ToType(flag.Value), flag.Variation, flag.Reason ?? EvaluationReason.OffReason);
+ result = new EvaluationDetail(converter.ToType(flag.Value), flag.Variation,
+ flag.Reason ?? EvaluationReason.OffReason);
}
}
+
var featureEvent = eventFactory.NewEvaluationEvent(featureKey, flag, Context,
- new EvaluationDetail(valueJson, flag.Variation, flag.Reason ?? EvaluationReason.OffReason), defaultJson);
+ new EvaluationDetail(valueJson, flag.Variation, flag.Reason ?? EvaluationReason.OffReason),
+ defaultJson);
SendEvaluationEventIfOnline(featureEvent);
return result;
}
@@ -631,6 +701,7 @@ public IDictionary AllFlags()
{
return ImmutableDictionary.Empty;
}
+
return data.Value.Items.Where(entry => entry.Value.Item != null)
.ToDictionary(p => p.Key, p => p.Value.Item.Value);
}
@@ -685,8 +756,15 @@ public bool Identify(Context context, TimeSpan maxWaitTime)
///
public async Task IdentifyAsync(Context context)
{
- Context newContext = _contextDecorator.DecorateContext(context);
- Context oldContext = newContext; // this initialization is overwritten below, it's only here to satisfy the compiler
+ Context newContext = _anonymousKeyContextDecorator.DecorateContext(context);
+ if (_config.AutoEnvAttributes)
+ {
+ newContext = _autoEnvContextDecorator.DecorateContext(newContext);
+ }
+
+ Context
+ oldContext =
+ newContext; // this initialization is overwritten below, it's only here to satisfy the compiler
LockUtils.WithWriteLock(_stateLock, () =>
{
@@ -706,11 +784,12 @@ public async Task IdentifyAsync(Context context)
{
_log.Debug("Identify found cached flag data for the new context");
}
+
_dataStore.Init(
newContext,
cachedData ?? new DataStoreTypes.FullDataSet(null),
false // false means "don't rewrite the flags to persistent storage"
- );
+ );
EventProcessorIfEnabled().RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent
{
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.android.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.android.cs
new file mode 100644
index 00000000..a4f71f11
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.android.cs
@@ -0,0 +1,97 @@
+/*
+Xamarin.Essentials
+
+The MIT License (MIT)
+
+Copyright (c) Microsoft Corporation
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+using System.Globalization;
+using Android.Content;
+using Android.Content.PM;
+using Android.Content.Res;
+using Android.Provider;
+using LaunchDarkly.Sdk.EnvReporting;
+#if __ANDROID_29__
+using AndroidX.Core.Content.PM;
+#else
+using Android.Support.V4.Content.PM;
+#endif
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class AppInfo
+ {
+ static ApplicationInfo? PlatformGetApplicationInfo() => new ApplicationInfo(
+ PlatformGetAppId(),
+ PlatformGetAppName(),
+ PlatformGetAppVersion(),
+ PlatformGetAppVersionName());
+
+ // The following methods are added by LaunchDarkly to align with the Application Info
+ // required by the SDK.
+ static string PlatformGetAppId() => Platform.AppContext.PackageName;
+ static string PlatformGetAppName() => PlatformGetName();
+ static string PlatformGetAppVersion() => PlatformGetBuild();
+ static string PlatformGetAppVersionName() => PlatformGetVersionString();
+
+ // End LaunchDarkly additions.
+
+ static string PlatformGetName()
+ {
+ var applicationInfo = Platform.AppContext.ApplicationInfo;
+ var packageManager = Platform.AppContext.PackageManager;
+ return applicationInfo.LoadLabel(packageManager);
+ }
+
+ static string PlatformGetVersionString()
+ {
+ var pm = Platform.AppContext.PackageManager;
+ var packageName = Platform.AppContext.PackageName;
+#pragma warning disable CS0618 // Type or member is obsolete
+ using (var info = pm.GetPackageInfo(packageName, PackageInfoFlags.MetaData))
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ return info.VersionName;
+ }
+ }
+
+ static string PlatformGetBuild()
+ {
+ var pm = Platform.AppContext.PackageManager;
+ var packageName = Platform.AppContext.PackageName;
+#pragma warning disable CS0618 // Type or member is obsolete
+ using (var info = pm.GetPackageInfo(packageName, PackageInfoFlags.MetaData))
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+#if __ANDROID_28__
+ return PackageInfoCompat.GetLongVersionCode(info).ToString(CultureInfo.InvariantCulture);
+#else
+#pragma warning disable CS0618 // Type or member is obsolete
+ return info.VersionCode.ToString(CultureInfo.InvariantCulture);
+#pragma warning restore CS0618 // Type or member is obsolete
+#endif
+ }
+ }
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.ios.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.ios.cs
new file mode 100644
index 00000000..1557e6dd
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.ios.cs
@@ -0,0 +1,22 @@
+using Foundation;
+#if __IOS__ || __TVOS__
+using UIKit;
+
+#elif __MACOS__
+using AppKit;
+#endif
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class AppInfo
+ {
+ static ApplicationInfo? PlatformGetApplicationInfo() => new ApplicationInfo(
+ GetBundleValue("CFBundleIdentifier"),
+ GetBundleValue("CFBundleName"),
+ GetBundleValue("CFBundleVersion"),
+ GetBundleValue("CFBundleShortString"));
+
+ static string GetBundleValue(string key)
+ => NSBundle.MainBundle.ObjectForInfoDictionary(key)?.ToString();
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.netstandard.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.netstandard.cs
new file mode 100644
index 00000000..c7e072b7
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.netstandard.cs
@@ -0,0 +1,7 @@
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class AppInfo
+ {
+ private static ApplicationInfo? PlatformGetApplicationInfo() => null;
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.shared.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.shared.cs
new file mode 100644
index 00000000..1573db1b
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.shared.cs
@@ -0,0 +1,7 @@
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class AppInfo
+ {
+ internal static ApplicationInfo? GetAppInfo() => PlatformGetApplicationInfo();
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.android.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.android.cs
new file mode 100644
index 00000000..3212c1a4
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.android.cs
@@ -0,0 +1,21 @@
+using Android.OS;
+using LaunchDarkly.Sdk.EnvReporting.LayerModels;
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class DeviceInfo
+ {
+ private static OsInfo? PlatformGetOsInfo() =>
+ new OsInfo(
+ DevicePlatform.Android.ToString(),
+ DevicePlatform.Android.ToString()+Build.VERSION.SdkInt,
+ Build.VERSION.Release
+ );
+
+ private static LaunchDarkly.Sdk.EnvReporting.LayerModels.DeviceInfo? PlatformGetDeviceInfo() =>
+ new EnvReporting.LayerModels.DeviceInfo(
+ Build.Manufacturer,
+ Build.Model
+ );
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.ios.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.ios.cs
new file mode 100644
index 00000000..0e5158e9
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.ios.cs
@@ -0,0 +1,29 @@
+using LaunchDarkly.Sdk.EnvReporting.LayerModels;
+#if __WATCHOS__
+using WatchKit;
+using UIDevice = WatchKit.WKInterfaceDevice;
+#else
+using UIKit;
+#endif
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class DeviceInfo
+ {
+
+ private static OsInfo? PlatformGetOsInfo() =>
+ new OsInfo("Apple", GetPlatform().ToString(), UIDevice.CurrentDevice.SystemVersion);
+
+ private static LaunchDarkly.Sdk.EnvReporting.LayerModels.DeviceInfo? PlatformGetDeviceInfo() =>
+ new EnvReporting.LayerModels.DeviceInfo(
+ "Apple", UIDevice.CurrentDevice.Model);
+ static DevicePlatform GetPlatform() =>
+#if __IOS__
+ DevicePlatform.iOS;
+#elif __TVOS__
+ DevicePlatform.tvOS;
+#elif __WATCHOS__
+ DevicePlatform.watchOS;
+#endif
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.netstandard.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.netstandard.cs
new file mode 100644
index 00000000..80f12528
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.netstandard.cs
@@ -0,0 +1,11 @@
+using LaunchDarkly.Sdk.EnvReporting.LayerModels;
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class DeviceInfo
+ {
+ private static OsInfo? PlatformGetOsInfo() => null;
+
+ private static LaunchDarkly.Sdk.EnvReporting.LayerModels.DeviceInfo? PlatformGetDeviceInfo() => null;
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.shared.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.shared.cs
new file mode 100644
index 00000000..4f4d267b
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.shared.cs
@@ -0,0 +1,11 @@
+using LaunchDarkly.Sdk.EnvReporting.LayerModels;
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal static partial class DeviceInfo
+ {
+ internal static OsInfo? GetOsInfo() => PlatformGetOsInfo();
+
+ internal static LaunchDarkly.Sdk.EnvReporting.LayerModels.DeviceInfo? GetDeviceInfo() => PlatformGetDeviceInfo();
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/DevicePlatform.shared.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DevicePlatform.shared.cs
new file mode 100644
index 00000000..dc745bb6
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/DevicePlatform.shared.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace LaunchDarkly.Sdk.Client.PlatformSpecific
+{
+ internal readonly struct DevicePlatform : IEquatable
+ {
+ readonly string devicePlatform;
+
+ public static DevicePlatform Android { get; } = new DevicePlatform(nameof(Android));
+
+ public static DevicePlatform iOS { get; } = new DevicePlatform(nameof(iOS));
+
+ public static DevicePlatform macOS { get; } = new DevicePlatform(nameof(macOS));
+
+ public static DevicePlatform tvOS { get; } = new DevicePlatform(nameof(tvOS));
+
+ public static DevicePlatform Tizen { get; } = new DevicePlatform(nameof(Tizen));
+
+ public static DevicePlatform UWP { get; } = new DevicePlatform(nameof(UWP));
+
+ public static DevicePlatform watchOS { get; } = new DevicePlatform(nameof(watchOS));
+
+ public static DevicePlatform Unknown { get; } = new DevicePlatform(nameof(Unknown));
+
+ DevicePlatform(string devicePlatform)
+ {
+ if (devicePlatform == null)
+ throw new ArgumentNullException(nameof(devicePlatform));
+
+ if (devicePlatform.Length == 0)
+ throw new ArgumentException(nameof(devicePlatform));
+
+ this.devicePlatform = devicePlatform;
+ }
+
+ public static DevicePlatform Create(string devicePlatform) =>
+ new DevicePlatform(devicePlatform);
+
+ public bool Equals(DevicePlatform other) =>
+ Equals(other.devicePlatform);
+
+ internal bool Equals(string other) =>
+ string.Equals(devicePlatform, other, StringComparison.Ordinal);
+
+ public override bool Equals(object obj) =>
+ obj is DevicePlatform && Equals((DevicePlatform)obj);
+
+ public override int GetHashCode() =>
+ devicePlatform == null ? 0 : devicePlatform.GetHashCode();
+
+ public override string ToString() =>
+ devicePlatform ?? string.Empty;
+
+ public static bool operator ==(DevicePlatform left, DevicePlatform right) =>
+ left.Equals(right);
+
+ public static bool operator !=(DevicePlatform left, DevicePlatform right) =>
+ !left.Equals(right);
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/PlatformSpecific/Platform.android.cs b/src/LaunchDarkly.ClientSdk/PlatformSpecific/Platform.android.cs
index 7972425f..fad87cbe 100644
--- a/src/LaunchDarkly.ClientSdk/PlatformSpecific/Platform.android.cs
+++ b/src/LaunchDarkly.ClientSdk/PlatformSpecific/Platform.android.cs
@@ -87,7 +87,7 @@ internal static partial class Platform
internal static bool HasApiLevel(BuildVersionCodes versionCode) =>
(int)Build.VERSION.SdkInt >= (int)versionCode;
-
+
//internal static CameraManager CameraManager =>
// AppContext.GetSystemService(Context.CameraService) as CameraManager;
diff --git a/src/LaunchDarkly.ClientSdk/Subsystems/LdClientContext.cs b/src/LaunchDarkly.ClientSdk/Subsystems/LdClientContext.cs
index 57d28a51..eea983d7 100644
--- a/src/LaunchDarkly.ClientSdk/Subsystems/LdClientContext.cs
+++ b/src/LaunchDarkly.ClientSdk/Subsystems/LdClientContext.cs
@@ -1,4 +1,5 @@
-using LaunchDarkly.Logging;
+using System;
+using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Client.Interfaces;
using LaunchDarkly.Sdk.Client.Internal;
using LaunchDarkly.Sdk.Client.PlatformSpecific;
@@ -95,7 +96,8 @@ public LdClientContext(
)
{
var logger = MakeLogger(configuration);
- var environmentReporter = MakeEnvironmentReporter(configuration.ApplicationInfo);
+ var environmentReporter = MakeEnvironmentReporter(configuration);
+
MobileKey = configuration.MobileKey;
BaseLogger = logger;
CurrentContext = currentContext;
@@ -228,16 +230,29 @@ internal static Logger MakeLogger(Configuration configuration)
return logAdapter.Logger(logConfig.BaseLoggerName ?? LogNames.Base);
}
- internal static IEnvironmentReporter MakeEnvironmentReporter(ApplicationInfoBuilder applicationInfoBuilder)
+ internal static IEnvironmentReporter MakeEnvironmentReporter(Configuration configuration)
{
+ var applicationInfoBuilder = configuration.ApplicationInfo;
+
var builder = new EnvironmentReporterBuilder();
if (applicationInfoBuilder != null)
{
var applicationInfo = applicationInfoBuilder.Build();
+
+ // If AppInfo is provided by the user, then the Config layer has first priority in the environment reporter.
builder.SetConfigLayer(new ConfigLayerBuilder().SetAppInfo(applicationInfo).Build());
}
+ // Enable the platform layer if auto env attributes is opted in.
+ if (configuration.AutoEnvAttributes)
+ {
+ // The platform layer has second priority if properties aren't set by the Config layer.
+ builder.SetPlatformLayer(PlatformAttributes.Layer);
+ }
+
+ // The SDK layer has third priority if properties aren't set by the Platform layer.
builder.SetSdkLayer(SdkAttributes.Layer);
+
return builder.Build();
}
}
diff --git a/src/LaunchDarkly.ClientSdk/Subsystems/PlatformAttributes.cs b/src/LaunchDarkly.ClientSdk/Subsystems/PlatformAttributes.cs
new file mode 100644
index 00000000..ff0b43aa
--- /dev/null
+++ b/src/LaunchDarkly.ClientSdk/Subsystems/PlatformAttributes.cs
@@ -0,0 +1,21 @@
+using System.Globalization;
+using LaunchDarkly.Sdk.Client.PlatformSpecific;
+using LaunchDarkly.Sdk.EnvReporting;
+
+namespace LaunchDarkly.Sdk.Client.Subsystems
+{
+ internal static class PlatformAttributes
+ {
+ internal static Layer Layer => new Layer(
+ AppInfo.GetAppInfo(),
+ DeviceInfo.GetOsInfo(),
+ DeviceInfo.GetDeviceInfo(),
+ // The InvariantCulture is default if none is set by the application. Microsoft says:
+ // "...it is associated with the English language but not with any country/region.."
+ // Source: https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture
+ // In order to avoid returning an empty string (their representation of InvariantCulture) as a context attribute,
+ // we will return "en" instead as the closest representation.
+ CultureInfo.CurrentCulture.Equals(CultureInfo.InvariantCulture) ? "en" : CultureInfo.CurrentCulture.ToString()
+ );
+ }
+}
diff --git a/src/LaunchDarkly.ClientSdk/Subsystems/SdkAttributes.cs b/src/LaunchDarkly.ClientSdk/Subsystems/SdkAttributes.cs
index 791d17f1..23a2d6f9 100644
--- a/src/LaunchDarkly.ClientSdk/Subsystems/SdkAttributes.cs
+++ b/src/LaunchDarkly.ClientSdk/Subsystems/SdkAttributes.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using LaunchDarkly.Sdk.Client.Internal;
using LaunchDarkly.Sdk.EnvReporting;
@@ -10,6 +11,6 @@ internal static class SdkAttributes
SdkPackage.Name,
SdkPackage.Version,
SdkPackage.Version),
- null, null);
+ null, null, null);
}
}
diff --git a/tests/LaunchDarkly.ClientSdk.Android.Tests/LaunchDarkly.ClientSdk.Android.Tests.csproj b/tests/LaunchDarkly.ClientSdk.Android.Tests/LaunchDarkly.ClientSdk.Android.Tests.csproj
index ea6aef1d..b124682c 100644
--- a/tests/LaunchDarkly.ClientSdk.Android.Tests/LaunchDarkly.ClientSdk.Android.Tests.csproj
+++ b/tests/LaunchDarkly.ClientSdk.Android.Tests/LaunchDarkly.ClientSdk.Android.Tests.csproj
@@ -93,6 +93,7 @@
project file format, they need to be referenced explicitly even though
they're in the project directory. -->
+
diff --git a/tests/LaunchDarkly.ClientSdk.Android.Tests/LdClientContextTests.cs b/tests/LaunchDarkly.ClientSdk.Android.Tests/LdClientContextTests.cs
new file mode 100644
index 00000000..c7add35b
--- /dev/null
+++ b/tests/LaunchDarkly.ClientSdk.Android.Tests/LdClientContextTests.cs
@@ -0,0 +1,56 @@
+using Xunit;
+
+namespace LaunchDarkly.Sdk.Client.Subsystems
+{
+ ///
+ /// These tests are for the . Since some of the behavior is platform dependent
+ /// and the .NET Standard does not support some platform capabilities we want to test, this test class is
+ /// in the Android tests package.
+ ///
+ public class LdClientContextTests
+ {
+ [Fact]
+ public void TestMakeEnvironmentReporterUsesApplicationInfoWhenSet()
+ {
+ var configuration = Configuration.Builder("aKey", ConfigurationBuilder.AutoEnvAttributes.Disabled)
+ .ApplicationInfo(
+ Components.ApplicationInfo().ApplicationId("mockId").ApplicationName("mockName")
+ ).Build();
+
+ var output = LdClientContext.MakeEnvironmentReporter(configuration);
+ Assert.Equal("mockId", output.ApplicationInfo?.ApplicationId);
+ }
+
+ [Fact]
+ public void TestMakeEnvironmentReporterDefaultsToSdkLayerWhenNothingSet()
+ {
+ var configuration = Configuration.Builder("aKey", ConfigurationBuilder.AutoEnvAttributes.Disabled)
+ .Build();
+
+ var output = LdClientContext.MakeEnvironmentReporter(configuration);
+ Assert.Equal(SdkAttributes.Layer.ApplicationInfo?.ApplicationId, output.ApplicationInfo?.ApplicationId);
+ }
+
+ [Fact]
+ public void TestMakeEnvironmentReporterUsesPlatformLayerWhenAutoEnvEnabled()
+ {
+ var configuration = Configuration.Builder("aKey", ConfigurationBuilder.AutoEnvAttributes.Enabled)
+ .Build();
+
+ var output = LdClientContext.MakeEnvironmentReporter(configuration);
+ Assert.NotEqual(SdkAttributes.Layer.ApplicationInfo?.ApplicationId, output.ApplicationInfo?.ApplicationId);
+ }
+
+ [Fact]
+ public void TestMakeEnvironmentReporterUsesApplicationInfoWhenSetAndAutoEnvEnabled()
+ {
+ var configuration = Configuration.Builder("aKey", ConfigurationBuilder.AutoEnvAttributes.Enabled)
+ .ApplicationInfo(
+ Components.ApplicationInfo().ApplicationId("mockId").ApplicationName("mockName")
+ ).Build();
+
+ var output = LdClientContext.MakeEnvironmentReporter(configuration);
+ Assert.Equal("mockId", output.ApplicationInfo?.ApplicationId);
+ }
+ }
+}
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/BaseTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/BaseTest.cs
index f4490fb5..a0a03822 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/BaseTest.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/BaseTest.cs
@@ -43,7 +43,7 @@ public void Dispose()
// needed, protects against accidental interaction with external services and also makes it easier to
// see which properties are important in a test.
protected ConfigurationBuilder BasicConfig() =>
- Configuration.Builder(BasicMobileKey)
+ Configuration.Builder(BasicMobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled)
.BackgroundModeManager(new MockBackgroundModeManager())
.ConnectivityStateManager(new MockConnectivityStateManager(true))
.DataSource(new MockDataSource().AsSingletonFactory())
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs
index eb95f354..beae849e 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs
@@ -10,24 +10,27 @@ namespace LaunchDarkly.Sdk.Client
public class ConfigurationTest : BaseTest
{
private readonly BuilderBehavior.BuildTester _tester =
- BuilderBehavior.For(() => Configuration.Builder(mobileKey), b => b.Build())
+ BuilderBehavior.For(() => Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled),
+ b => b.Build())
.WithCopyConstructor(c => Configuration.Builder(c));
const string mobileKey = "any-key";
- public ConfigurationTest(ITestOutputHelper testOutput) : base(testOutput) { }
+ public ConfigurationTest(ITestOutputHelper testOutput) : base(testOutput)
+ {
+ }
[Fact]
public void DefaultSetsKey()
{
- var config = Configuration.Default(mobileKey);
+ var config = Configuration.Default(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled);
Assert.Equal(mobileKey, config.MobileKey);
}
[Fact]
public void BuilderSetsKey()
{
- var config = Configuration.Builder(mobileKey).Build();
+ var config = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled).Build();
Assert.Equal(mobileKey, config.MobileKey);
}
@@ -99,7 +102,8 @@ public void Logging()
public void LoggingAdapterShortcut()
{
var adapter = Logs.ToWriter(Console.Out);
- var config = Configuration.Builder("key").Logging(adapter).Build();
+ var config = Configuration.Builder("key", ConfigurationBuilder.AutoEnvAttributes.Disabled).Logging(adapter)
+ .Build();
var logConfig = config.LoggingConfigurationBuilder.CreateLoggingConfiguration();
Assert.Same(adapter, logConfig.LogAdapter);
}
@@ -130,13 +134,15 @@ public void Persistence()
[Fact]
public void MobileKeyCannotBeNull()
{
- Assert.Throws(() => Configuration.Default(null));
+ Assert.Throws(() =>
+ Configuration.Default(null, ConfigurationBuilder.AutoEnvAttributes.Disabled));
}
[Fact]
public void MobileKeyCannotBeEmpty()
{
- Assert.Throws(() => Configuration.Default(""));
+ Assert.Throws(() =>
+ Configuration.Default("", ConfigurationBuilder.AutoEnvAttributes.Disabled));
}
}
}
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataTest.cs
index b6bd55fa..906d8fcb 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataTest.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataTest.cs
@@ -21,7 +21,8 @@ public class TestDataTest : BaseTest
public TestDataTest(ITestOutputHelper testOutput) : base(testOutput)
{
- _context = new LdClientContext(Configuration.Builder("key").Logging(testLogging).Build(), _initialUser);
+ _context = new LdClientContext(Configuration.Builder("key", ConfigurationBuilder.AutoEnvAttributes.Disabled)
+ .Logging(testLogging).Build(), _initialUser);
}
[Fact]
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataWithClientTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataWithClientTest.cs
index aeeb9ad4..ff622c7a 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataWithClientTest.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/TestDataWithClientTest.cs
@@ -12,7 +12,7 @@ public class TestDataWithClientTest : BaseTest
public TestDataWithClientTest(ITestOutputHelper testOutput) : base(testOutput)
{
- _config = Configuration.Builder("mobile-key")
+ _config = Configuration.Builder("mobile-key", ConfigurationBuilder.AutoEnvAttributes.Disabled)
.DataSource(_td)
.Events(Components.NoEvents)
.Build();
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Internal/ContextDecoratorTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Internal/AnonymousKeyContextDecoratorTest.cs
similarity index 94%
rename from tests/LaunchDarkly.ClientSdk.Tests/Internal/ContextDecoratorTest.cs
rename to tests/LaunchDarkly.ClientSdk.Tests/Internal/AnonymousKeyContextDecoratorTest.cs
index 908d732a..cdec94ae 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/Internal/ContextDecoratorTest.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Internal/AnonymousKeyContextDecoratorTest.cs
@@ -5,7 +5,7 @@
namespace LaunchDarkly.Sdk.Client.Internal
{
- public class ContextDecoratorTest : BaseTest
+ public class AnonymousKeyContextDecoratorTest : BaseTest
{
private static readonly ContextKind Kind1 = ContextKind.Of("kind1");
private static readonly ContextKind Kind2 = ContextKind.Of("kind2");
@@ -162,10 +162,10 @@ public void GeneratedKeysAreNotReusedAcrossRestartsIfPersistentStorageIsDisabled
Assert.NotEqual(c2TransformedA.Key, c2TransformedB.Key);
}
- private ContextDecorator MakeDecoratorWithPersistence(IPersistentDataStore store, bool generateAnonymousKeys = false) =>
- new ContextDecorator(new PersistentDataStoreWrapper(store, BasicMobileKey, testLogger), generateAnonymousKeys);
+ private AnonymousKeyContextDecorator MakeDecoratorWithPersistence(IPersistentDataStore store, bool generateAnonymousKeys = false) =>
+ new AnonymousKeyContextDecorator(new PersistentDataStoreWrapper(store, BasicMobileKey, testLogger), generateAnonymousKeys);
- private ContextDecorator MakeDecoratorWithoutPersistence(bool generateAnonymousKeys = false) =>
+ private AnonymousKeyContextDecorator MakeDecoratorWithoutPersistence(bool generateAnonymousKeys = false) =>
MakeDecoratorWithPersistence(new NullPersistentDataStore(), generateAnonymousKeys);
private void AssertContextHasBeenTransformedWithNewKey(Context original, Context transformed)
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Internal/AutoEnvContextDecoratorTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Internal/AutoEnvContextDecoratorTest.cs
new file mode 100644
index 00000000..e52bf1e2
--- /dev/null
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Internal/AutoEnvContextDecoratorTest.cs
@@ -0,0 +1,142 @@
+using System.Globalization;
+using LaunchDarkly.Sdk.Client.Internal.DataStores;
+using LaunchDarkly.Sdk.Client.Subsystems;
+using LaunchDarkly.Sdk.EnvReporting;
+using LaunchDarkly.Sdk.EnvReporting.LayerModels;
+using Xunit;
+
+namespace LaunchDarkly.Sdk.Client.Internal
+{
+ public class AutoEnvContextDecoratorTest : BaseTest
+ {
+ [Fact]
+ public void AdheresToSchemaTest()
+ {
+ const string osFamily = "family_foo";
+ const string osName = "name_bar";
+ const string osVersion = null;
+
+ var envReporter = new EnvironmentReporterBuilder().SetSdkLayer(SdkAttributes.Layer)
+ .SetPlatformLayer(new Layer(null, new OsInfo(osFamily, osName, osVersion), null, null)).Build();
+
+ var store = MakeMockDataStoreWrapper();
+ var decoratorUnderTest = MakeDecoratorWithPersistence(store, envReporter);
+
+ var input = Context.Builder("aKey").Kind(ContextKind.Of("aKind"))
+ .Set("dontOverwriteMeBro", "really bro").Build();
+ var output = decoratorUnderTest.DecorateContext(input);
+
+ // Create the expected context after the code runs
+ // because there will be persistence side effects
+ var applicationKind = ContextKind.Of(AutoEnvContextDecorator.LdApplicationKind);
+ var expectedApplicationKey = Base64.UrlSafeSha256Hash(envReporter.ApplicationInfo?.ApplicationId ?? "");
+ var expectedAppContext = Context.Builder(applicationKind, expectedApplicationKey)
+ .Set(AutoEnvContextDecorator.EnvAttributesVersion, AutoEnvContextDecorator.SpecVersion)
+ .Set(AutoEnvContextDecorator.AttrId, SdkPackage.Name)
+ .Set(AutoEnvContextDecorator.AttrName, SdkPackage.Name)
+ .Set(AutoEnvContextDecorator.AttrVersion, SdkPackage.Version)
+ .Set(AutoEnvContextDecorator.AttrVersionName, SdkPackage.Version)
+ .Build();
+
+ var deviceKind = ContextKind.Of(AutoEnvContextDecorator.LdDeviceKind);
+ var expectedDeviceContext = Context.Builder(deviceKind, store.GetGeneratedContextKey(deviceKind))
+ .Set(AutoEnvContextDecorator.EnvAttributesVersion, AutoEnvContextDecorator.SpecVersion)
+ .Set(AutoEnvContextDecorator.AttrOs,
+ LdValue.BuildObject().Set("family", osFamily).Set("name", osName).Build()).Build();
+
+ var expectedOutput = Context.MultiBuilder()
+ .Add(input)
+ .Add(expectedAppContext)
+ .Add(expectedDeviceContext)
+ .Build();
+
+ Assert.Equal(expectedOutput, output);
+ }
+
+ [Fact]
+ public void CustomCultureInPlatformLayerIsPropagated()
+ {
+ var platform = new Layer(null, null, null, "en-GB");
+
+ var envReporter = new EnvironmentReporterBuilder().SetPlatformLayer(platform).Build();
+ var store = MakeMockDataStoreWrapper();
+ var decoratorUnderTest = MakeDecoratorWithPersistence(store, envReporter);
+
+ var input = Context.Builder("aKey").Kind(ContextKind.Of("aKind")).Build();
+ var output = decoratorUnderTest.DecorateContext(input);
+
+
+ Context outContext;
+ Assert.True(output.TryGetContextByKind(new ContextKind(AutoEnvContextDecorator.LdApplicationKind),
+ out outContext));
+
+ Assert.Equal("en-GB", outContext.GetValue("locale").AsString);
+ }
+
+ [Fact]
+ public void DoesNotOverwriteCustomerDataTest()
+ {
+ var envReporter = new EnvironmentReporterBuilder().SetSdkLayer(SdkAttributes.Layer).Build();
+ var store = MakeMockDataStoreWrapper();
+ var decoratorUnderTest = MakeDecoratorWithPersistence(store, envReporter);
+
+ var input = Context.Builder(ContextKind.Of(AutoEnvContextDecorator.LdApplicationKind), "aKey")
+ .Set("dontOverwriteMeBro", "really bro").Build();
+ var output = decoratorUnderTest.DecorateContext(input);
+
+ var expectedOutput = Context.MultiBuilder().Add(input).Build();
+
+ Assert.Equal(expectedOutput, output);
+ }
+
+ [Fact]
+ public void DoesNotOverwriteCustomerDataMultiContextTest()
+ {
+ var envReporter = new EnvironmentReporterBuilder().SetSdkLayer(SdkAttributes.Layer).Build();
+ var store = MakeMockDataStoreWrapper();
+ var decoratorUnderTest = MakeDecoratorWithPersistence(store, envReporter);
+
+ var input1 = Context.Builder(ContextKind.Of(AutoEnvContextDecorator.LdApplicationKind), "aKey")
+ .Set("dontOverwriteMeBro", "really bro").Build();
+ var input2 = Context.Builder(ContextKind.Of(AutoEnvContextDecorator.LdDeviceKind), "anotherKey")
+ .Set("AndDontOverwriteThisEither", "bro").Build();
+ var multiContextInput = Context.MultiBuilder().Add(input1).Add(input2).Build();
+ var output = decoratorUnderTest.DecorateContext(multiContextInput);
+
+ // input and output should be the same
+ Assert.Equal(multiContextInput, output);
+ }
+
+ [Fact]
+ public void GeneratesConsistentKeysAcrossMultipleCalls()
+ {
+ var envReporter = new EnvironmentReporterBuilder().SetSdkLayer(SdkAttributes.Layer).Build();
+ var store = MakeMockDataStoreWrapper();
+ var decoratorUnderTest = MakeDecoratorWithPersistence(store, envReporter);
+
+ var input = Context.Builder(ContextKind.Of("aKind"), "aKey")
+ .Set("dontOverwriteMeBro", "really bro").Build();
+
+ var output1 = decoratorUnderTest.DecorateContext(input);
+ output1.TryGetContextByKind(ContextKind.Of("ld_application"), out var appContext1);
+ var key1 = appContext1.Key;
+
+ var output2 = decoratorUnderTest.DecorateContext(input);
+ output2.TryGetContextByKind(ContextKind.Of("ld_application"), out var appContext2);
+ var key2 = appContext2.Key;
+
+ Assert.Equal(key1, key2);
+ }
+
+ private PersistentDataStoreWrapper MakeMockDataStoreWrapper()
+ {
+ return new PersistentDataStoreWrapper(new MockPersistentDataStore(), BasicMobileKey, testLogger);
+ }
+
+ private AutoEnvContextDecorator MakeDecoratorWithPersistence(PersistentDataStoreWrapper store,
+ IEnvironmentReporter reporter)
+ {
+ return new AutoEnvContextDecorator(store, reporter, testLogger);
+ }
+ }
+}
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Internal/Base64Test.cs b/tests/LaunchDarkly.ClientSdk.Tests/Internal/Base64Test.cs
index 1e9d6d75..a0e55dcd 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/Internal/Base64Test.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Internal/Base64Test.cs
@@ -10,5 +10,13 @@ public void TestUrlSafeBase64Encode()
Assert.Equal("eyJrZXkiOiJmb28-YmFyX18_In0=",
Base64.UrlSafeEncode(@"{""key"":""foo>bar__?""}"));
}
+
+ [Fact]
+ public void TestUrlSafeSha256Hash()
+ {
+ var input = "OhYeah?HashThis!!!"; // hash is KzDwVRpvTuf//jfMK27M4OMpIRTecNcJoaffvAEi+as= and it has a + and a /
+ var expectedOutput = "KzDwVRpvTuf__jfMK27M4OMpIRTecNcJoaffvAEi-as=";
+ Assert.Equal(expectedOutput, Base64.UrlSafeSha256Hash(input));
+ }
}
}
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Internal/DataSources/FeatureFlagRequestorTests.cs b/tests/LaunchDarkly.ClientSdk.Tests/Internal/DataSources/FeatureFlagRequestorTests.cs
index 9bfa878f..27b02681 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/Internal/DataSources/FeatureFlagRequestorTests.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/Internal/DataSources/FeatureFlagRequestorTests.cs
@@ -5,7 +5,6 @@
using LaunchDarkly.TestHelpers.HttpTest;
using Xunit;
using Xunit.Abstractions;
-
using static LaunchDarkly.TestHelpers.JsonAssertions;
namespace LaunchDarkly.Sdk.Client.Internal.DataSources
@@ -16,7 +15,9 @@ namespace LaunchDarkly.Sdk.Client.Internal.DataSources
public class FeatureFlagRequestorTests : BaseTest
{
- public FeatureFlagRequestorTests(ITestOutputHelper testOutput) : base(testOutput) { }
+ public FeatureFlagRequestorTests(ITestOutputHelper testOutput) : base(testOutput)
+ {
+ }
private const string _mobileKey = "FAKE_KEY";
@@ -26,7 +27,9 @@ public FeatureFlagRequestorTests(ITestOutputHelper testOutput) : base(testOutput
// Note that in a real use case, the user encoding may vary depending on the target platform, because the SDK adds custom
// user attributes like "os". But the lower-level FeatureFlagRequestor component does not do that.
- private const string _allDataJson = "{}"; // Note that in this implementation, unlike the .NET SDK, FeatureFlagRequestor does not unmarshal the response
+ private const string
+ _allDataJson =
+ "{}"; // Note that in this implementation, unlike the .NET SDK, FeatureFlagRequestor does not unmarshal the response
[Theory]
[InlineData("", false, "/msdk/evalx/contexts/", "")]
@@ -40,20 +43,20 @@ public async Task GetFlagsUsesCorrectUriAndMethodInGetModeAsync(
bool withReasons,
string expectedPathWithoutUser,
string expectedQuery
- )
+ )
{
using (var server = HttpServer.Start(Handlers.BodyJson(_allDataJson)))
{
var baseUri = new Uri(server.Uri.ToString().TrimEnd('/') + baseUriExtraPath);
- var config = Configuration.Default(_mobileKey);
+ var config = Configuration.Default(_mobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled);
using (var requestor = new FeatureFlagRequestor(
- baseUri,
- _context,
- withReasons,
- new LdClientContext(config, _context).Http,
- testLogger))
+ baseUri,
+ _context,
+ withReasons,
+ new LdClientContext(config, _context).Http,
+ testLogger))
{
var resp = await requestor.FeatureFlagsAsync();
Assert.Equal(200, resp.statusCode);
@@ -61,7 +64,8 @@ string expectedQuery
var req = server.Recorder.RequireRequest();
Assert.Equal("GET", req.Method);
- AssertHelpers.ContextsEqual(_context, TestUtil.Base64ContextFromUrlPath(req.Path, expectedPathWithoutUser));
+ AssertHelpers.ContextsEqual(_context,
+ TestUtil.Base64ContextFromUrlPath(req.Path, expectedPathWithoutUser));
Assert.Equal(expectedQuery, req.Query);
Assert.Equal(_mobileKey, req.Headers["Authorization"]);
Assert.Equal("", req.Body);
@@ -83,22 +87,22 @@ public async Task GetFlagsUsesCorrectUriAndMethodInReportModeAsync(
bool withReasons,
string expectedPath,
string expectedQuery
- )
+ )
{
using (var server = HttpServer.Start(Handlers.BodyJson(_allDataJson)))
{
var baseUri = new Uri(server.Uri.ToString().TrimEnd('/') + baseUriExtraPath);
- var config = Configuration.Builder(_mobileKey)
+ var config = Configuration.Builder(_mobileKey, ConfigurationBuilder.AutoEnvAttributes.Disabled)
.Http(Components.HttpConfiguration().UseReport(true))
.Build();
using (var requestor = new FeatureFlagRequestor(
- baseUri,
- _context,
- withReasons,
- new LdClientContext(config, _context).Http,
- testLogger))
+ baseUri,
+ _context,
+ withReasons,
+ new LdClientContext(config, _context).Http,
+ testLogger))
{
var resp = await requestor.FeatureFlagsAsync();
Assert.Equal(200, resp.statusCode);
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs
index f627a400..d432c0ad 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs
@@ -9,11 +9,11 @@ namespace LaunchDarkly.Sdk.Client
{
public class LdClientTests : BaseTest
{
- private static readonly Context AnonUser = Context.Builder("anon-placeholder-key")
- .Anonymous(true)
- .Set("email", "example")
- .Set("other", 3)
- .Build();
+ private static readonly Context AnonUser = Context.Builder("anon-placeholder-key")
+ .Anonymous(true)
+ .Set("email", "example")
+ .Set("other", 3)
+ .Build();
public LdClientTests(ITestOutputHelper testOutput) : base(testOutput) { }
@@ -54,87 +54,87 @@ public async void InitPassesUserToDataSource()
Assert.Equal(BasicUser.Key, actualUser.Key);
Assert.Equal(actualUser, dataSourceConfig.ReceivedClientContext.CurrentContext);
}
- }
-
- [Fact]
- public async Task InitWithAnonUserAddsRandomizedKey()
- {
- // Note, we don't care about polling mode vs. streaming mode for this functionality.
- var config = BasicConfig().Persistence(Components.NoPersistence).GenerateAnonymousKeys(true).Build();
-
- string key1;
-
- using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
- {
- key1 = client.Context.Key;
- Assert.NotNull(key1);
- Assert.NotEqual("", key1);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key1).Build(),
- client.Context);
- }
-
- // Starting again should generate a new key, since we've turned off persistence
- using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
- {
- var key2 = client.Context.Key;
- Assert.NotNull(key2);
- Assert.NotEqual("", key2);
- Assert.NotEqual(key1, key2);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key2).Build(),
- client.Context);
- }
- }
-
- [Fact]
- public async Task InitWithAnonUserDoesNotChangeKeyIfConfigOptionIsNotSet()
- {
- // Note, we don't care about polling mode vs. streaming mode for this functionality.
- var config = BasicConfig().Persistence(Components.NoPersistence).Build();
-
- using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
- {
- AssertHelpers.ContextsEqual(AnonUser, client.Context);
- }
- }
-
- [Fact]
- public async Task InitWithAnonUserCanReusePreviousRandomizedKey()
- {
- // Note, we don't care about polling mode vs. streaming mode for this functionality.
- var store = new MockPersistentDataStore();
- var config = BasicConfig().Persistence(Components.Persistence().Storage(
- store.AsSingletonFactory()))
- .GenerateAnonymousKeys(true).Build();
-
- string key1;
-
- using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
- {
- key1 = client.Context.Key;
- Assert.NotNull(key1);
- Assert.NotEqual("", key1);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key1).Build(),
- client.Context);
- }
-
- // Starting again should reuse the persisted key
- using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
- {
- Assert.Equal(key1, client.Context.Key);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key1).Build(),
- client.Context);
- }
- }
-
+ }
+
+ [Fact]
+ public async Task InitWithAnonUserAddsRandomizedKey()
+ {
+ // Note, we don't care about polling mode vs. streaming mode for this functionality.
+ var config = BasicConfig().Persistence(Components.NoPersistence).GenerateAnonymousKeys(true).Build();
+
+ string key1;
+
+ using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
+ {
+ key1 = client.Context.Key;
+ Assert.NotNull(key1);
+ Assert.NotEqual("", key1);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key1).Build(),
+ client.Context);
+ }
+
+ // Starting again should generate a new key, since we've turned off persistence
+ using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
+ {
+ var key2 = client.Context.Key;
+ Assert.NotNull(key2);
+ Assert.NotEqual("", key2);
+ Assert.NotEqual(key1, key2);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key2).Build(),
+ client.Context);
+ }
+ }
+
+ [Fact]
+ public async Task InitWithAnonUserDoesNotChangeKeyIfConfigOptionIsNotSet()
+ {
+ // Note, we don't care about polling mode vs. streaming mode for this functionality.
+ var config = BasicConfig().Persistence(Components.NoPersistence).Build();
+
+ using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
+ {
+ AssertHelpers.ContextsEqual(AnonUser, client.Context);
+ }
+ }
+
+ [Fact]
+ public async Task InitWithAnonUserCanReusePreviousRandomizedKey()
+ {
+ // Note, we don't care about polling mode vs. streaming mode for this functionality.
+ var store = new MockPersistentDataStore();
+ var config = BasicConfig().Persistence(Components.Persistence().Storage(
+ store.AsSingletonFactory()))
+ .GenerateAnonymousKeys(true).Build();
+
+ string key1;
+
+ using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
+ {
+ key1 = client.Context.Key;
+ Assert.NotNull(key1);
+ Assert.NotEqual("", key1);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key1).Build(),
+ client.Context);
+ }
+
+ // Starting again should reuse the persisted key
+ using (var client = await TestUtil.CreateClientAsync(config, AnonUser))
+ {
+ Assert.Equal(key1, client.Context.Key);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key1).Build(),
+ client.Context);
+ }
+ }
+
[Fact]
public async void InitWithAnonUserPassesGeneratedUserToDataSource()
{
- MockPollingProcessor stub = new MockPollingProcessor(DataSetBuilder.Empty);
-
+ MockPollingProcessor stub = new MockPollingProcessor(DataSetBuilder.Empty);
+
var dataSourceConfig = new CapturingComponentConfigurer(stub.AsSingletonFactory());
var config = BasicConfig()
.DataSource(dataSourceConfig)
@@ -146,8 +146,48 @@ public async void InitWithAnonUserPassesGeneratedUserToDataSource()
var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
Assert.NotEqual(AnonUser, receivedContext);
Assert.Equal(client.Context, receivedContext);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(receivedContext.Key).Build(),
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(receivedContext.Key).Build(),
+ receivedContext);
+ }
+ }
+
+ [Fact]
+ public async void InitWithAutoEnvAttributesEnabledAddAppInfoContext()
+ {
+ MockPollingProcessor stub = new MockPollingProcessor(DataSetBuilder.Empty);
+ var dataSourceConfig = new CapturingComponentConfigurer(stub.AsSingletonFactory());
+ var config = BasicConfig()
+ .AutoEnvironmentAttributes(ConfigurationBuilder.AutoEnvAttributes.Enabled)
+ .DataSource(dataSourceConfig)
+ .GenerateAnonymousKeys(true)
+ .Build();
+
+ using (var client = await LdClient.InitAsync(config, AnonUser))
+ {
+ var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
+ Assert.True(receivedContext.TryGetContextByKind(ContextKind.Of("ld_application"), out _));
+ }
+ }
+
+ [Fact]
+ public async void InitWithAutoEnvAttributesDisabledNoAddedContexts()
+ {
+ MockPollingProcessor stub = new MockPollingProcessor(DataSetBuilder.Empty);
+ var dataSourceConfig = new CapturingComponentConfigurer(stub.AsSingletonFactory());
+ var config = BasicConfig()
+ .AutoEnvironmentAttributes(ConfigurationBuilder.AutoEnvAttributes.Disabled)
+ .DataSource(dataSourceConfig)
+ .GenerateAnonymousKeys(true)
+ .Build();
+
+ using (var client = await LdClient.InitAsync(config, AnonUser))
+ {
+ var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
+ Assert.NotEqual(AnonUser, receivedContext);
+ Assert.Equal(client.Context, receivedContext);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(receivedContext.Key).Build(),
receivedContext);
}
}
@@ -255,95 +295,95 @@ public async void IdentifyPassesUserToDataSource()
AssertHelpers.ContextsEqual(newUser, client.Context);
Assert.Equal(client.Context, receivedContext);
}
- }
-
- [Fact]
- public async Task IdentifyWithAnonUserAddsRandomizedKey()
- {
- var config = BasicConfig().Persistence(Components.NoPersistence).GenerateAnonymousKeys(true).Build();
-
- string key1;
-
- using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
- {
- await client.IdentifyAsync(AnonUser);
-
- key1 = client.Context.Key;
- Assert.NotNull(key1);
- Assert.NotEqual("", key1);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key1).Build(),
- client.Context);
-
- var anonUser2 = TestUtil.BuildAutoContext().Name("other").Build();
- await client.IdentifyAsync(anonUser2);
- var key2 = client.Context.Key;
- Assert.Equal(key1, key2); // Even though persistence is disabled, the key is stable during the lifetime of the SDK client.
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(anonUser2).Key(key2).Build(),
- client.Context);
- }
-
- using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
- {
- await client.IdentifyAsync(AnonUser);
-
- var key3 = client.Context.Key;
- Assert.NotNull(key3);
- Assert.NotEqual("", key3);
- Assert.NotEqual(key1, key3); // The previously generated key was discarded with the previous client.
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key3).Build(),
- client.Context);
- }
- }
-
- [Fact]
- public async Task IdentifyWithAnonUserDoesNotChangeKeyIfConfigOptionIsNotSet()
- {
- var config = BasicConfig().Persistence(Components.NoPersistence).Build();
-
- using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
- {
- await client.IdentifyAsync(AnonUser);
-
- AssertHelpers.ContextsEqual(AnonUser, client.Context);
- }
- }
-
- [Fact]
- public async Task IdentifyWithAnonUserCanReusePersistedRandomizedKey()
- {
- var store = new MockPersistentDataStore();
- var config = BasicConfig().Persistence(Components.Persistence().Storage(
- store.AsSingletonFactory()))
- .GenerateAnonymousKeys(true).Build();
-
- string key1;
-
- using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
- {
- await client.IdentifyAsync(AnonUser);
-
- key1 = client.Context.Key;
- Assert.NotNull(key1);
- Assert.NotEqual("", key1);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key1).Build(),
- client.Context);
- }
-
- using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
- {
- await client.IdentifyAsync(AnonUser);
-
- var key2 = client.Context.Key;
- Assert.Equal(key1, key2);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(key2).Build(),
- client.Context);
- }
- }
+ }
+
+ [Fact]
+ public async Task IdentifyWithAnonUserAddsRandomizedKey()
+ {
+ var config = BasicConfig().Persistence(Components.NoPersistence).GenerateAnonymousKeys(true).Build();
+
+ string key1;
+
+ using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ key1 = client.Context.Key;
+ Assert.NotNull(key1);
+ Assert.NotEqual("", key1);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key1).Build(),
+ client.Context);
+
+ var anonUser2 = TestUtil.BuildAutoContext().Name("other").Build();
+ await client.IdentifyAsync(anonUser2);
+ var key2 = client.Context.Key;
+ Assert.Equal(key1, key2); // Even though persistence is disabled, the key is stable during the lifetime of the SDK client.
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(anonUser2).Key(key2).Build(),
+ client.Context);
+ }
+
+ using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ var key3 = client.Context.Key;
+ Assert.NotNull(key3);
+ Assert.NotEqual("", key3);
+ Assert.NotEqual(key1, key3); // The previously generated key was discarded with the previous client.
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key3).Build(),
+ client.Context);
+ }
+ }
+
+ [Fact]
+ public async Task IdentifyWithAnonUserDoesNotChangeKeyIfConfigOptionIsNotSet()
+ {
+ var config = BasicConfig().Persistence(Components.NoPersistence).Build();
+
+ using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ AssertHelpers.ContextsEqual(AnonUser, client.Context);
+ }
+ }
+
+ [Fact]
+ public async Task IdentifyWithAnonUserCanReusePersistedRandomizedKey()
+ {
+ var store = new MockPersistentDataStore();
+ var config = BasicConfig().Persistence(Components.Persistence().Storage(
+ store.AsSingletonFactory()))
+ .GenerateAnonymousKeys(true).Build();
+
+ string key1;
+
+ using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ key1 = client.Context.Key;
+ Assert.NotNull(key1);
+ Assert.NotEqual("", key1);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key1).Build(),
+ client.Context);
+ }
+
+ using (var client = await TestUtil.CreateClientAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ var key2 = client.Context.Key;
+ Assert.Equal(key1, key2);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(key2).Build(),
+ client.Context);
+ }
+ }
[Fact]
public async void IdentifyWithAnonUserPassesGeneratedUserToDataSource()
@@ -363,8 +403,52 @@ public async void IdentifyWithAnonUserPassesGeneratedUserToDataSource()
var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
Assert.NotEqual(AnonUser, receivedContext);
Assert.Equal(client.Context, receivedContext);
- AssertHelpers.ContextsEqual(
- Context.BuilderFromContext(AnonUser).Key(client.Context.Key).Build(),
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(client.Context.Key).Build(),
+ receivedContext);
+ }
+ }
+
+ [Fact]
+ public async void IdentifyWithAutoEnvAttributesEnabledAddsAppInfoContext()
+ {
+ var stub = new MockPollingProcessor(DataSetBuilder.Empty);
+ var dataSourceConfig = new CapturingComponentConfigurer(stub.AsSingletonFactory());
+ var config = BasicConfig()
+ .AutoEnvironmentAttributes(ConfigurationBuilder.AutoEnvAttributes.Enabled)
+ .DataSource(dataSourceConfig)
+ .GenerateAnonymousKeys(true)
+ .Build();
+
+ using (var client = await LdClient.InitAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
+ Assert.True(receivedContext.TryGetContextByKind(ContextKind.Of("ld_application"), out _));
+ }
+ }
+
+ [Fact]
+ public async void IdentifyWithAutoEnvAttributesDisabledNoAddedContexts()
+ {
+ var stub = new MockPollingProcessor(DataSetBuilder.Empty);
+ var dataSourceConfig = new CapturingComponentConfigurer(stub.AsSingletonFactory());
+ var config = BasicConfig()
+ .AutoEnvironmentAttributes(ConfigurationBuilder.AutoEnvAttributes.Disabled)
+ .DataSource(dataSourceConfig)
+ .GenerateAnonymousKeys(true)
+ .Build();
+
+ using (var client = await LdClient.InitAsync(config, BasicUser))
+ {
+ await client.IdentifyAsync(AnonUser);
+
+ var receivedContext = dataSourceConfig.ReceivedClientContext.CurrentContext;
+ Assert.NotEqual(AnonUser, receivedContext);
+ Assert.Equal(client.Context, receivedContext);
+ AssertHelpers.ContextsEqual(
+ Context.BuilderFromContext(AnonUser).Key(receivedContext.Key).Build(),
receivedContext);
}
}
@@ -421,8 +505,8 @@ public void FlagsAreLoadedFromPersistentStorageByDefault()
var config = BasicConfig()
.Persistence(Components.Persistence().Storage(storage.AsSingletonFactory()))
.Offline(true)
- .Build();
- storage.SetupUserData(config.MobileKey, BasicUser.Key, data);
+ .Build();
+ storage.SetupUserData(config.MobileKey, BasicUser.Key, data);
using (var client = TestUtil.CreateClient(config, BasicUser))
{
diff --git a/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs
index e332b024..de00e482 100644
--- a/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs
+++ b/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs
@@ -6,7 +6,6 @@
using LaunchDarkly.Sdk.Internal;
using LaunchDarkly.Sdk.Json;
using Xunit;
-
using static LaunchDarkly.Sdk.Client.DataModel;
using static LaunchDarkly.Sdk.Client.Subsystems.DataStoreTypes;
@@ -20,7 +19,8 @@ public static class TestUtil
private static ThreadLocal InClientLock = new ThreadLocal();
- public static LdClientContext SimpleContext => new LdClientContext(Configuration.Default("key"), Context.New("userkey"));
+ public static LdClientContext SimpleContext => new LdClientContext(
+ Configuration.Default("key", ConfigurationBuilder.AutoEnvAttributes.Enabled), Context.New("userkey"));
public static Context Base64ContextFromUrlPath(string path, string pathPrefix)
{
@@ -42,6 +42,7 @@ public static T WithClientLock(Func f)
{
return f.Invoke();
}
+
ClientInstanceLock.Wait();
try
{
@@ -62,6 +63,7 @@ public static void WithClientLock(Action a)
a.Invoke();
return;
}
+
ClientInstanceLock.Wait();
try
{
@@ -81,6 +83,7 @@ public static async Task WithClientLockAsync(Func> f)
{
return await f.Invoke();
}
+
await ClientInstanceLock.WaitAsync();
try
{
@@ -124,10 +127,7 @@ public static async Task CreateClientAsync(Configuration config, Conte
public static void ClearClient()
{
- WithClientLock(() =>
- {
- LdClient.Instance?.Dispose();
- });
+ WithClientLock(() => { LdClient.Instance?.Dispose(); });
}
internal static string MakeJsonData(FullDataSet data)
@@ -143,6 +143,7 @@ internal static string MakeJsonData(FullDataSet data)
FeatureFlagJsonConverter.WriteJsonValue(item.Value.Item, w);
}
}
+
w.WriteEndObject();
});
}