From 3bd8fc7ac06ec4741dc8e5298e90436cea0a6db7 Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 12 Oct 2022 00:40:54 +0200 Subject: [PATCH 1/7] event system callback configuration on unleash-instance --- src/Unleash/DefaultUnleash.cs | 20 +++++++++++++ src/Unleash/IUnleash.cs | 2 ++ src/Unleash/Internal/EventCallbackConfig.cs | 10 +++++++ tests/Unleash.Tests/DefaultUnleashTests.cs | 31 +++++++++++++++++++++ tests/Unleash.Tests/Unleash.Tests.csproj | 1 + 5 files changed, 64 insertions(+) create mode 100644 src/Unleash/Internal/EventCallbackConfig.cs create mode 100644 tests/Unleash.Tests/DefaultUnleashTests.cs diff --git a/src/Unleash/DefaultUnleash.cs b/src/Unleash/DefaultUnleash.cs index a132be2..4831b18 100644 --- a/src/Unleash/DefaultUnleash.cs +++ b/src/Unleash/DefaultUnleash.cs @@ -3,6 +3,7 @@ namespace Unleash using Internal; using Logging; using Strategies; + using System; using System.Collections.Generic; using System.Linq; using Unleash.Variants; @@ -64,6 +65,8 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies, /// public ICollection FeatureToggles => services.ToggleCollection.Instance.Features; + private EventCallbackConfig EventConfig => new EventCallbackConfig(); + /// public bool IsEnabled(string toggleName) { @@ -221,6 +224,23 @@ private IEnumerable ResolveConstraints(ActivationStrategy activation } } + public void ConfigureEvents(Action callback) + { + if (callback == null) + { + Logger.Error($"UNLEASH: Unleash->ConfigureEvents parameter callback is null"); + } + + try + { + callback(EventConfig); + } + catch (Exception ex) + { + Logger.Error($"UNLEASH: Unleash->ConfigureEvents executing callback threw exception {ex.Message}"); + } + } + public void Dispose() { services?.Dispose(); diff --git a/src/Unleash/IUnleash.cs b/src/Unleash/IUnleash.cs index e9e21da..4d05887 100644 --- a/src/Unleash/IUnleash.cs +++ b/src/Unleash/IUnleash.cs @@ -82,5 +82,7 @@ public interface IUnleash : IDisposable /// /// The Unleash context to evaluate the toggle state against. /// A list of available variants. IEnumerable GetVariants(string toggleName, UnleashContext context); + + void ConfigureEvents(Action config); } } \ No newline at end of file diff --git a/src/Unleash/Internal/EventCallbackConfig.cs b/src/Unleash/Internal/EventCallbackConfig.cs new file mode 100644 index 0000000..f470b27 --- /dev/null +++ b/src/Unleash/Internal/EventCallbackConfig.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Unleash.Internal +{ + public class EventCallbackConfig + { + } +} diff --git a/tests/Unleash.Tests/DefaultUnleashTests.cs b/tests/Unleash.Tests/DefaultUnleashTests.cs new file mode 100644 index 0000000..edcc393 --- /dev/null +++ b/tests/Unleash.Tests/DefaultUnleashTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using NUnit.Framework; +using System; + +namespace Unleash.Tests +{ + public class DefaultUnleashTests + { + [Test] + public void ConfigureEvents_should_invoke_callback() + { + // Arrange + var settings = new UnleashSettings + { + AppName = "testapp", + }; + + var unleash = new DefaultUnleash(settings); + var callbackCalled = false; + + // Act + unleash.ConfigureEvents(cfg => + { + callbackCalled = true; + }); + + // Assert + callbackCalled.Should().BeTrue(); + } + } +} diff --git a/tests/Unleash.Tests/Unleash.Tests.csproj b/tests/Unleash.Tests/Unleash.Tests.csproj index 617a7ad..87d5b7e 100644 --- a/tests/Unleash.Tests/Unleash.Tests.csproj +++ b/tests/Unleash.Tests/Unleash.Tests.csproj @@ -106,6 +106,7 @@ + From 649d95c013ea394877bb1997b0261f4f393a7d08 Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 26 Oct 2022 00:40:10 +0200 Subject: [PATCH 2/7] improve eventconfig callback --- src/Unleash/DefaultUnleash.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Unleash/DefaultUnleash.cs b/src/Unleash/DefaultUnleash.cs index 4831b18..cff82f1 100644 --- a/src/Unleash/DefaultUnleash.cs +++ b/src/Unleash/DefaultUnleash.cs @@ -65,7 +65,7 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies, /// public ICollection FeatureToggles => services.ToggleCollection.Instance.Features; - private EventCallbackConfig EventConfig => new EventCallbackConfig(); + private EventCallbackConfig EventConfig { get; set; } /// public bool IsEnabled(string toggleName) @@ -229,15 +229,18 @@ public void ConfigureEvents(Action callback) if (callback == null) { Logger.Error($"UNLEASH: Unleash->ConfigureEvents parameter callback is null"); + return; } try { - callback(EventConfig); + var evtConfig = new EventCallbackConfig(); + callback(evtConfig); + EventConfig = evtConfig; } catch (Exception ex) { - Logger.Error($"UNLEASH: Unleash->ConfigureEvents executing callback threw exception {ex.Message}"); + Logger.Error($"UNLEASH: Unleash->ConfigureEvents executing callback threw exception: {ex.Message}"); } } From 29bf1f28051647cd1da973145fe34a31e482e121 Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 26 Oct 2022 00:42:20 +0200 Subject: [PATCH 3/7] add impressionData field to featuretoggle --- src/Unleash/Internal/FeatureToggle.cs | 7 +++++-- src/Unleash/Serialization/JsonSerializerTester.cs | 4 ++-- .../Internal/CachedFilesLoader_Bootstrap_Tests.cs | 4 ++-- tests/Unleash.Tests/Mock/MockApiClient.cs | 4 ++-- .../Serialization/DynamicJsonSerializerTests.cs | 4 ++-- tests/Unleash.Tests/Strategy/Segments_Tests.cs | 6 +++--- tests/Unleash.Tests/Variants/VariantUtilsTests.cs | 14 +++++++++++++- 7 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/Unleash/Internal/FeatureToggle.cs b/src/Unleash/Internal/FeatureToggle.cs index 4f3c456..f56c8c5 100644 --- a/src/Unleash/Internal/FeatureToggle.cs +++ b/src/Unleash/Internal/FeatureToggle.cs @@ -5,11 +5,12 @@ namespace Unleash.Internal { public class FeatureToggle { - public FeatureToggle(string name, string type, bool enabled, List strategies, List variants = null) + public FeatureToggle(string name, string type, bool enabled, bool impressionData, List strategies, List variants = null) { Name = name; Type = type; Enabled = enabled; + ImpressionData = impressionData; Strategies = strategies; Variants = variants ?? new List(); } @@ -17,13 +18,15 @@ public FeatureToggle(string name, string type, bool enabled, List Strategies { get; } public List Variants { get; } public override string ToString() { - return $"FeatureToggle{{name=\'{Name}{'\''}, enabled={Enabled}, strategies=\'{Strategies}{'\''}{'}'}"; + return $"FeatureToggle{{name=\'{Name}{'\''}, enabled={Enabled}, impressionData={ImpressionData}, strategies=\'{Strategies}{'\''}{'}'}"; } } } \ No newline at end of file diff --git a/src/Unleash/Serialization/JsonSerializerTester.cs b/src/Unleash/Serialization/JsonSerializerTester.cs index 5ebf706..f21d8ff 100644 --- a/src/Unleash/Serialization/JsonSerializerTester.cs +++ b/src/Unleash/Serialization/JsonSerializerTester.cs @@ -14,14 +14,14 @@ public static class JsonSerializerTester { private static readonly ToggleCollection Toggles = new ToggleCollection(new List { - new FeatureToggle("Feature1", "release", true, new List() + new FeatureToggle("Feature1", "release", true, false, new List() { new ActivationStrategy("remoteAddress", new Dictionary() { {"IPs", "127.0.0.1"} }) }), - new FeatureToggle("feature2", "release", false, new List() + new FeatureToggle("feature2", "release", false, false, new List() { new ActivationStrategy("userWithId", new Dictionary() { diff --git a/tests/Unleash.Tests/Internal/CachedFilesLoader_Bootstrap_Tests.cs b/tests/Unleash.Tests/Internal/CachedFilesLoader_Bootstrap_Tests.cs index 698dbad..2529116 100644 --- a/tests/Unleash.Tests/Internal/CachedFilesLoader_Bootstrap_Tests.cs +++ b/tests/Unleash.Tests/Internal/CachedFilesLoader_Bootstrap_Tests.cs @@ -17,7 +17,7 @@ private static ToggleCollection GetTestToggles() { return new ToggleCollection(new List { - new FeatureToggle("one-enabled", "release", true, new List() + new FeatureToggle("one-enabled", "release", true, false, new List() { new ActivationStrategy("userWithId", new Dictionary(){ {"userIds", "userA" } @@ -29,7 +29,7 @@ private static ToggleCollection GetTestToggles() new VariantDefinition("Ab", 34, null, new List{ new VariantOverride("context", new[] { "a", "b"}) }), } ), - new FeatureToggle("one-disabled", "release", false, new List() + new FeatureToggle("one-disabled", "release", false, false, new List() { new ActivationStrategy("userWithId", new Dictionary() { diff --git a/tests/Unleash.Tests/Mock/MockApiClient.cs b/tests/Unleash.Tests/Mock/MockApiClient.cs index 761bddd..008fe9f 100644 --- a/tests/Unleash.Tests/Mock/MockApiClient.cs +++ b/tests/Unleash.Tests/Mock/MockApiClient.cs @@ -12,7 +12,7 @@ internal class MockApiClient : IUnleashApiClient { private static readonly ToggleCollection Toggles = new ToggleCollection(new List { - new FeatureToggle("one-enabled", "release", true, new List() + new FeatureToggle("one-enabled", "release", true, false, new List() { new ActivationStrategy("userWithId", new Dictionary(){ {"userIds", "userA" } @@ -24,7 +24,7 @@ internal class MockApiClient : IUnleashApiClient new VariantDefinition("Ab", 34, null, new List{ new VariantOverride("context", new[] { "a", "b"}) }), } ), - new FeatureToggle("one-disabled", "release", false, new List() + new FeatureToggle("one-disabled", "release", false, false, new List() { new ActivationStrategy("userWithId", new Dictionary() { diff --git a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs index aa0403d..0fe134d 100644 --- a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs +++ b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs @@ -53,13 +53,13 @@ public void Serialize_SameAsNewtonSoft(Type type) var collection = new ToggleCollection(new List() { - new FeatureToggle("one", "release", true, new List() + new FeatureToggle("one", "release", true, false, new List() { new ActivationStrategy("userByName", new Dictionary(){ {"Demo", "Demo" } }) }), - new FeatureToggle("two", "release", false, new List() + new FeatureToggle("two", "release", false, false, new List() { new ActivationStrategy("userByName2", new Dictionary() { diff --git a/tests/Unleash.Tests/Strategy/Segments_Tests.cs b/tests/Unleash.Tests/Strategy/Segments_Tests.cs index d348bad..3ac5009 100644 --- a/tests/Unleash.Tests/Strategy/Segments_Tests.cs +++ b/tests/Unleash.Tests/Strategy/Segments_Tests.cs @@ -27,7 +27,7 @@ public void Two_Constraints_With_Item_Id_Equals_1_And_Context_Item_Id_Equals_1_S var segmentIds = new List() { "1", "2" }; var toggles = new List() { - new FeatureToggle("item", "release", true, new List() { new ActivationStrategy("default", new Dictionary(), null, segmentIds) }) + new FeatureToggle("item", "release", true, false, new List() { new ActivationStrategy("default", new Dictionary(), null, segmentIds) }) }; var segments = segmentIds.Select(id => new Segment(id, new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") })).ToList(); @@ -52,7 +52,7 @@ public void Two_Constraints_One_Correct_In_Segment_One_Wrong_In_Strategy_Should_ var segmentIds = new List() { "1" }; var toggles = new List() { - new FeatureToggle("item", "release", true, new List() { new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "15") }, segmentIds) }) + new FeatureToggle("item", "release", true, false, new List() { new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "15") }, segmentIds) }) }; var segments = segmentIds.Select(id => new Segment(id, new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") })).ToList(); @@ -77,7 +77,7 @@ public void Two_Constraints_One_In_Segment_One_In_Strategy_Both_Correct_Should_E var segmentIds = new List() { "1" }; var toggles = new List() { - new FeatureToggle("item", "release", true, new List() { new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }, segmentIds) }) + new FeatureToggle("item", "release", true, false, new List() { new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }, segmentIds) }) }; var segments = segmentIds.Select(id => new Segment(id, new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") })).ToList(); diff --git a/tests/Unleash.Tests/Variants/VariantUtilsTests.cs b/tests/Unleash.Tests/Variants/VariantUtilsTests.cs index a86bad7..31d4035 100644 --- a/tests/Unleash.Tests/Variants/VariantUtilsTests.cs +++ b/tests/Unleash.Tests/Variants/VariantUtilsTests.cs @@ -16,7 +16,7 @@ public class VariantUtilsTests public void ShouldReturnDefaultVariantWhenToggleHasNoVariants() { // Arrange - var toggle = new FeatureToggle("test.variants", "release", true, new List { defaultStrategy }); + var toggle = new FeatureToggle("test.variants", "release", true, false, new List { defaultStrategy }); var context = new UnleashContext { UserId = "userA", @@ -44,6 +44,7 @@ public void ShouldReturnVariant1() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -76,6 +77,7 @@ public void ShouldReturnVariant2() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -106,6 +108,7 @@ public void ShouldReturnVariant3() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -137,6 +140,7 @@ public void ShouldReturnVariantOverride() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -168,6 +172,7 @@ public void ShouldReturnVariantOverrideOnRemoteAdress() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -201,6 +206,7 @@ public void ShouldReturnVariantOverrideOnCustomProperty() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -235,6 +241,7 @@ public void ShouldReturnVariantOverrideOnSessionId() "test.variants", "release", true, + false, new List { defaultStrategy }, new List { v1, v2, v3 }); @@ -268,6 +275,7 @@ public void Custom_Stickiness_CustomField_528_Yields_Blue() "Feature.flexible.rollout.custom.stickiness_100", "release", true, + false, new List { defaultStrategy }, new List { blue, red, green, yellow }); @@ -301,6 +309,7 @@ public void Custom_Stickiness_CustomField_16_Yields_Blue() "Feature.flexible.rollout.custom.stickiness_100", "release", true, + false, new List { defaultStrategy }, new List { blue, red, green, yellow }); @@ -334,6 +343,7 @@ public void Custom_Stickiness_CustomField_198_Yields_Red() "Feature.flexible.rollout.custom.stickiness_100", "release", true, + false, new List { defaultStrategy }, new List { blue, red, green, yellow }); @@ -367,6 +377,7 @@ public void Custom_Stickiness_CustomField_43_Yields_Green() "Feature.flexible.rollout.custom.stickiness_100", "release", true, + false, new List { defaultStrategy }, new List { blue, red, green, yellow }); @@ -400,6 +411,7 @@ public void Custom_Stickiness_CustomField_112_Yields_Yellow() "Feature.flexible.rollout.custom.stickiness_100", "release", true, + false, new List { defaultStrategy }, new List { blue, red, green, yellow }); From 30fa6fcda6bc32fc03ee4ea25c7a2fa5af57595d Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 26 Oct 2022 00:42:54 +0200 Subject: [PATCH 4/7] add impressionevent logic and tests --- src/Unleash/DefaultUnleash.cs | 32 +++++ src/Unleash/Internal/EventCallbackConfig.cs | 1 + src/Unleash/Internal/ImpressionEvent.cs | 16 +++ .../Internal/ImpressionData_Tests.cs | 132 ++++++++++++++++++ tests/Unleash.Tests/Unleash.Tests.csproj | 1 + 5 files changed, 182 insertions(+) create mode 100644 src/Unleash/Internal/ImpressionEvent.cs create mode 100644 tests/Unleash.Tests/Internal/ImpressionData_Tests.cs diff --git a/src/Unleash/DefaultUnleash.cs b/src/Unleash/DefaultUnleash.cs index cff82f1..f9e028c 100644 --- a/src/Unleash/DefaultUnleash.cs +++ b/src/Unleash/DefaultUnleash.cs @@ -114,6 +114,9 @@ private bool CheckIsEnabled(string toggleName, UnleashContext context, bool defa } RegisterCount(toggleName, enabled); + + if (featureToggle?.ImpressionData ?? false) EmitImpressionEvent("isEnabled", context, enabled, featureToggle.Name); + return enabled; } @@ -135,6 +138,9 @@ public Variant GetVariant(string toggleName, UnleashContext context, Variant def var variant = enabled ? VariantUtils.SelectVariant(toggle, context, defaultValue) : defaultValue; RegisterVariant(toggleName, variant); + + if (toggle?.ImpressionData ?? false) EmitImpressionEvent("getVariant", context, enabled, toggle.Name, variant.Name); + return variant; } @@ -244,6 +250,32 @@ public void ConfigureEvents(Action callback) } } + private void EmitImpressionEvent(string type, UnleashContext context, bool enabled, string name, string variant = null) + { + if (EventConfig.ImpressionEvent == null) + { + Logger.Error($"UNLEASH: Unleash->ImpressionData callback is null, unable to emit event"); + return; + } + + try + { + EventConfig.ImpressionEvent(new ImpressionEvent + { + Type = type, + Context = context, + EventId = Guid.NewGuid().ToString(), + Enabled = enabled, + FeatureName = name, + Variant = variant + }); + } + catch (Exception ex) + { + Logger.Error($"UNLEASH: Emitting impression event callback threw exception: {ex.Message}"); + } + } + public void Dispose() { services?.Dispose(); diff --git a/src/Unleash/Internal/EventCallbackConfig.cs b/src/Unleash/Internal/EventCallbackConfig.cs index f470b27..bf8053a 100644 --- a/src/Unleash/Internal/EventCallbackConfig.cs +++ b/src/Unleash/Internal/EventCallbackConfig.cs @@ -6,5 +6,6 @@ namespace Unleash.Internal { public class EventCallbackConfig { + public Action ImpressionEvent { get; set; } } } diff --git a/src/Unleash/Internal/ImpressionEvent.cs b/src/Unleash/Internal/ImpressionEvent.cs new file mode 100644 index 0000000..52ccf7f --- /dev/null +++ b/src/Unleash/Internal/ImpressionEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Unleash.Internal +{ + public class ImpressionEvent + { + public string Type { get; set; } + public string EventId { get; set; } + public UnleashContext Context { get; set; } + public bool Enabled { get; set; } + public string FeatureName { get; set; } + public string Variant { get; set; } + } +} diff --git a/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs b/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs new file mode 100644 index 0000000..4442f2d --- /dev/null +++ b/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs @@ -0,0 +1,132 @@ +using FakeItEasy; +using FluentAssertions; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using static Unleash.Tests.Specifications.TestFactory; +using Unleash.Tests.Mock; +using Unleash.Internal; +using Unleash.Scheduling; +using System.Threading; +using Unleash.Variants; + +namespace Unleash.Tests.Internal +{ + public class ImpressionData_Tests + { + [Test] + public void Impression_Event_Gets_Called_For_IsEnabled() + { + // Arrange + ImpressionEvent callbackEvent = null; + var appname = "testapp"; + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var toggles = new List() + { + new FeatureToggle("item", "release", true, true, new List() { strategy }) + }; + + + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = evt => { callbackEvent = evt; }; + }); + + // Act + var result = unleash.IsEnabled("item"); + unleash.Dispose(); + + // Assert + result.Should().BeTrue(); + callbackEvent.Should().NotBeNull(); + callbackEvent.Enabled.Should().BeTrue(); + callbackEvent.Variant.Should().BeNull(); + } + + [Test] + public void Impression_Event_Gets_Called_For_Variants() + { + // Arrange + ImpressionEvent callbackEvent = null; + var appname = "testapp"; + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var payload = new Payload("string", "val1"); + var variant = new VariantDefinition("blue", 100, payload); + var toggles = new List() + { + new FeatureToggle("item", "release", true, true, new List() { strategy }, new List() { variant }) + }; + + + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = evt => { callbackEvent = evt; }; + }); + + // Act + var result = unleash.GetVariant("item"); + unleash.Dispose(); + + // Assert + result.Name.Should().Be("blue"); + callbackEvent.Should().NotBeNull(); + callbackEvent.Enabled.Should().BeTrue(); + callbackEvent.Variant.Should().Be("blue"); + } + + public static IUnleash CreateUnleash(string name, ToggleCollection state) + { + var fakeHttpClientFactory = A.Fake(); + var fakeHttpMessageHandler = new TestHttpMessageHandler(); + var httpClient = new HttpClient(fakeHttpMessageHandler) { BaseAddress = new Uri("http://localhost") }; + var fakeScheduler = A.Fake(); + var fakeFileSystem = new MockFileSystem(); + var toggleState = Newtonsoft.Json.JsonConvert.SerializeObject(state); + + A.CallTo(() => fakeHttpClientFactory.Create(A._)).Returns(httpClient); + A.CallTo(() => fakeScheduler.Configure(A>._, A._)).Invokes(action => + { + var task = ((IEnumerable)action.Arguments[0]).First(); + task.ExecuteAsync((CancellationToken)action.Arguments[1]).Wait(); + }); + + fakeHttpMessageHandler.Response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(toggleState, Encoding.UTF8, "application/json"), + Headers = + { + ETag = new EntityTagHeaderValue("\"123\"") + } + }; + + var contextBuilder = new UnleashContext.Builder(); + contextBuilder.AddProperty("item-id", "1"); + + var settings = new UnleashSettings + { + AppName = name, + UnleashContextProvider = new DefaultUnleashContextProvider(contextBuilder.Build()), + HttpClientFactory = fakeHttpClientFactory, + ScheduledTaskManager = fakeScheduler, + FileSystem = fakeFileSystem + }; + + var unleash = new DefaultUnleash(settings); + + return unleash; + } + } +} diff --git a/tests/Unleash.Tests/Unleash.Tests.csproj b/tests/Unleash.Tests/Unleash.Tests.csproj index 87d5b7e..8f5e819 100644 --- a/tests/Unleash.Tests/Unleash.Tests.csproj +++ b/tests/Unleash.Tests/Unleash.Tests.csproj @@ -84,6 +84,7 @@ + From 9992f0437a9ca7947d44330ca4f465c37ad5c8f0 Mon Sep 17 00:00:00 2001 From: David Leek Date: Thu, 3 Nov 2022 00:21:20 +0100 Subject: [PATCH 5/7] improvement: document new events feature --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index c8a7449..83e8507 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,27 @@ When your application shuts down, remember to dispose the unleash instance. unleash?.Dispose() ``` +### Handling events +Currently supported events: +- [Impression data events](https://docs.getunleash.io/advanced/impression-data#impression-event-data) + +```csharp + +var settings = new UnleashSettings() +{ + // ... +}; + +var unleash = new DefaultUnleash(settings); + +// Set up handling of impression events +unleash.ConfigureEvents(cfg => +{ + cfg.ImpressionEvent = evt => { Console.WriteLine($"{evt.FeatureName}: {evt.Enabled}"); }; +}); + +``` + ### Configuring projects in unleash client If you're organizing your feature toggles in `Projects` in Unleash Enterprise, you can specify the `ProjectId` on the `UnleashSettings` to select which project to fetch feature toggles for. From 4154a0176a1f66f13a367091536bf459dff65c5e Mon Sep 17 00:00:00 2001 From: David Leek Date: Thu, 3 Nov 2022 00:32:20 +0100 Subject: [PATCH 6/7] add more tests --- .../Internal/ImpressionData_Tests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs b/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs index 4442f2d..d5d3a6a 100644 --- a/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs +++ b/tests/Unleash.Tests/Internal/ImpressionData_Tests.cs @@ -52,6 +52,86 @@ public void Impression_Event_Gets_Called_For_IsEnabled() callbackEvent.Variant.Should().BeNull(); } + [Test] + public void Impression_Event_Does_Not_Get_Called_When_Not_Opted_In() + { + // Arrange + ImpressionEvent callbackEvent = null; + var appname = "testapp"; + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var toggles = new List() + { + new FeatureToggle("item", "release", true, false, new List() { strategy }) + }; + + + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = evt => { callbackEvent = evt; }; + }); + + // Act + var result = unleash.IsEnabled("item"); + unleash.Dispose(); + + // Assert + result.Should().BeTrue(); + callbackEvent.Should().BeNull(); + } + + [Test] + public void Impression_Event_Callback_Invoker_Catches_Exception() + { + // Arrange + var appname = "testapp"; + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var toggles = new List() + { + new FeatureToggle("item", "release", true, true, new List() { strategy }) + }; + + + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = evt => { throw new Exception("Something bad just happened!"); }; + }); + + // Act, Assert + Assert.DoesNotThrow(() => { unleash.IsEnabled("item"); }); + unleash.Dispose(); + } + + [Test] + public void Impression_Event_Callback_Null_Does_Not_Throw() + { + // Arrange + var appname = "testapp"; + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var toggles = new List() + { + new FeatureToggle("item", "release", true, true, new List() { strategy }) + }; + + + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = null; + }); + + // Act, Assert + Assert.DoesNotThrow(() => { unleash.IsEnabled("item"); }); + unleash.Dispose(); + } + [Test] public void Impression_Event_Gets_Called_For_Variants() { From 4addefdba3c3a32b8a4614f46ea1494bdb8a46ef Mon Sep 17 00:00:00 2001 From: David Leek Date: Mon, 7 Nov 2022 23:26:21 +0100 Subject: [PATCH 7/7] add some serialization testing for impressiondata --- .../App_Data/impressiondata-v2.json | 30 +++++++++++++++ .../DynamicJsonSerializerTests.cs | 37 +++++++++++++++++++ tests/Unleash.Tests/Unleash.Tests.csproj | 3 ++ 3 files changed, 70 insertions(+) create mode 100644 tests/Unleash.Tests/App_Data/impressiondata-v2.json diff --git a/tests/Unleash.Tests/App_Data/impressiondata-v2.json b/tests/Unleash.Tests/App_Data/impressiondata-v2.json new file mode 100644 index 0000000..f9cd99d --- /dev/null +++ b/tests/Unleash.Tests/App_Data/impressiondata-v2.json @@ -0,0 +1,30 @@ +{ + "version": 2, + "features": [ + { + "name": "Tests only", + "description": "Where name has test in it", + "enabled": true, + "impressionData": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "segments": [ 1 ] + } + ] + } + ], + "segments": [ + { + "id": 1, + "constraints": [ + { + "contextName": "name", + "operator": "STR_CONTAINS", + "value": "test" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs index 0fe134d..4490b0b 100644 --- a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs +++ b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs @@ -7,6 +7,8 @@ using NUnit.Framework; using Unleash.Serialization; using Unleash.Internal; +using System.Linq; +using System.Collections.ObjectModel; namespace Unleash.Tests.Serialization { @@ -99,5 +101,40 @@ public void Serialize_SameAsNewtonSoft(Type type) resultingJson.Should().Be(expected); } } + + [Test] + public void Deserializes_ImpressionData_Property() + { + // Arrange + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "App_Data", "impressiondata-v2.json"); + var originalJson = File.ReadAllText(path); + + // Act + var deserialized = JsonConvert.DeserializeObject(originalJson); + var toggle = deserialized.Features.First(); + toggle.Should().NotBeNull(); + toggle.ImpressionData.Should().BeTrue(); + } + + [Test] + public void Serializes_ImpressionData_Property() + { + // Arrange + var strategy = new ActivationStrategy("default", new Dictionary(), new List() { new Constraint("item-id", Operator.NUM_EQ, false, false, "1") }); + var toggles = new List() + { + new FeatureToggle("item", "release", true, true, new List() { strategy }) + }; + + var state = new ToggleCollection(toggles); + state.Version = 2; + + // Act + var serialized = JsonConvert.SerializeObject(toggles, new JsonSerializerSettings()); + + // Assert + var contains = serialized.IndexOf("\"ImpressionData\":true") >= 0; + contains.Should().BeTrue(); + } } } \ No newline at end of file diff --git a/tests/Unleash.Tests/Unleash.Tests.csproj b/tests/Unleash.Tests/Unleash.Tests.csproj index c96d351..c426485 100644 --- a/tests/Unleash.Tests/Unleash.Tests.csproj +++ b/tests/Unleash.Tests/Unleash.Tests.csproj @@ -118,6 +118,9 @@ + + PreserveNewest + PreserveNewest