private void TestDiagnosticConfig( Func <ConfigurationBuilder, ConfigurationBuilder> modConfig, Func <EventProcessorBuilder, EventProcessorBuilder> modEvents, LdValue.ObjectBuilder expected ) { var eventsBuilder = Components.SendEvents() .EventSender(testEventSender); modEvents?.Invoke(eventsBuilder); var configBuilder = BasicConfig() .DataSource(null) .Events(eventsBuilder) .Http(Components.HttpConfiguration().MessageHandler(new StubMessageHandler(HttpStatusCode.Unauthorized))) .StartWaitTime(testStartWaitTime); configBuilder = modConfig is null ? configBuilder : modConfig(configBuilder); using (var client = new LdClient(configBuilder.Build())) { var payload = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); var data = JsonOf(payload.Data); AssertJsonEqual(JsonFromValue("diagnostic-init"), data.Property("kind")); AssertJsonEqual(JsonOf(expected.Build().ToJsonString()), data.Property("configuration")); } }
public void DiagnosticInitEventIsSent() { var config = BasicConfig() .Events(Components.SendEvents().EventSender(testEventSender)) .Http( Components.HttpConfiguration().Wrapper(testWrapperName, testWrapperVersion) ) .Build(); using (var client = new LdClient(config)) { var payload = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); var data = JsonOf(payload.Data); AssertJsonEqual(JsonFromValue("diagnostic-init"), data.Property("kind")); AssertJsonEqual(JsonFromValue("dotnet"), data.RequiredProperty("platform").Property("name")); AssertJsonEqual(JsonFromValue(ServerDiagnosticStore.GetDotNetTargetFramework()), data.RequiredProperty("platform").Property("dotNetTargetFramework")); AssertJsonEqual(expectedSdk, data.Property("sdk")); AssertJsonEqual(JsonFromValue("dk-key"), data.Property("id").Property("sdkKeySuffix")); data.RequiredProperty("creationDate"); } }
public void DiagnosticInitEventIsSent() { var config = Configuration.Builder(sdkKey) .DataSource(Components.ExternalUpdatesOnly) .Events(Components.SendEvents().EventSender(testEventSender)) .Http( Components.HttpConfiguration().Wrapper(testWrapperName, testWrapperVersion) ) .Logging(Components.Logging(testLogging)) .Build(); using (var client = new LdClient(config)) { var payload = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); var data = LdValue.Parse(payload.Data); Assert.Equal("diagnostic-init", data.Get("kind").AsString); AssertHelpers.JsonEqual(ExpectedPlatform(), data.Get("platform")); AssertHelpers.JsonEqual(expectedSdk, data.Get("sdk")); Assert.Equal("DK_KEY", data.Get("id").Get("sdkKeySuffix").AsString); var timestamp = data.Get("creationDate").AsLong; Assert.NotEqual(0, timestamp); } }
private void TestDiagnosticConfig( Func <ConfigurationBuilder, ConfigurationBuilder> modConfig, Func <EventProcessorBuilder, EventProcessorBuilder> modEvents, LdValue.ObjectBuilder expected ) { var eventsBuilder = Components.SendEvents() .EventSender(testEventSender); modEvents?.Invoke(eventsBuilder); var configBuilder = Configuration.Builder(sdkKey) .Events(eventsBuilder) .Http(Components.HttpConfiguration().MessageHandler(new StubMessageHandler(HttpStatusCode.Unauthorized))) .Logging(Components.Logging(testLogging)) .StartWaitTime(testStartWaitTime); modConfig?.Invoke(configBuilder); using (var client = new LdClient(configBuilder.Build())) { var payload = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); var data = LdValue.Parse(payload.Data); Assert.Equal("diagnostic-init", data.Get("kind").AsString); AssertHelpers.JsonEqual(expected.Build(), data.Get("configuration")); } }
public void DefaultEventsBaseUri() { using (var client = new LdClient( BasicConfig() .Events(Components.SendEvents()) .Http(Components.HttpConfiguration().MessageHandler(_stubHandler)) .Build())) { var req = _stubHandler.Requests.ExpectValue(); Assert.Equal(StandardEndpoints.DefaultEventsBaseUri, BaseUriOf(req.RequestUri)); } }
public void NoDiagnosticInitEventIsSentIfOptedOut() { var config = BasicConfig() .DiagnosticOptOut(true) .Events(Components.SendEvents().EventSender(testEventSender)) .Build(); using (var client = new LdClient(config)) { testEventSender.RequireNoPayloadSent(TimeSpan.FromMilliseconds(100)); } }
public void NoDiagnosticInitEventIsSentIfOptedOut() { var config = Configuration.Builder(sdkKey) .DiagnosticOptOut(true) .DataSource(Components.ExternalUpdatesOnly) .Events(Components.SendEvents().EventSender(testEventSender)) .Logging(Components.Logging(testLogging)) .Build(); using (var client = new LdClient(config)) { testEventSender.RequireNoPayloadSent(TimeSpan.FromMilliseconds(100)); } }
public void CustomEventsBaseUriWithDeprecatedMethod() { using (var client = new LdClient( BasicConfig() .Events(Components.SendEvents().BaseUri(CustomUri)) .Http(Components.HttpConfiguration().MessageHandler(_stubHandler)) .Build())) { var req = _stubHandler.Requests.ExpectValue(); Assert.Equal(CustomUri, BaseUriOf(req.RequestUri)); AssertLogMessageRegex(false, LogLevel.Error, "You have set custom ServiceEndpoints without specifying"); } }
public void ErrorIsLoggedIfANecessaryUriIsNotSetWhenOtherCustomUrisAreSet() { var logCapture1 = Logs.Capture(); using (var client = new LdClient( BasicConfig() .DataSource(Components.StreamingDataSource()) .Http(Components.HttpConfiguration().MessageHandler(_stubHandler)) .Logging(logCapture1) .ServiceEndpoints(Components.ServiceEndpoints().Polling(CustomUri)) .Build())) { Assert.True(logCapture1.HasMessageWithRegex(LogLevel.Error, "You have set custom ServiceEndpoints without specifying the Streaming base URI")); } var logCapture2 = Logs.Capture(); using (var client = new LdClient( BasicConfig() .DataSource(Components.PollingDataSource()) .Http(Components.HttpConfiguration().MessageHandler(_stubHandler)) .Logging(logCapture2) .ServiceEndpoints(Components.ServiceEndpoints().Events(CustomUri)) .Build())) { Assert.True(logCapture2.HasMessageWithRegex(LogLevel.Error, "You have set custom ServiceEndpoints without specifying the Polling base URI")); } var logCapture3 = Logs.Capture(); using (var client = new LdClient( BasicConfig() .Events(Components.SendEvents()) .Http(Components.HttpConfiguration().MessageHandler(_stubHandler)) .Logging(logCapture3) .ServiceEndpoints(Components.ServiceEndpoints().Streaming(CustomUri)) .Build())) { Assert.True(logCapture3.HasMessageWithRegex(LogLevel.Error, "You have set custom ServiceEndpoints without specifying the Events base URI")); } }
public void DiagnosticPeriodicEventsAreSent() { var config = Configuration.Builder(sdkKey) .DataSource(Components.ExternalUpdatesOnly) .Events(Components.SendEvents() .EventSender(testEventSender) .DiagnosticRecordingIntervalNoMinimum(TimeSpan.FromMilliseconds(50))) .Logging(Components.Logging(testLogging)) .Build(); using (var client = new LdClient(config)) { var payload1 = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload1.Kind); Assert.Equal(1, payload1.EventCount); var data1 = LdValue.Parse(payload1.Data); Assert.Equal("diagnostic-init", data1.Get("kind").AsString); var timestamp1 = data1.Get("creationDate").AsLong; Assert.NotEqual(0, timestamp1); var payload2 = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload2.Kind); Assert.Equal(1, payload2.EventCount); var data2 = LdValue.Parse(payload2.Data); Assert.Equal("diagnostic", data2.Get("kind").AsString); var timestamp2 = data2.Get("creationDate").AsLong; Assert.InRange(timestamp2, timestamp1, timestamp1 + 1000); var payload3 = testEventSender.RequirePayload(); Assert.Equal(EventDataKind.DiagnosticEvent, payload3.Kind); Assert.Equal(1, payload3.EventCount); var data3 = LdValue.Parse(payload3.Data); Assert.Equal("diagnostic", data3.Get("kind").AsString); var timestamp3 = data2.Get("creationDate").AsLong; Assert.InRange(timestamp3, timestamp2, timestamp1 + 1000); } }
public void HttpConfigurationIsAppliedToEvents() { TestHttpUtils.TestWithSpecialHttpConfigurations( EventsAcceptedResponse, (targetUri, httpConfig, server) => { var config = BasicConfig() .DiagnosticOptOut(true) .Events(Components.SendEvents()) .Http(httpConfig) .ServiceEndpoints(Components.ServiceEndpoints().Events(targetUri)) .StartWaitTime(TimeSpan.FromSeconds(5)) .Build(); using (var client = new LdClient(config)) { client.Identify(User.WithKey("userkey")); client.Flush(); server.Recorder.RequireRequest(); } }, TestLogger ); }
public void EventsAreSentToCorrectEndpoints( string baseUriExtraPath, string expectedBasePath ) { using (var server = HttpServer.Start(EventsAcceptedResponse)) { var baseUri = server.Uri.ToString().TrimEnd('/') + baseUriExtraPath; var config = BasicConfig() .Events(Components.SendEvents()) .ServiceEndpoints(Components.ServiceEndpoints().Events(baseUri)) .StartWaitTime(TimeSpan.FromSeconds(5)) .Build(); using (var client = new LdClient(config)) { client.Identify(User.WithKey("userkey")); client.Flush(); var request1 = server.Recorder.RequireRequest(); var request2 = server.Recorder.RequireRequest(); if (request1.Path.EndsWith("diagnostic")) { var temp = request1; request1 = request2; request2 = temp; } Assert.Equal("POST", request1.Method.ToUpper()); Assert.Equal(expectedBasePath + "/bulk", request1.Path); Assert.Equal("POST", request2.Method.ToUpper()); Assert.Equal(expectedBasePath + "/diagnostic", request2.Path); } } }
/// <summary> /// Creates a new client to connect to LaunchDarkly with a custom configuration. /// </summary> /// <remarks> /// <para> /// Applications should instantiate a single instance for the lifetime of the application. In /// unusual cases where an application needs to evaluate feature flags from different LaunchDarkly /// projects or environments, you may create multiple clients, but they should still be retained /// for the lifetime of the application rather than created per request or per thread. /// </para> /// <para> /// Normally, the client will begin attempting to connect to LaunchDarkly as soon as you call the /// constructor. The constructor returns as soon as any of the following things has happened: /// </para> /// <list type="number"> /// <item><description> It has successfully connected to LaunchDarkly and received feature flag data. In this /// case, <see cref="Initialized"/> will be true, and the <see cref="DataSourceStatusProvider"/> /// will return a state of <see cref="DataSourceState.Valid"/>. </description></item> /// <item><description> It has not succeeded in connecting within the <see cref="ConfigurationBuilder.StartWaitTime(TimeSpan)"/> /// timeout (the default for this is 5 seconds). This could happen due to a network problem or a /// temporary service outage. In this case, <see cref="Initialized"/> will be false, and the /// <see cref="DataSourceStatusProvider"/> will return a state of <see cref="DataSourceState.Initializing"/>, /// indicating that the SDK will still continue trying to connect in the background. </description></item> /// <item><description> It has encountered an unrecoverable error: for instance, LaunchDarkly has rejected the /// SDK key. Since an invalid key will not become valid, the SDK will not retry in this case. /// <see cref="Initialized"/> will be false, and the <see cref="DataSourceStatusProvider"/> will /// return a state of <see cref="DataSourceState.Off"/>. </description></item> /// </list> /// <para> /// If you have specified <see cref="ConfigurationBuilder.Offline"/> mode or /// <see cref="Components.ExternalUpdatesOnly"/>, the constructor returns immediately without /// trying to connect to LaunchDarkly. /// </para> /// <para> /// Failure to connect to LaunchDarkly will never cause the constructor to throw an exception. /// Under any circumstance where it is not able to get feature flag data from LaunchDarkly (and /// therefore <see cref="Initialized"/> is false), if it does not have any other source of data /// (such as a persistent data store) then feature flag evaluations will behave the same as if /// the flags were not found: that is, they will return whatever default value is specified in /// your code. /// </para> /// </remarks> /// <param name="config">a client configuration object (which includes an SDK key)</param> /// <example> /// <code> /// var config = Configuration.Builder("my-sdk-key") /// .AllAttributesPrivate(true) /// .EventCapacity(1000) /// .Build(); /// var client = new LDClient(config); /// </code> /// </example> /// <seealso cref="LdClient.LdClient(string)"/> public LdClient(Configuration config) { _configuration = config; var logConfig = (config.LoggingConfigurationFactory ?? Components.Logging()) .CreateLoggingConfiguration(); _log = logConfig.LogAdapter.Logger(logConfig.BaseLoggerName ?? LogNames.DefaultBase); _log.Info("Starting LaunchDarkly client {0}", AssemblyVersions.GetAssemblyVersionStringForType(typeof(LdClient))); _evalLog = _log.SubLogger(LogNames.EvaluationSubLog); var basicConfig = new BasicConfiguration(config, _log); var httpConfig = (config.HttpConfigurationFactory ?? Components.HttpConfiguration()) .CreateHttpConfiguration(basicConfig); ServerDiagnosticStore diagnosticStore = _configuration.DiagnosticOptOut ? null : new ServerDiagnosticStore(_configuration, basicConfig, httpConfig); var taskExecutor = new TaskExecutor(this, _log); var clientContext = new LdClientContext(basicConfig, httpConfig, diagnosticStore, taskExecutor); var dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor, _log.SubLogger(LogNames.DataStoreSubLog)); _dataStore = (_configuration.DataStoreFactory ?? Components.InMemoryDataStore) .CreateDataStore(clientContext, dataStoreUpdates); _dataStoreStatusProvider = new DataStoreStatusProviderImpl(_dataStore, dataStoreUpdates); var bigSegmentsConfig = (_configuration.BigSegmentsConfigurationFactory ?? Components.BigSegments(null)) .CreateBigSegmentsConfiguration(clientContext); _bigSegmentStoreWrapper = bigSegmentsConfig.Store is null ? null : new BigSegmentStoreWrapper( bigSegmentsConfig, taskExecutor, _log.SubLogger(LogNames.BigSegmentsSubLog) ); _bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderImpl(_bigSegmentStoreWrapper); _evaluator = new Evaluator( GetFlag, GetSegment, _bigSegmentStoreWrapper == null ? (Func <string, BigSegmentsInternalTypes.BigSegmentsQueryResult>)null : _bigSegmentStoreWrapper.GetUserMembership, _log ); var eventProcessorFactory = config.Offline ? Components.NoEvents : (_configuration.EventProcessorFactory ?? Components.SendEvents()); _eventProcessor = eventProcessorFactory.CreateEventProcessor(clientContext); var dataSourceUpdates = new DataSourceUpdatesImpl(_dataStore, _dataStoreStatusProvider, taskExecutor, _log, logConfig.LogDataSourceOutageAsErrorAfter); IDataSourceFactory dataSourceFactory = config.Offline ? Components.ExternalUpdatesOnly : (_configuration.DataSourceFactory ?? Components.StreamingDataSource()); _dataSource = dataSourceFactory.CreateDataSource(clientContext, dataSourceUpdates); _dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceUpdates); _flagTracker = new FlagTrackerImpl(dataSourceUpdates, (string key, User user) => JsonVariation(key, user, LdValue.Null)); var initTask = _dataSource.Start(); if (!(_dataSource is ComponentsImpl.NullDataSource)) { _log.Info("Waiting up to {0} milliseconds for LaunchDarkly client to start...", _configuration.StartWaitTime.TotalMilliseconds); } try { var success = initTask.Wait(_configuration.StartWaitTime); if (!success) { _log.Warn("Timeout encountered waiting for LaunchDarkly client initialization"); } } catch (AggregateException) { // StreamProcessor may throw an exception if initialization fails, because we want that behavior // in the Xamarin client. However, for backward compatibility we do not want to throw exceptions // from the LdClient constructor in the .NET client, so we'll just swallow this. } }