+ /// var config = Configuration.Builder(mobileKey)
+ /// .Events(Components.NoEvents)
+ /// .Build();
+ ///
+ ///
- /// 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());