From d34b2390cb92147347359d20bc5450aff6508186 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:13:53 -0700 Subject: [PATCH] prepare 4.0.0 release (#63) ## [4.0.0] - 2023-10-18 ### Added: - Added Automatic Mobile Environment Attributes functionality which 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. To learn more, read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). --------- Co-authored-by: Eli Bishop Co-authored-by: Gavin Whelan Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: LaunchDarklyReleaseBot Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Louis Chan Co-authored-by: Ember Stevens Co-authored-by: Ember Stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Todd Anderson Co-authored-by: Casey Waldren Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> --- contract-tests/Representations.cs | 1 + contract-tests/SdkClientEntity.cs | 6 +- contract-tests/TestService.cs | 5 +- src/LaunchDarkly.ClientSdk/Configuration.cs | 57 ++- .../ConfigurationBuilder.cs | 56 ++- .../Integrations/HttpConfigurationBuilder.cs | 10 +- .../PersistenceConfigurationBuilder.cs | 2 +- ...tor.cs => AnonymousKeyContextDecorator.cs} | 4 +- .../Internal/AutoEnvContextDecorator.cs | 235 ++++++++++ .../Internal/SdkPackage.cs | 1 - .../LaunchDarkly.ClientSdk.csproj | 14 +- src/LaunchDarkly.ClientSdk/LdClient.cs | 257 ++++++---- .../PlatformSpecific/AppInfo.android.cs | 97 ++++ .../PlatformSpecific/AppInfo.ios.cs | 22 + .../PlatformSpecific/AppInfo.netstandard.cs | 7 + .../PlatformSpecific/AppInfo.shared.cs | 7 + .../PlatformSpecific/DeviceInfo.android.cs | 21 + .../PlatformSpecific/DeviceInfo.ios.cs | 29 ++ .../DeviceInfo.netstandard.cs | 11 + .../PlatformSpecific/DeviceInfo.shared.cs | 11 + .../PlatformSpecific/DevicePlatform.shared.cs | 60 +++ .../PlatformSpecific/Platform.android.cs | 2 +- .../Subsystems/LdClientContext.cs | 21 +- .../Subsystems/PlatformAttributes.cs | 21 + .../Subsystems/SdkAttributes.cs | 3 +- ...aunchDarkly.ClientSdk.Android.Tests.csproj | 1 + .../LdClientContextTests.cs | 56 +++ .../LaunchDarkly.ClientSdk.Tests/BaseTest.cs | 2 +- .../ConfigurationTest.cs | 20 +- .../Integrations/TestDataTest.cs | 3 +- .../Integrations/TestDataWithClientTest.cs | 2 +- ...cs => AnonymousKeyContextDecoratorTest.cs} | 8 +- .../Internal/AutoEnvContextDecoratorTest.cs | 142 ++++++ .../Internal/Base64Test.cs | 8 + .../DataSources/FeatureFlagRequestorTests.cs | 40 +- .../LdClientTests.cs | 440 +++++++++++------- .../LaunchDarkly.ClientSdk.Tests/TestUtil.cs | 13 +- 37 files changed, 1351 insertions(+), 344 deletions(-) rename src/LaunchDarkly.ClientSdk/Internal/{ContextDecorator.cs => AnonymousKeyContextDecorator.cs} (96%) create mode 100644 src/LaunchDarkly.ClientSdk/Internal/AutoEnvContextDecorator.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.android.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.ios.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.netstandard.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/AppInfo.shared.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.android.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.ios.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.netstandard.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/DeviceInfo.shared.cs create mode 100644 src/LaunchDarkly.ClientSdk/PlatformSpecific/DevicePlatform.shared.cs create mode 100644 src/LaunchDarkly.ClientSdk/Subsystems/PlatformAttributes.cs create mode 100644 tests/LaunchDarkly.ClientSdk.Android.Tests/LdClientContextTests.cs rename tests/LaunchDarkly.ClientSdk.Tests/Internal/{ContextDecoratorTest.cs => AnonymousKeyContextDecoratorTest.cs} (94%) create mode 100644 tests/LaunchDarkly.ClientSdk.Tests/Internal/AutoEnvContextDecoratorTest.cs 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(); }); }