diff --git a/src/LaunchDarkly.ClientSdk/Components.cs b/src/LaunchDarkly.ClientSdk/Components.cs index aeb07a74..cf7b7c29 100644 --- a/src/LaunchDarkly.ClientSdk/Components.cs +++ b/src/LaunchDarkly.ClientSdk/Components.cs @@ -1,6 +1,7 @@ using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Client.Integrations; using LaunchDarkly.Sdk.Client.Interfaces; +using LaunchDarkly.Sdk.Client.Internal; namespace LaunchDarkly.Sdk.Client { @@ -88,6 +89,24 @@ public static LoggingConfigurationBuilder Logging() => public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) => new LoggingConfigurationBuilder().Adapter(adapter); + /// + /// Returns a configuration object that disables analytics events. + /// + /// + /// Passing this to causes + /// the SDK to discard all analytics events and not send them to LaunchDarkly, regardless of + /// any other configuration. + /// + /// + /// + /// var config = Configuration.Builder(mobileKey) + /// .Events(Components.NoEvents) + /// .Build(); + /// + /// + public static IEventProcessorFactory NoEvents => + ComponentsImpl.NullEventProcessorFactory.Instance; + /// /// A configuration object that disables logging. /// @@ -96,7 +115,7 @@ public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) => /// /// /// - /// var config = Configuration.Builder(sdkKey) + /// var config = Configuration.Builder(mobileKey) /// .Logging(Components.NoLogging) /// .Build(); /// @@ -131,7 +150,7 @@ public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) => /// /// /// - /// var config = Configuration.Builder(sdkKey) + /// var config = Configuration.Builder(mobileKey) /// .DataSource(Components.PollingDataSource() /// .PollInterval(TimeSpan.FromSeconds(45))) /// .Build(); @@ -143,6 +162,32 @@ public static LoggingConfigurationBuilder Logging(ILogAdapter adapter) => public static PollingDataSourceBuilder PollingDataSource() => new PollingDataSourceBuilder(); + /// + /// Returns a configuration builder for analytics event delivery. + /// + /// + /// + /// The default configuration has events enabled with default settings. If you want to + /// customize this behavior, call this method to obtain a builder, change its properties + /// with the methods, and pass it to + /// . + /// + /// + /// To completely disable sending analytics events, use instead. + /// + /// + /// + /// + /// var config = Configuration.Builder(mobileKey) + /// .Events(Components.SendEvents() + /// .Capacity(5000) + /// .FlushInterval(TimeSpan.FromSeconds(2))) + /// .Build(); + /// + /// + /// a builder for setting event properties + public static EventProcessorBuilder SendEvents() => new EventProcessorBuilder(); + /// /// Returns a configurable factory for using streaming mode to get feature flag data. /// @@ -165,7 +210,7 @@ public static PollingDataSourceBuilder PollingDataSource() => /// /// /// - /// var config = Configuration.Builder(sdkKey) + /// var config = Configuration.Builder(mobileKey) /// .DataSource(Components.StreamingDataSource() /// .InitialReconnectDelay(TimeSpan.FromMilliseconds(500))) /// .Build(); diff --git a/src/LaunchDarkly.ClientSdk/Configuration.cs b/src/LaunchDarkly.ClientSdk/Configuration.cs index d53737d7..93942865 100644 --- a/src/LaunchDarkly.ClientSdk/Configuration.cs +++ b/src/LaunchDarkly.ClientSdk/Configuration.cs @@ -36,22 +36,10 @@ public sealed class Configuration internal IBackgroundModeManager BackgroundModeManager { get; } internal IConnectivityStateManager ConnectivityStateManager { get; } internal IDeviceInfo DeviceInfo { get; } - internal IEventProcessor EventProcessor { get; } internal IFlagCacheManager FlagCacheManager { get; } internal IFlagChangedEventManager FlagChangedEventManager { get; } internal IPersistentStorage PersistentStorage { get; } - /// - /// Whether or not user attributes (other than the key) should be private (not sent to - /// the LaunchDarkly server). - /// - /// - /// By default, this is . If , all of the user attributes - /// will be private, not just the attributes specified with - /// or with the method. - /// - public bool AllAttributesPrivate { get; } - /// /// Whether to disable the automatic sending of an alias event when the current user is changed /// to a non-anonymous user and the previous user was anonymous. @@ -97,43 +85,18 @@ public sealed class Configuration /// public bool EvaluationReasons { get; } - /// - /// The capacity of the event buffer. - /// - /// - /// The client buffers up to this many events in memory before flushing. If the capacity is exceeded - /// before the buffer is flushed, events will be discarded. Increasing the capacity means that events - /// are less likely to be discarded, at the cost of consuming more memory. - /// - public int EventCapacity { get; } - - /// - /// The time between flushes of the event buffer. - /// - /// - /// Decreasing the flush interval means that the event buffer is less likely to reach capacity. - /// - public TimeSpan EventFlushInterval { get; } /// - /// The base URL of the LaunchDarkly analytics event server. + /// A factory object that creates an implementation of , responsible + /// for sending analytics events. /// - public Uri EventsUri { get; } + public IEventProcessorFactory EventProcessorFactory { get; } /// /// The object to be used for sending HTTP requests, if a specific implementation is desired. /// public HttpMessageHandler HttpMessageHandler { get; } - /// - /// Sets whether to include full user details in every analytics event. - /// - /// - /// The default is : events will only include the user key, except for one - /// "index" event that provides the full details for the user. - /// - public bool InlineUsersInEvents { get; } - internal ILoggingConfigurationFactory LoggingConfigurationFactory { get; } /// @@ -158,16 +121,6 @@ public sealed class Configuration /// public bool PersistFlagValues { get; } - /// - /// Attribute names that have been marked as private for all users. - /// - /// - /// Any users sent to LaunchDarkly with this configuration active will have attributes with this name - /// removed, even if you did not use the - /// method when building the user. - /// - public IImmutableSet PrivateAttributeNames { get; } - /// /// The timeout when reading data from the streaming connection. /// @@ -184,11 +137,6 @@ public sealed class Configuration internal bool UseReport { get; } // UseReport is currently disabled due to Android HTTP issues (ch47341), but it's still implemented internally - internal static readonly Uri DefaultUri = new Uri("https://app.launchdarkly.com"); - internal static readonly Uri DefaultStreamUri = new Uri("https://clientstream.launchdarkly.com"); - internal static readonly Uri DefaultEventsUri = new Uri("https://mobile.launchdarkly.com"); - internal static readonly int DefaultEventCapacity = 100; - internal static readonly TimeSpan DefaultEventFlushInterval = TimeSpan.FromSeconds(5); internal static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); internal static readonly TimeSpan DefaultReconnectTime = TimeSpan.FromSeconds(1); internal static readonly TimeSpan DefaultConnectionTimeout = TimeSpan.FromSeconds(10); @@ -243,31 +191,24 @@ public static ConfigurationBuilder Builder(Configuration fromConfiguration) internal Configuration(ConfigurationBuilder builder) { - AllAttributesPrivate = builder._allAttributesPrivate; AutoAliasingOptOut = builder._autoAliasingOptOut; ConnectionTimeout = builder._connectionTimeout; DataSourceFactory = builder._dataSourceFactory; EnableBackgroundUpdating = builder._enableBackgroundUpdating; EvaluationReasons = builder._evaluationReasons; - EventFlushInterval = builder._eventFlushInterval; - EventCapacity = builder._eventCapacity; - EventsUri = builder._eventsUri; + EventProcessorFactory = builder._eventProcessorFactory; HttpMessageHandler = object.ReferenceEquals(builder._httpMessageHandler, ConfigurationBuilder.DefaultHttpMessageHandlerInstance) ? PlatformSpecific.Http.CreateHttpMessageHandler(builder._connectionTimeout, builder._readTimeout) : builder._httpMessageHandler; - InlineUsersInEvents = builder._inlineUsersInEvents; LoggingConfigurationFactory = builder._loggingConfigurationFactory; MobileKey = builder._mobileKey; Offline = builder._offline; PersistFlagValues = builder._persistFlagValues; - PrivateAttributeNames = builder._privateAttributeNames is null ? null : - builder._privateAttributeNames.ToImmutableHashSet(); UseReport = builder._useReport; BackgroundModeManager = builder._backgroundModeManager; ConnectivityStateManager = builder._connectivityStateManager; DeviceInfo = builder._deviceInfo; - EventProcessor = builder._eventProcessor; FlagCacheManager = builder._flagCacheManager; FlagChangedEventManager = builder._flagChangedEventManager; PersistentStorage = builder._persistentStorage; diff --git a/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs index 845a313a..cca120d5 100644 --- a/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.ClientSdk/ConfigurationBuilder.cs @@ -34,21 +34,16 @@ public sealed class ConfigurationBuilder internal static readonly HttpMessageHandler DefaultHttpMessageHandlerInstance = new HttpClientHandler(); internal bool _autoAliasingOptOut = false; - internal bool _allAttributesPrivate = false; internal TimeSpan _connectionTimeout = Configuration.DefaultConnectionTimeout; internal IDataSourceFactory _dataSourceFactory = null; internal bool _enableBackgroundUpdating = true; internal bool _evaluationReasons = false; - internal int _eventCapacity = Configuration.DefaultEventCapacity; - internal TimeSpan _eventFlushInterval = Configuration.DefaultEventFlushInterval; - internal Uri _eventsUri = Configuration.DefaultEventsUri; + internal IEventProcessorFactory _eventProcessorFactory = null; internal HttpMessageHandler _httpMessageHandler = DefaultHttpMessageHandlerInstance; - internal bool _inlineUsersInEvents = false; internal ILoggingConfigurationFactory _loggingConfigurationFactory = null; internal string _mobileKey; internal bool _offline = false; internal bool _persistFlagValues = true; - internal HashSet _privateAttributeNames = null; internal TimeSpan _readTimeout = Configuration.DefaultReadTimeout; internal bool _useReport = false; @@ -56,7 +51,6 @@ public sealed class ConfigurationBuilder internal IBackgroundModeManager _backgroundModeManager; internal IConnectivityStateManager _connectivityStateManager; internal IDeviceInfo _deviceInfo; - internal IEventProcessor _eventProcessor; internal IFlagCacheManager _flagCacheManager; internal IFlagChangedEventManager _flagChangedEventManager; internal IPersistentStorage _persistentStorage; @@ -68,22 +62,17 @@ internal ConfigurationBuilder(string mobileKey) internal ConfigurationBuilder(Configuration copyFrom) { - _allAttributesPrivate = copyFrom.AllAttributesPrivate; _autoAliasingOptOut = copyFrom.AutoAliasingOptOut; _connectionTimeout = copyFrom.ConnectionTimeout; + _dataSourceFactory = copyFrom.DataSourceFactory; _enableBackgroundUpdating = copyFrom.EnableBackgroundUpdating; _evaluationReasons = copyFrom.EvaluationReasons; - _eventCapacity = copyFrom.EventCapacity; - _eventFlushInterval = copyFrom.EventFlushInterval; - _eventsUri = copyFrom.EventsUri; + _eventProcessorFactory = copyFrom.EventProcessorFactory; _httpMessageHandler = copyFrom.HttpMessageHandler; - _inlineUsersInEvents = copyFrom.InlineUsersInEvents; _loggingConfigurationFactory = copyFrom.LoggingConfigurationFactory; _mobileKey = copyFrom.MobileKey; _offline = copyFrom.Offline; _persistFlagValues = copyFrom.PersistFlagValues; - _privateAttributeNames = copyFrom.PrivateAttributeNames is null ? null : - new HashSet(copyFrom.PrivateAttributeNames); _readTimeout = copyFrom.ReadTimeout; _useReport = copyFrom.UseReport; } @@ -98,23 +87,6 @@ public Configuration Build() return new Configuration(this); } - /// - /// Sets whether or not user attributes (other than the key) should be private (not sent to - /// the LaunchDarkly server). - /// - /// - /// By default, this is . If , all of the user attributes - /// will be private, not just the attributes specified with - /// or with the method. - /// - /// true if all attributes should be private - /// the same builder - public ConfigurationBuilder AllAttributesPrivate(bool allAttributesPrivate) - { - _allAttributesPrivate = allAttributesPrivate; - return this; - } - /// /// Whether to disable the automatic sending of an alias event when the current user is changed /// to a non-anonymous user and the previous user was anonymous. @@ -211,45 +183,18 @@ public ConfigurationBuilder EvaluationReasons(bool evaluationReasons) } /// - /// Sets the capacity of the event buffer. + /// Sets the implementation of the component that processes analytics events. /// /// - /// The client buffers up to this many events in memory before flushing. If the capacity is exceeded - /// before the buffer is flushed, events will be discarded. Increasing the capacity means that events - /// are less likely to be discarded, at the cost of consuming more memory. + /// The default is , but you may choose to set it to a customized + /// , a custom implementation (for instance, a test fixture), or + /// disable events with . /// - /// the capacity of the event buffer + /// a builder/factory object for event configuration /// the same builder - public ConfigurationBuilder EventCapacity(int eventCapacity) + public ConfigurationBuilder Events(IEventProcessorFactory eventProcessorFactory) { - _eventCapacity = eventCapacity <= 0 ? Configuration.DefaultEventCapacity : eventCapacity; - return this; - } - - /// - /// Sets the time between flushes of the event buffer. - /// - /// - /// Decreasing the flush interval means that the event buffer is less likely to reach capacity. The - /// default value is 5 seconds. - /// - /// the flush interval - /// the same builder - public ConfigurationBuilder EventFlushInterval(TimeSpan eventflushInterval) - { - _eventFlushInterval = eventflushInterval <= TimeSpan.Zero ? - Configuration.DefaultEventFlushInterval : eventflushInterval; - return this; - } - - /// - /// Sets the base URL of the LaunchDarkly analytics event server. - /// - /// the events URI - /// the same builder - public ConfigurationBuilder EventsUri(Uri eventsUri) - { - _eventsUri = eventsUri ?? Configuration.DefaultEventsUri; + _eventProcessorFactory = eventProcessorFactory; return this; } @@ -273,21 +218,6 @@ public ConfigurationBuilder HttpMessageHandler(HttpMessageHandler httpMessageHan return this; } - /// - /// Sets whether to include full user details in every analytics event. - /// - /// - /// The default is : events will only include the user key, except for one - /// "index" event that provides the full details for the user. - /// - /// true or false - /// the same builder - public ConfigurationBuilder InlineUsersInEvents(bool inlineUsersInEvents) - { - _inlineUsersInEvents = inlineUsersInEvents; - return this; - } - /// /// Sets the SDK's logging destination. /// @@ -384,31 +314,6 @@ public ConfigurationBuilder PersistFlagValues(bool persistFlagValues) return this; } - /// - /// Marks an attribute name as private for all users. - /// - /// - /// - /// Any users sent to LaunchDarkly with this configuration active will have attributes with this name - /// removed, even if you did not use the - /// method in . - /// - /// - /// You may call this method repeatedly to mark multiple attributes as private. - /// - /// - /// the attribute - /// the same builder - public ConfigurationBuilder PrivateAttribute(UserAttribute privateAttribute) - { - if (_privateAttributeNames is null) - { - _privateAttributeNames = new HashSet(); - } - _privateAttributeNames.Add(privateAttribute); - return this; - } - /// /// Sets the timeout when reading data from the streaming connection. /// @@ -443,12 +348,6 @@ internal ConfigurationBuilder DeviceInfo(IDeviceInfo deviceInfo) return this; } - internal ConfigurationBuilder EventProcessor(IEventProcessor eventProcessor) - { - _eventProcessor = eventProcessor; - return this; - } - internal ConfigurationBuilder FlagCacheManager(IFlagCacheManager flagCacheManager) { _flagCacheManager = flagCacheManager; diff --git a/src/LaunchDarkly.ClientSdk/Integrations/EventProcessorBuilder.cs b/src/LaunchDarkly.ClientSdk/Integrations/EventProcessorBuilder.cs new file mode 100644 index 00000000..695690cd --- /dev/null +++ b/src/LaunchDarkly.ClientSdk/Integrations/EventProcessorBuilder.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using LaunchDarkly.Sdk.Client.Interfaces; +using LaunchDarkly.Sdk.Client.Internal; +using LaunchDarkly.Sdk.Client.Internal.Events; +using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Events; + +namespace LaunchDarkly.Sdk.Client.Integrations +{ + /// + /// Contains methods for configuring delivery of analytics events. + /// + /// + /// The SDK normally buffers analytics events and sends them to LaunchDarkly at intervals. If you want + /// to customize this behavior, create a builder with , change its + /// properties with the methods of this class, and pass it to . + /// + /// + /// + /// var config = Configuration.Builder(sdkKey) + /// .Events( + /// Components.SendEvents().Capacity(5000).FlushInterval(TimeSpan.FromSeconds(2)) + /// ) + /// .Build(); + /// + /// + public sealed class EventProcessorBuilder : IEventProcessorFactory + { + /// + /// The default value for . + /// + public const int DefaultCapacity = 100; + + /// + /// The default value for . + /// + public static readonly TimeSpan DefaultFlushInterval = TimeSpan.FromSeconds(5); + + internal static readonly Uri DefaultBaseUri = new Uri("https://mobile.launchdarkly.com"); + + internal bool _allAttributesPrivate = false; + internal Uri _baseUri = null; + internal int _capacity = DefaultCapacity; + internal TimeSpan _flushInterval = DefaultFlushInterval; + internal bool _inlineUsersInEvents = false; + internal HashSet _privateAttributes = new HashSet(); + internal IEventSender _eventSender = null; // used in testing + + /// + /// Sets whether or not all optional user attributes should be hidden from LaunchDarkly. + /// + /// + /// If this is , all user attribute values (other than the key) will be private, not just + /// the attributes specified in or on a per-user basis with + /// methods. By default, it is . + /// + /// true if all user attributes should be private + /// the builder + public EventProcessorBuilder AllAttributesPrivate(bool allAttributesPrivate) + { + _allAttributesPrivate = allAttributesPrivate; + return this; + } + + /// + /// Sets a custom base URI for the events service. + /// + /// + /// You will only need to change this value in the following cases: + /// + /// + /// + /// You are using the Relay Proxy. + /// Set BaseUri to the base URI of the Relay Proxy instance. + /// + /// + /// + /// + /// You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. + /// + /// + /// + /// + /// the base URI of the events service; null to use the default + /// the builder + public EventProcessorBuilder BaseUri(Uri baseUri) + { + _baseUri = baseUri; + return this; + } + + /// + /// Sets the capacity of the events buffer. + /// + /// + /// + /// The client buffers up to this many events in memory before flushing. If the capacity is exceeded before + /// the buffer is flushed (see ), events will be discarded. Increasing the + /// capacity means that events are less likely to be discarded, at the cost of consuming more memory. + /// + /// + /// The default value is . A zero or negative value will be changed to the default. + /// + /// + /// the capacity of the event buffer + /// the builder + public EventProcessorBuilder Capacity(int capacity) + { + _capacity = (capacity <= 0) ? DefaultCapacity : capacity; + return this; + } + + // Used only in testing + internal EventProcessorBuilder EventSender(IEventSender eventSender) + { + _eventSender = eventSender; + return this; + } + + /// + /// Sets the interval between flushes of the event buffer. + /// + /// + /// Decreasing the flush interval means that the event buffer is less likely to reach capacity. + /// The default value is . A zero or negative value will be changed to + /// the default. + /// + /// the flush interval + /// the builder + public EventProcessorBuilder FlushInterval(TimeSpan flushInterval) + { + _flushInterval = (flushInterval.CompareTo(TimeSpan.Zero) <= 0) ? + DefaultFlushInterval : flushInterval; + return this; + } + + /// + /// Sets whether to include full user details in every analytics event. + /// + /// + /// The default value is : events will only include the user key, except for one + /// "identify" event that provides the full details for the user. + /// + /// true if you want full user details in each event + /// the builder + public EventProcessorBuilder InlineUsersInEvents(bool inlineUsersInEvents) + { + _inlineUsersInEvents = inlineUsersInEvents; + return this; + } + + /// + /// Marks a set of attribute names as private. + /// + /// + /// Any users sent to LaunchDarkly with this configuration active will have attributes with these + /// names removed. This is in addition to any attributes that were marked as private for an + /// individual user with methods. + /// + /// a set of attributes that will be removed from user data set to LaunchDarkly + /// the builder + /// + public EventProcessorBuilder PrivateAttributes(params UserAttribute[] attributes) + { + foreach (var a in attributes) + { + _privateAttributes.Add(a); + } + return this; + } + + /// + /// Marks a set of attribute names as private. + /// + /// + /// + /// Any users sent to LaunchDarkly with this configuration active will have attributes with these + /// names removed. This is in addition to any attributes that were marked as private for an + /// individual user with methods. + /// + /// + /// Using is preferable to avoid the possibility of + /// misspelling a built-in attribute. + /// + /// + /// a set of names that will be removed from user data set to LaunchDarkly + /// the builder + /// + public EventProcessorBuilder PrivateAttributeNames(params string[] attributes) + { + foreach (var a in attributes) + { + _privateAttributes.Add(UserAttribute.ForName(a)); + } + return this; + } + + /// + public IEventProcessor CreateEventProcessor(LdClientContext context) + { + var uri = _baseUri ?? DefaultBaseUri; + var eventsConfig = new EventsConfiguration + { + AllAttributesPrivate = _allAttributesPrivate, + EventCapacity = _capacity, + EventFlushInterval = _flushInterval, + EventsUri = uri.AddPath(Constants.EVENTS_PATH), + //DiagnosticUri = uri.AddPath("diagnostic"), // no diagnostic events yet + InlineUsersInEvents = _inlineUsersInEvents, + PrivateAttributeNames = _privateAttributes.ToImmutableHashSet(), + RetryInterval = TimeSpan.FromSeconds(1) + }; + var logger = context.BaseLogger.SubLogger(LogNames.EventsSubLog); + var eventSender = _eventSender ?? + new DefaultEventSender( + context.HttpProperties, + eventsConfig, + logger + ); + return new DefaultEventProcessorWrapper( + new EventProcessor( + eventsConfig, + eventSender, + null, // no user deduplicator, because the client-side SDK doesn't send index events + null, // diagnostic store would go here, but we haven't implemented diagnostic events + null, + logger, + null + )); + } + } +} diff --git a/src/LaunchDarkly.ClientSdk/Internal/Events/EventProcessorTypes.cs b/src/LaunchDarkly.ClientSdk/Interfaces/EventProcessorTypes.cs similarity index 98% rename from src/LaunchDarkly.ClientSdk/Internal/Events/EventProcessorTypes.cs rename to src/LaunchDarkly.ClientSdk/Interfaces/EventProcessorTypes.cs index a5d7459b..8cfa1c5f 100644 --- a/src/LaunchDarkly.ClientSdk/Internal/Events/EventProcessorTypes.cs +++ b/src/LaunchDarkly.ClientSdk/Interfaces/EventProcessorTypes.cs @@ -1,5 +1,5 @@  -namespace LaunchDarkly.Sdk.Client.Internal.Events +namespace LaunchDarkly.Sdk.Client.Interfaces { /// /// Parameter types for use by implementations. @@ -9,7 +9,7 @@ namespace LaunchDarkly.Sdk.Client.Internal.Events /// functionality. They are provided to allow a custom implementation /// or test fixture to be substituted for the SDK's normal analytics event logic. /// - internal static class EventProcessorTypes + public static class EventProcessorTypes { /// /// Parameters for . diff --git a/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessor.cs b/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessor.cs new file mode 100644 index 00000000..c24e544c --- /dev/null +++ b/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessor.cs @@ -0,0 +1,65 @@ +using System; + +namespace LaunchDarkly.Sdk.Client.Interfaces +{ + /// + /// Interface for an object that can send or store analytics events. + /// + /// + /// + /// Application code normally does not need to interact with or its + /// related parameter types. They are provided to allow a custom implementation or test fixture to be + /// substituted for the SDK's normal analytics event logic. + /// + /// + /// All of the Record methods must return as soon as possible without waiting for events to be + /// delivered; event delivery is done asynchronously by a background task. + /// + /// + public interface IEventProcessor : IDisposable + { + /// + /// Records the action of evaluating a feature flag. + /// + /// + /// Depending on the feature flag properties and event properties, this may be transmitted to the + /// events service as an individual event, or may only be added into summary data. + /// + /// parameters for an evaluation event + void RecordEvaluationEvent(EventProcessorTypes.EvaluationEvent e); + + /// + /// Records a set of user properties. + /// + /// parameters for an identify event + void RecordIdentifyEvent(EventProcessorTypes.IdentifyEvent e); + + /// + /// Records a custom event. + /// + /// parameters for a custom event + void RecordCustomEvent(EventProcessorTypes.CustomEvent e); + + /// + /// Records an alias event. + /// + void RecordAliasEvent(EventProcessorTypes.AliasEvent e); + + /// + /// Puts the component into offline mode if appropriate. + /// + /// true if the SDK has been put offline + void SetOffline(bool offline); + + /// + /// Specifies that any buffered events should be sent as soon as possible, rather than waiting + /// for the next flush interval. + /// + /// + /// This method triggers an asynchronous task, so events still may not be sent until a later + /// until a later time. However, calling will synchronously + /// deliver any events that were not yet delivered prior to shutting down. + /// + void Flush(); + } +} diff --git a/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessorFactory.cs b/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessorFactory.cs new file mode 100644 index 00000000..b75f3eff --- /dev/null +++ b/src/LaunchDarkly.ClientSdk/Interfaces/IEventProcessorFactory.cs @@ -0,0 +1,17 @@ + +namespace LaunchDarkly.Sdk.Client.Interfaces +{ + /// + /// Interface for a factory that creates some implementation of . + /// + public interface IEventProcessorFactory + { + /// + /// Called internally by the SDK to create an implementation instance. Applications do not need + /// to call this method. + /// + /// configuration of the current client instance + /// an IEventProcessor instance + IEventProcessor CreateEventProcessor(LdClientContext context); + } +} diff --git a/src/LaunchDarkly.ClientSdk/Interfaces/ILdClient.cs b/src/LaunchDarkly.ClientSdk/Interfaces/ILdClient.cs index d2ebbd30..70e3625b 100644 --- a/src/LaunchDarkly.ClientSdk/Interfaces/ILdClient.cs +++ b/src/LaunchDarkly.ClientSdk/Interfaces/ILdClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using LaunchDarkly.Sdk.Client.Integrations; namespace LaunchDarkly.Sdk.Client.Interfaces { @@ -361,7 +362,7 @@ public interface ILdClient : IDisposable /// When the LaunchDarkly client generates analytics events (from flag evaluations, or from /// or ), they are queued on a worker thread. /// The event thread normally sends all queued events to LaunchDarkly at regular intervals, controlled by the - /// option. Calling triggers a send + /// option. Calling triggers a send /// without waiting for the next interval. /// /// diff --git a/src/LaunchDarkly.ClientSdk/Internal/ComponentsImpl.cs b/src/LaunchDarkly.ClientSdk/Internal/ComponentsImpl.cs index 896cbc36..5f5ac3a3 100644 --- a/src/LaunchDarkly.ClientSdk/Internal/ComponentsImpl.cs +++ b/src/LaunchDarkly.ClientSdk/Internal/ComponentsImpl.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using LaunchDarkly.Sdk.Client.Interfaces; namespace LaunchDarkly.Sdk.Client.Internal @@ -25,5 +24,32 @@ public void Dispose() { } public Task Start() => Task.FromResult(true); } + + internal sealed class NullEventProcessorFactory : IEventProcessorFactory + { + internal static NullEventProcessorFactory Instance = new NullEventProcessorFactory(); + + public IEventProcessor CreateEventProcessor(LdClientContext context) => + NullEventProcessor.Instance; + } + + internal sealed class NullEventProcessor : IEventProcessor + { + internal static NullEventProcessor Instance = new NullEventProcessor(); + + public void Dispose() { } + + public void Flush() { } + + public void RecordAliasEvent(EventProcessorTypes.AliasEvent e) { } + + public void RecordCustomEvent(EventProcessorTypes.CustomEvent e) { } + + public void RecordEvaluationEvent(EventProcessorTypes.EvaluationEvent e) { } + + public void RecordIdentifyEvent(EventProcessorTypes.IdentifyEvent e) { } + + public void SetOffline(bool offline) { } + } } } diff --git a/src/LaunchDarkly.ClientSdk/Internal/Events/DefaultEventProcessorWrapper.cs b/src/LaunchDarkly.ClientSdk/Internal/Events/DefaultEventProcessorWrapper.cs index 0eb51cf7..466ae9a3 100644 --- a/src/LaunchDarkly.ClientSdk/Internal/Events/DefaultEventProcessorWrapper.cs +++ b/src/LaunchDarkly.ClientSdk/Internal/Events/DefaultEventProcessorWrapper.cs @@ -1,4 +1,5 @@ -using LaunchDarkly.Sdk.Internal.Events; +using LaunchDarkly.Sdk.Client.Interfaces; +using LaunchDarkly.Sdk.Internal.Events; namespace LaunchDarkly.Sdk.Client.Internal.Events { diff --git a/src/LaunchDarkly.ClientSdk/Internal/Events/EventFactory.cs b/src/LaunchDarkly.ClientSdk/Internal/Events/EventFactory.cs index 03a8e04e..e66a8004 100644 --- a/src/LaunchDarkly.ClientSdk/Internal/Events/EventFactory.cs +++ b/src/LaunchDarkly.ClientSdk/Internal/Events/EventFactory.cs @@ -1,6 +1,6 @@  using static LaunchDarkly.Sdk.Client.DataModel; -using static LaunchDarkly.Sdk.Client.Internal.Events.EventProcessorTypes; +using static LaunchDarkly.Sdk.Client.Interfaces.EventProcessorTypes; namespace LaunchDarkly.Sdk.Client.Internal.Events { diff --git a/src/LaunchDarkly.ClientSdk/Internal/Events/IEventProcessor.cs b/src/LaunchDarkly.ClientSdk/Internal/Events/IEventProcessor.cs deleted file mode 100644 index bc25f7b4..00000000 --- a/src/LaunchDarkly.ClientSdk/Internal/Events/IEventProcessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace LaunchDarkly.Sdk.Client.Internal.Events -{ - internal interface IEventProcessor : IDisposable - { - void RecordEvaluationEvent(EventProcessorTypes.EvaluationEvent e); - - void RecordIdentifyEvent(EventProcessorTypes.IdentifyEvent e); - - void RecordCustomEvent(EventProcessorTypes.CustomEvent e); - - void RecordAliasEvent(EventProcessorTypes.AliasEvent e); - - void SetOffline(bool offline); - - void Flush(); - } -} diff --git a/src/LaunchDarkly.ClientSdk/Internal/Factory.cs b/src/LaunchDarkly.ClientSdk/Internal/Factory.cs index 4d3ed983..d585aa84 100644 --- a/src/LaunchDarkly.ClientSdk/Internal/Factory.cs +++ b/src/LaunchDarkly.ClientSdk/Internal/Factory.cs @@ -1,11 +1,7 @@ -using System; -using LaunchDarkly.Logging; +using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Client.Internal.DataSources; using LaunchDarkly.Sdk.Client.Internal.DataStores; -using LaunchDarkly.Sdk.Client.Internal.Events; using LaunchDarkly.Sdk.Client.Internal.Interfaces; -using LaunchDarkly.Sdk.Internal; -using LaunchDarkly.Sdk.Internal.Events; namespace LaunchDarkly.Sdk.Client.Internal { @@ -34,39 +30,6 @@ internal static IConnectivityStateManager CreateConnectivityStateManager(Configu return configuration.ConnectivityStateManager ?? new DefaultConnectivityStateManager(); } - internal static IEventProcessor CreateEventProcessor(Configuration configuration, Logger baseLog) - { - if (configuration.EventProcessor != null) - { - return configuration.EventProcessor; - } - - var log = baseLog.SubLogger(LogNames.EventsSubLog); - var eventsConfig = new EventsConfiguration - { - AllAttributesPrivate = configuration.AllAttributesPrivate, - DiagnosticRecordingInterval = TimeSpan.FromMinutes(15), // TODO - DiagnosticUri = null, - EventCapacity = configuration.EventCapacity, - EventFlushInterval = configuration.EventFlushInterval, - EventsUri = configuration.EventsUri.AddPath(Constants.EVENTS_PATH), - InlineUsersInEvents = configuration.InlineUsersInEvents, - PrivateAttributeNames = configuration.PrivateAttributeNames, - RetryInterval = TimeSpan.FromSeconds(1) - }; - var httpProperties = configuration.HttpProperties; - var eventProcessor = new EventProcessor( - eventsConfig, - new DefaultEventSender(httpProperties, eventsConfig, log), - null, - null, - null, - log, - null - ); - return new DefaultEventProcessorWrapper(eventProcessor); - } - internal static IPersistentStorage CreatePersistentStorage(Configuration configuration, Logger log) { return configuration.PersistentStorage ?? new DefaultPersistentStorage(log); diff --git a/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj b/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj index 8ba9099b..c583dd45 100644 --- a/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj +++ b/src/LaunchDarkly.ClientSdk/LaunchDarkly.ClientSdk.csproj @@ -3,7 +3,7 @@ 2.0.0-alpha.1 - netstandard2.0;xamarin.ios10;monoandroid81 + netstandard2.0;xamarin.ios10;monoandroid71;monoandroid80;monoandroid81 $(BaseTargetFrameworks);net452 $(BaseTargetFrameworks) $(LD_TARGET_FRAMEWORKS) @@ -69,4 +69,9 @@ + + + Code + + diff --git a/src/LaunchDarkly.ClientSdk/LdClient.cs b/src/LaunchDarkly.ClientSdk/LdClient.cs index 9bfabb3f..3a72ebbe 100644 --- a/src/LaunchDarkly.ClientSdk/LdClient.cs +++ b/src/LaunchDarkly.ClientSdk/LdClient.cs @@ -35,7 +35,7 @@ public sealed class LdClient : ILdClient readonly IBackgroundModeManager _backgroundModeManager; readonly IDeviceInfo deviceInfo; readonly IConnectivityStateManager _connectivityStateManager; - readonly IEventProcessor eventProcessor; + readonly IEventProcessor _eventProcessor; readonly IFlagCacheManager flagCacheManager; internal readonly IFlagChangedEventManager flagChangedEventManager; // exposed for testing readonly IPersistentStorage persister; @@ -164,18 +164,20 @@ public event EventHandler FlagChanged { _log.Debug("Setting online to {0} due to a connectivity change event", networkAvailable); _ = _connectionManager.SetNetworkEnabled(networkAvailable); // do not await the result - eventProcessor.SetOffline(!networkAvailable || _connectionManager.ForceOffline); + _eventProcessor.SetOffline(!networkAvailable || _connectionManager.ForceOffline); }; var isConnected = _connectivityStateManager.IsConnected; _connectionManager.SetNetworkEnabled(isConnected); - eventProcessor = Factory.CreateEventProcessor(configuration, _log); - eventProcessor.SetOffline(configuration.Offline || !isConnected); + _eventProcessor = (configuration.EventProcessorFactory ?? Components.SendEvents()) + .CreateEventProcessor(_context); + _eventProcessor.SetOffline(configuration.Offline || !isConnected); // Send an initial identify event, but only if we weren't explicitly set to be offline + if (!configuration.Offline) { - eventProcessor.RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent + _eventProcessor.RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent { Timestamp = UnixMillisecondTime.Now, User = user @@ -344,7 +346,7 @@ public bool SetOffline(bool value, TimeSpan maxWaitTime) /// public async Task SetOfflineAsync(bool value) { - eventProcessor.SetOffline(value || !_connectionManager.NetworkEnabled); + _eventProcessor.SetOffline(value || !_connectionManager.NetworkEnabled); await _connectionManager.SetForceOffline(value); } @@ -469,10 +471,7 @@ EvaluationDetail errorResult(EvaluationErrorKind kind) => private void SendEvaluationEventIfOnline(EventProcessorTypes.EvaluationEvent e) { - if (!_connectionManager.ForceOffline) - { - eventProcessor.RecordEvaluationEvent(e); - } + EventProcessorIfEnabled().RecordEvaluationEvent(e); } /// @@ -485,32 +484,26 @@ public IDictionary AllFlags() /// public void Track(string eventName, LdValue data, double metricValue) { - if (!_connectionManager.ForceOffline) + EventProcessorIfEnabled().RecordCustomEvent(new EventProcessorTypes.CustomEvent { - eventProcessor.RecordCustomEvent(new EventProcessorTypes.CustomEvent - { - Timestamp = UnixMillisecondTime.Now, - EventKey = eventName, - User = User, - Data = data, - MetricValue = metricValue - }); - } + Timestamp = UnixMillisecondTime.Now, + EventKey = eventName, + User = User, + Data = data, + MetricValue = metricValue + }); } /// public void Track(string eventName, LdValue data) { - if (!_connectionManager.ForceOffline) + EventProcessorIfEnabled().RecordCustomEvent(new EventProcessorTypes.CustomEvent { - eventProcessor.RecordCustomEvent(new EventProcessorTypes.CustomEvent - { - Timestamp = UnixMillisecondTime.Now, - EventKey = eventName, - User = User, - Data = data - }); - } + Timestamp = UnixMillisecondTime.Now, + EventKey = eventName, + User = User, + Data = data + }); } /// @@ -522,7 +515,7 @@ public void Track(string eventName) /// public void Flush() { - eventProcessor.Flush(); // eventProcessor will ignore this if it is offline + _eventProcessor.Flush(); // eventProcessor will ignore this if it is offline } /// @@ -553,22 +546,19 @@ public async Task IdentifyAsync(User user) _user = newUser; }); - if (!_connectionManager.ForceOffline) + EventProcessorIfEnabled().RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent { - eventProcessor.RecordIdentifyEvent(new EventProcessorTypes.IdentifyEvent + Timestamp = UnixMillisecondTime.Now, + User = user + }); + if (oldUser.Anonymous && !newUser.Anonymous && !_config.AutoAliasingOptOut) + { + EventProcessorIfEnabled().RecordAliasEvent(new EventProcessorTypes.AliasEvent { Timestamp = UnixMillisecondTime.Now, - User = user + User = user, + PreviousUser = oldUser }); - if (oldUser.Anonymous && !newUser.Anonymous && !_config.AutoAliasingOptOut) - { - eventProcessor.RecordAliasEvent(new EventProcessorTypes.AliasEvent - { - Timestamp = UnixMillisecondTime.Now, - User = user, - PreviousUser = oldUser - }); - } } return await _connectionManager.SetDataSourceConstructor( @@ -588,14 +578,11 @@ public void Alias(User user, User previousUser) { throw new ArgumentNullException(nameof(previousUser)); } - if (!_connectionManager.ForceOffline) + EventProcessorIfEnabled().RecordAliasEvent(new EventProcessorTypes.AliasEvent { - eventProcessor.RecordAliasEvent(new EventProcessorTypes.AliasEvent - { - User = user, - PreviousUser = previousUser - }); - } + User = user, + PreviousUser = previousUser + }); } User DecorateUser(User user) @@ -656,7 +643,7 @@ void Dispose(bool disposing) _backgroundModeManager.BackgroundModeChanged -= OnBackgroundModeChanged; _connectionManager.Dispose(); - eventProcessor.Dispose(); + _eventProcessor.Dispose(); // Reset the static Instance to null *if* it was referring to this instance DetachInstance(); @@ -703,6 +690,12 @@ await _connectionManager.SetDataSourceConstructor( ); } + // Returns our configured event processor (which might be the null implementation, if configured + // with NoEvents)-- or, a stub if we have been explicitly put offline. This way, during times + // when the application does not want any network activity, we won't bother buffering events. + internal IEventProcessor EventProcessorIfEnabled() => + Offline ? ComponentsImpl.NullEventProcessor.Instance : _eventProcessor; + internal Func MakeDataSourceConstructor(User user, bool background) { return () => _dataSourceFactory.CreateDataSource( diff --git a/tests/LaunchDarkly.ClientSdk.Tests/BuilderBehavior.cs b/tests/LaunchDarkly.ClientSdk.Tests/BuilderBehavior.cs new file mode 100644 index 00000000..18f0b48f --- /dev/null +++ b/tests/LaunchDarkly.ClientSdk.Tests/BuilderBehavior.cs @@ -0,0 +1,283 @@ +using System; +using Xunit; + +// THIS CODE WILL BE MOVED into the dotnet-test-helpers project where it can be shared + +namespace LaunchDarkly.TestHelpers +{ + /// + /// Factories for helper classes that provide useful test patterns for builder types. + /// + /// + /// + /// These helpers make it easier to provide thorough test coverage of builder types. + /// It is easy when implementing builders to make basic mistakes like not setting the + /// right property in a setter, not enforcing desired constraints, or not copying all + /// the properties in a copy constructor. + /// + /// + /// The general pattern consists of creating a generic helper for the builder type and + /// the type that it builds, then creating a property helper for each settable property + /// and performing standard assertions with it. Example: + /// + ///

+    ///     // This assumes there is a type MyBuilder whose Build method creates an
+    ///     // instance of MyType, with properties Height and Weight.
+    ///     
+    ///     var tester = BuilderBehavior.For(() => new MyBuilder(), b => b.Build());
+    ///
+    ///     var height = tester.Property(x => x.Height, b => (b, value) => b.Height(value));
+    ///     height.AssertDefault(DefaultHeight);
+    ///     height.AssertCanSet(72);
+    ///     height.AssertSetIsChangedTo(-1, 0); // setter should enforce minimum height of 0
+    ///
+    ///     var weight = tester.Property(x => x.Weight, b => (b, value) => b.Weight(value));
+    ///     weight.AssertDefault(DefaultWeight);
+    ///     weight.AssertCanSet(200);
+    ///     weight.AssertSetIsChangedTo(-1, 0); // setter should enforce minimum weight of 0
+    /// 
+ /// + /// It uses Xunit assertion methods. + /// + ///
+ public static class BuilderBehavior + { + /// + /// Provides a generic for testing + /// methods of a builder against properties of the type it builds. + /// + /// the builder type + /// the type that it builds + /// function that constructs a + /// function that creates a + /// from a + /// a instance + public static BuildTester For( + Func constructor, Func buildMethod) => + new BuildTester(constructor, buildMethod, null); + + /// + /// Provides a generic for testing + /// methods of a builder against the builder's own internal state. + /// + /// + /// This can be used in cases where it is not feasible for the test code to actually + /// call the builder's build method, for instance if it has unwanted side effects. + /// + /// the builder type + /// function that constructs a + /// an instance + // Use this when we want to test the builder's internal state directly, without + // calling Build - i.e. if the object is difficult to inspect after it's built. + public static InternalStateTester For(Func constructor) => + new InternalStateTester(constructor); + + /// + /// Helper class that provides useful test patterns for a builder type and the + /// type that it builds. + /// + /// + /// Create instances of this class with + /// . + /// + /// the builder type + /// the type that it builds + public sealed class BuildTester + { + private readonly Func _constructor; + internal readonly Func _buildMethod; + internal readonly Func _copyConstructor; + + internal BuildTester(Func constructor, + Func buildMethod, + Func copyConstructor + ) + { + _constructor = constructor; + _buildMethod = buildMethod; + _copyConstructor = copyConstructor; + } + + /// + /// Creates a helper for testing a specific property of the builder. + /// + /// type of the property + /// function that gets that property from the built object + /// function that sets the property in the builder + /// + public IPropertyAssertions Property( + Func getter, + Action builderSetter + ) => + new BuildTesterProperty( + this, getter, builderSetter); + + /// + /// Creates an instance of the builder. + /// + /// a new instance + public TBuilder New() => _constructor(); + + /// + /// Adds the ability to test the builder's copy constructor. + /// + /// + /// The effect of this is that all + /// assertions created from the resulting helper will also verify that copying + /// the builder also copies the value of this property. + /// + /// function that should create a new builder with an + /// identical state to the existing one + /// a copy of the BuilderTestHelper with this additional behavior + public BuildTester WithCopyConstructor( + Func copyConstructor + ) => + new BuildTester(_constructor, _buildMethod, copyConstructor); + } + + /// + /// Similar to , but instead of testing the values of + /// properties in the built object, it inspects the builder directly. + /// + /// + /// Create instances of this class with . + /// + /// the builder type + public class InternalStateTester + { + private readonly Func _constructor; + + internal InternalStateTester(Func constructor) + { + _constructor = constructor; + } + + /// + /// Creates a helper for testing a specific property of the builder. + /// + /// type of the property + /// function that gets that property from the builder's internal state + /// function that sets the property in the builder + /// + public IPropertyAssertions Property( + Func builderGetter, + Action builderSetter + ) => + new InternalStateTesterProperty(this, + builderGetter, builderSetter); + + /// + /// Creates an instance of the builder. + /// + /// a new instance + public TBuilder New() => _constructor(); + } + + /// + /// Assertions provided by the property-specific helpers. + /// + /// type of the property + public interface IPropertyAssertions + { + /// + /// Asserts that the property has the expected value when it has not been set. + /// + /// the expected value + void AssertDefault(TValue defaultValue); + + /// + /// Asserts that calling the setter for a specific value causes the property + /// to have that value. + /// + /// the expected value + void AssertCanSet(TValue newValue); + + /// + /// Asserts that calling the setter for a specific value causes the property + /// to have another specific value for the corresponding property. + /// + /// the value to pass to the setter + /// the expected result value + void AssertSetIsChangedTo(TValue attemptedValue, TValue resultingValue); + } + + internal class BuildTesterProperty : IPropertyAssertions + { + private readonly BuildTester _owner; + private readonly Func _getter; + private readonly Action _builderSetter; + + internal BuildTesterProperty(BuildTester owner, + Func getter, + Action builderSetter) + { + _owner = owner; + _getter = getter; + _builderSetter = builderSetter; + } + + public void AssertDefault(TValue defaultValue) + { + var b = _owner.New(); + AssertValue(b, defaultValue); + } + + public void AssertCanSet(TValue newValue) + { + AssertSetIsChangedTo(newValue, newValue); + } + + public void AssertSetIsChangedTo(TValue attemptedValue, TValue resultingValue) + { + var b = _owner.New(); + _builderSetter(b, attemptedValue); + AssertValue(b, resultingValue); + } + + private void AssertValue(TBuilder b, TValue v) + { + var o = _owner._buildMethod(b); + Assert.Equal(v, _getter(o)); + if (_owner._copyConstructor != null) + { + var b1 = _owner._copyConstructor(o); + var o1 = _owner._buildMethod(b1); + Assert.Equal(v, _getter(o1)); + } + } + } + + internal class InternalStateTesterProperty : IPropertyAssertions + { + private readonly InternalStateTester _owner; + private readonly Func _builderGetter; + private readonly Action _builderSetter; + + internal InternalStateTesterProperty(InternalStateTester owner, + Func builderGetter, + Action builderSetter) + { + _owner = owner; + _builderGetter = builderGetter; + _builderSetter = builderSetter; + } + + public void AssertDefault(TValue defaultValue) + { + Assert.Equal(defaultValue, _builderGetter(_owner.New())); + } + + public void AssertCanSet(TValue newValue) + { + AssertSetIsChangedTo(newValue, newValue); + } + + public void AssertSetIsChangedTo(TValue attemptedValue, TValue resultingValue) + { + var b = _owner.New(); + _builderSetter(b, attemptedValue); + Assert.Equal(resultingValue, _builderGetter(b)); + } + } + } +} diff --git a/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs index 01b78307..fefefe59 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/ConfigurationTest.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Immutable; using System.Net.Http; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Client.Internal; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -10,8 +10,8 @@ namespace LaunchDarkly.Sdk.Client { public class ConfigurationTest : BaseTest { - private readonly BuilderTestUtil _tester = - BuilderTestUtil.For(() => Configuration.Builder(mobileKey), b => b.Build()) + private readonly BuilderBehavior.BuildTester _tester = + BuilderBehavior.For(() => Configuration.Builder(mobileKey), b => b.Build()) .WithCopyConstructor(c => Configuration.Builder(c)); const string mobileKey = "any-key"; @@ -49,40 +49,27 @@ public void DataSource() } [Fact] - public void AllAttributesPrivate() + public void EnableBackgroundUpdating() { - var prop = _tester.Property(b => b.AllAttributesPrivate, (b, v) => b.AllAttributesPrivate(v)); - prop.AssertDefault(false); - prop.AssertCanSet(true); - } - - [Fact] - public void EventCapacity() - { - var prop = _tester.Property(b => b.EventCapacity, (b, v) => b.EventCapacity(v)); - prop.AssertDefault(Configuration.DefaultEventCapacity); - prop.AssertCanSet(1); - prop.AssertSetIsChangedTo(0, Configuration.DefaultEventCapacity); - prop.AssertSetIsChangedTo(-1, Configuration.DefaultEventCapacity); + var prop = _tester.Property(c => c.EnableBackgroundUpdating, (b, v) => b.EnableBackgroundUpdating(v)); + prop.AssertDefault(true); + prop.AssertCanSet(false); } [Fact] - public void EventsUri() + public void EvaluationReasons() { - var prop = _tester.Property(b => b.EventsUri, (b, v) => b.EventsUri(v)); - prop.AssertDefault(Configuration.DefaultEventsUri); - prop.AssertCanSet(new Uri("http://x")); - prop.AssertSetIsChangedTo(null, Configuration.DefaultEventsUri); + var prop = _tester.Property(c => c.EvaluationReasons, (b, v) => b.EvaluationReasons(v)); + prop.AssertDefault(false); + prop.AssertCanSet(true); } [Fact] - public void FlushInterval() + public void Events() { - var prop = _tester.Property(b => b.EventFlushInterval, (b, v) => b.EventFlushInterval(v)); - prop.AssertDefault(Configuration.DefaultEventFlushInterval); - prop.AssertCanSet(TimeSpan.FromMinutes(7)); - prop.AssertSetIsChangedTo(TimeSpan.Zero, Configuration.DefaultEventFlushInterval); - prop.AssertSetIsChangedTo(TimeSpan.FromMilliseconds(-1), Configuration.DefaultEventFlushInterval); + var prop = _tester.Property(c => c.EventProcessorFactory, (b, v) => b.Events(v)); + prop.AssertDefault(null); + prop.AssertCanSet(new ComponentsImpl.NullEventProcessorFactory()); } [Fact] @@ -93,14 +80,6 @@ public void HttpMessageHandler() prop.AssertCanSet(new HttpClientHandler()); } - [Fact] - public void InlineUsersInEvents() - { - var prop = _tester.Property(b => b.InlineUsersInEvents, (b, v) => b.InlineUsersInEvents(v)); - prop.AssertDefault(false); - prop.AssertCanSet(true); - } - [Fact] public void Logging() { @@ -134,15 +113,11 @@ public void Offline() } [Fact] - public void PrivateAttributes() + public void PersistFlagValues() { - var b = _tester.New(); - Assert.Null(b.Build().PrivateAttributeNames); - b.PrivateAttribute(UserAttribute.Name); - b.PrivateAttribute(UserAttribute.Email); - b.PrivateAttribute(UserAttribute.ForName("other")); - Assert.Equal(ImmutableHashSet.Create( - UserAttribute.Name, UserAttribute.Email, UserAttribute.ForName("other")), b.Build().PrivateAttributeNames); + var prop = _tester.Property(c => c.PersistFlagValues, (b, v) => b.PersistFlagValues(v)); + prop.AssertDefault(true); + prop.AssertCanSet(false); } [Fact] diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/EventProcessorBuilderTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/EventProcessorBuilderTest.cs new file mode 100644 index 00000000..985c691d --- /dev/null +++ b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/EventProcessorBuilderTest.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using LaunchDarkly.TestHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace LaunchDarkly.Sdk.Client.Integrations +{ + public class EventProcessorBuilderTest : BaseTest + { + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.SendEvents); + + public EventProcessorBuilderTest(ITestOutputHelper testOutput) : base(testOutput) { } + + [Fact] + public void AllAttributesPrivate() + { + var prop = _tester.Property(b => b._allAttributesPrivate, (b, v) => b.AllAttributesPrivate(v)); + prop.AssertDefault(false); + prop.AssertCanSet(true); + } + + [Fact] + public void EventCapacity() + { + var prop = _tester.Property(b => b._capacity, (b, v) => b.Capacity(v)); + prop.AssertDefault(EventProcessorBuilder.DefaultCapacity); + prop.AssertCanSet(1); + prop.AssertSetIsChangedTo(0, EventProcessorBuilder.DefaultCapacity); + prop.AssertSetIsChangedTo(-1, EventProcessorBuilder.DefaultCapacity); + } + + [Fact] + public void EventsUri() + { + var prop = _tester.Property(b => b._baseUri, (b, v) => b.BaseUri(v)); + prop.AssertDefault(null); + prop.AssertCanSet(new Uri("http://x")); + } + + [Fact] + public void FlushInterval() + { + var prop = _tester.Property(b => b._flushInterval, (b, v) => b.FlushInterval(v)); + prop.AssertDefault(EventProcessorBuilder.DefaultFlushInterval); + prop.AssertCanSet(TimeSpan.FromMinutes(7)); + prop.AssertSetIsChangedTo(TimeSpan.Zero, EventProcessorBuilder.DefaultFlushInterval); + prop.AssertSetIsChangedTo(TimeSpan.FromMilliseconds(-1), EventProcessorBuilder.DefaultFlushInterval); + } + + [Fact] + public void InlineUsersInEvents() + { + var prop = _tester.Property(b => b._inlineUsersInEvents, (b, v) => b.InlineUsersInEvents(v)); + prop.AssertDefault(false); + prop.AssertCanSet(true); + } + + [Fact] + public void PrivateAttributes() + { + var b = _tester.New(); + Assert.Empty(b._privateAttributes); + b.PrivateAttributes(UserAttribute.Name); + b.PrivateAttributes(UserAttribute.Email, UserAttribute.ForName("other")); + b.PrivateAttributeNames("country"); + Assert.Equal( + new HashSet + { + UserAttribute.Name, UserAttribute.Email, UserAttribute.Country, UserAttribute.ForName("other") + }, + b._privateAttributes); + } + } +} \ No newline at end of file diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs index 76d0a579..ea757975 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs @@ -1,12 +1,13 @@ using System; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Client.Integrations { public class PollingDataSourceBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(Components.PollingDataSource); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.PollingDataSource); [Fact] public void BackgroundPollInterval() diff --git a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs index 656ae502..3b07f67e 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs @@ -1,12 +1,13 @@ using System; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Client.Integrations { public class StreamingDataSourceBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(Components.StreamingDataSource); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.StreamingDataSource); [Fact] public void BackgroundPollInterval() diff --git a/tests/LaunchDarkly.ClientSdk.Tests/LDClientEndToEndTests.cs b/tests/LaunchDarkly.ClientSdk.Tests/LDClientEndToEndTests.cs index e64587e1..e7d86ac4 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/LDClientEndToEndTests.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/LDClientEndToEndTests.cs @@ -266,7 +266,7 @@ string expectedPath { var config = Configuration.Builder(_mobileKey) .DataSource(MockPollingProcessor.Factory("{}")) - .EventsUri(new Uri(server.Uri.ToString().TrimEnd('/') + baseUriExtraPath)) + .Events(Components.SendEvents().BaseUri(new Uri(server.Uri.ToString().TrimEnd('/') + baseUriExtraPath))) .PersistFlagValues(false) .Build(); @@ -480,7 +480,7 @@ public void DateLikeStringValueIsStillParsedAsString(UpdateMode mode) private Configuration BaseConfig(Func extraConfig = null) { var builderInternal = Configuration.Builder(_mobileKey) - .EventProcessor(new MockEventProcessor()); + .Events(new SingleEventProcessorFactory(new MockEventProcessor())); builderInternal .Logging(testLogging) .PersistFlagValues(false); // unless we're specifically testing flag caching, this helps to prevent test state contamination diff --git a/tests/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs index 5644cbd0..3f41bb7d 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs @@ -1,8 +1,9 @@ using System; +using LaunchDarkly.Sdk.Client.Interfaces; using Xunit; using Xunit.Abstractions; -using static LaunchDarkly.Sdk.Client.Internal.Events.EventProcessorTypes; +using static LaunchDarkly.Sdk.Client.Interfaces.EventProcessorTypes; namespace LaunchDarkly.Sdk.Client { @@ -10,13 +11,16 @@ public class LdClientEventTests : BaseTest { private static readonly User user = User.WithKey("userkey"); private MockEventProcessor eventProcessor = new MockEventProcessor(); + private IEventProcessorFactory _factory; - public LdClientEventTests(ITestOutputHelper testOutput) : base(testOutput) { } + public LdClientEventTests(ITestOutputHelper testOutput) : base(testOutput) { + _factory = new SingleEventProcessorFactory(eventProcessor); + } public LdClient MakeClient(User user, string flagsJson) { var config = TestUtil.ConfigWithFlagsJson(user, "appkey", flagsJson); - config.EventProcessor(eventProcessor).Logging(testLogging); + config.Events(_factory).Logging(testLogging); return TestUtil.CreateClient(config.Build(), user); } @@ -138,7 +142,7 @@ public void IdentifyDoesNotSendAliasEventIfOptedOUt() User newUser = User.WithKey("real-key"); var config = TestUtil.ConfigWithFlagsJson(oldUser, "appkey", "{}"); - config.EventProcessor(eventProcessor).Logging(testLogging); + config.Events(_factory).Logging(testLogging); config.AutoAliasingOptOut(true); using (LdClient client = TestUtil.CreateClient(config.Build(), oldUser)) @@ -270,7 +274,7 @@ public void VariationSendsFeatureEventForUnknownFlagWhenClientIsNotInitialized() { var config = TestUtil.ConfigWithFlagsJson(user, "appkey", "{}") .DataSource(MockUpdateProcessorThatNeverInitializes.Factory()) - .EventProcessor(eventProcessor) + .Events(_factory) .Logging(testLogging); using (LdClient client = TestUtil.CreateClient(config.Build(), user)) @@ -378,7 +382,7 @@ public void VariationSendsFeatureEventWithReasonForUnknownFlagWhenClientIsNotIni { var config = TestUtil.ConfigWithFlagsJson(user, "appkey", "{}") .DataSource(MockUpdateProcessorThatNeverInitializes.Factory()) - .EventProcessor(eventProcessor) + .Events(_factory) .Logging(testLogging); using (LdClient client = TestUtil.CreateClient(config.Build(), user)) diff --git a/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs b/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs index 35093d53..62bff46f 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/LdClientTests.cs @@ -467,7 +467,7 @@ public void EventProcessorIsOnlineByDefault() { var eventProcessor = new MockEventProcessor(); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") - .EventProcessor(eventProcessor) + .Events(new SingleEventProcessorFactory(eventProcessor)) .Logging(testLogging) .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) @@ -483,7 +483,7 @@ public void EventProcessorIsOfflineWhenClientIsConfiguredOffline() var eventProcessor = new MockEventProcessor(); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") .ConnectivityStateManager(connectivityStateManager) - .EventProcessor(eventProcessor) + .Events(new SingleEventProcessorFactory(eventProcessor)) .Offline(true) .Logging(testLogging) .Build(); @@ -513,7 +513,7 @@ public void EventProcessorIsOfflineWhenNetworkIsUnavailable() var eventProcessor = new MockEventProcessor(); var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") .ConnectivityStateManager(connectivityStateManager) - .EventProcessor(eventProcessor) + .Events(new SingleEventProcessorFactory(eventProcessor)) .Logging(testLogging) .Build(); using (var client = TestUtil.CreateClient(config, simpleUser)) diff --git a/tests/LaunchDarkly.ClientSdk.Tests/MockComponents.cs b/tests/LaunchDarkly.ClientSdk.Tests/MockComponents.cs index e8c4e1bd..b246d900 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/MockComponents.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/MockComponents.cs @@ -100,6 +100,18 @@ public void RecordAliasEvent(EventProcessorTypes.AliasEvent e) => Events.Add(e); } + internal class SingleEventProcessorFactory : IEventProcessorFactory + { + private readonly IEventProcessor _instance; + + public SingleEventProcessorFactory(IEventProcessor instance) + { + _instance = instance; + } + + public IEventProcessor CreateEventProcessor(LdClientContext context) => _instance; + } + internal class MockFeatureFlagRequestor : IFeatureFlagRequestor { private readonly string _jsonFlags; diff --git a/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs b/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs index d6b1ca4a..36bfa929 100644 --- a/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.ClientSdk.Tests/TestUtil.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using LaunchDarkly.Sdk.Client.Interfaces; using LaunchDarkly.Sdk.Client.Internal; using LaunchDarkly.Sdk.Client.Internal.DataStores; using LaunchDarkly.Sdk.Client.Internal.Interfaces; @@ -19,6 +20,8 @@ public static class TestUtil private static ThreadLocal InClientLock = new ThreadLocal(); + public static LdClientContext SimpleContext => new LdClientContext(Configuration.Default("key")); + public static T WithClientLock(Func f) { // This cumbersome combination of a ThreadLocal and a SemaphoreSlim is simply because 1. we have to use @@ -144,7 +147,7 @@ internal static ConfigurationBuilder ConfigWithFlagsJson(User user, string appKe return Configuration.Builder(appKey) .FlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) .ConnectivityStateManager(new MockConnectivityStateManager(true)) - .EventProcessor(new MockEventProcessor()) + .Events(new SingleEventProcessorFactory(new MockEventProcessor())) .DataSource(MockPollingProcessor.Factory(null)) .PersistentStorage(new MockPersistentStorage()) .DeviceInfo(new MockDeviceInfo());