From 4cf5f436a0ccda741f044a57ee47734a4300d492 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Thu, 24 Sep 2020 19:19:17 +0500 Subject: [PATCH 01/15] Implemented flag decision logic --- .../OptimizelySDK.NetStandard20.csproj | 3 +++ OptimizelySDK.Tests/TestData.json | 3 ++- OptimizelySDK/Config/DatafileProjectConfig.cs | 5 +++++ OptimizelySDK/Event/Builder/EventBuilder.cs | 16 +++++++++++---- OptimizelySDK/Event/Builder/Params.cs | 2 ++ OptimizelySDK/Event/Entity/Decision.cs | 6 ++++-- .../Event/Entity/DecisionMetadata.cs | 20 +++++++++++++++++++ OptimizelySDK/Event/Entity/ImpressionEvent.cs | 10 ++++++++++ OptimizelySDK/Event/EventFactory.cs | 3 ++- OptimizelySDK/Event/UserEventFactory.cs | 16 ++++++++++++--- OptimizelySDK/Optimizely.cs | 14 +++++++------ OptimizelySDK/OptimizelySDK.csproj | 1 + OptimizelySDK/ProjectConfig.cs | 6 ++++++ 13 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 OptimizelySDK/Event/Entity/DecisionMetadata.cs diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 82cf73c9..1a126f2c 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -174,6 +174,9 @@ </Compile> <Compile Include="..\OptimizelySDK\Event\Entity\Decision.cs"> <Link>Event\Entity\Decision.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\Event\Entity\DecisionMetadata.cs"> + <Link>Event\Entity\DecisionMetadata.cs</Link> </Compile> <Compile Include="..\OptimizelySDK\Event\Entity\EventBatch.cs"> <Link>Event\Entity\EventBatch.cs</Link> diff --git a/OptimizelySDK.Tests/TestData.json b/OptimizelySDK.Tests/TestData.json index 11f3decb..b8b72fbc 100644 --- a/OptimizelySDK.Tests/TestData.json +++ b/OptimizelySDK.Tests/TestData.json @@ -565,7 +565,7 @@ "key": "integer_variable", "type": "integer", "defaultValue": "7" - } + } ] }, { @@ -940,5 +940,6 @@ ], "revision": "15", "anonymizeIP": false, + "sendFlagDecisions": true, "botFiltering": true } diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index a90df7fa..fc479b0c 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -69,6 +69,11 @@ public enum OPTLYSDKVersion /// </summary> public string Revision { get; set; } + /// <summary> + /// SendFlagDecisions determines whether impressions events are sent for ALL decision types. + /// </summary> + public bool SendFlagDecisions { get; set; } + /// <summary> /// Allow Anonymize IP by truncating the last block of visitors' IP address. /// </summary> diff --git a/OptimizelySDK/Event/Builder/EventBuilder.cs b/OptimizelySDK/Event/Builder/EventBuilder.cs index 0f0b41b8..8f24390b 100644 --- a/OptimizelySDK/Event/Builder/EventBuilder.cs +++ b/OptimizelySDK/Event/Builder/EventBuilder.cs @@ -15,6 +15,7 @@ */ using OptimizelySDK.Bucketing; using OptimizelySDK.Entity; +using OptimizelySDK.Event.Entity; using OptimizelySDK.Logger; using OptimizelySDK.Utils; using System; @@ -121,10 +122,16 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string return comonParams; } - private Dictionary<string, object> GetImpressionParams(Experiment experiment, string variationId) + private Dictionary<string, object> GetImpressionParams(Experiment experiment, + string variationId, + string flagKey, + string flagType) { var impressionEvent = new Dictionary<string, object>(); + + string variationKey = variationId != null ? experiment.VariationIdToVariationMap[variationId].Key : null; + var metadata = new DecisionMetadata(flagKey, flagType, variationKey); var decisions = new object[] { @@ -132,7 +139,8 @@ private Dictionary<string, object> GetImpressionParams(Experiment experiment, st { { Params.CAMPAIGN_ID, experiment.LayerId }, { Params.EXPERIMENT_ID, experiment.Id }, - { Params.VARIATION_ID, variationId } + { Params.VARIATION_ID, variationId }, + { Params.METADATA, metadata } } }; @@ -216,11 +224,11 @@ private Dictionary<string, object> GetImpressionOrConversionParamsWithCommonPara /// <param name="userAttributes">associative array of attributes for the user</param> /// <returns>LogEvent object to be sent to dispatcher</returns> public virtual LogEvent CreateImpressionEvent(ProjectConfig config, Experiment experiment, string variationId, - string userId, UserAttributes userAttributes) + string userId, UserAttributes userAttributes, string flagKey, string flagType) { var commonParams = GetCommonParams(config, userId, userAttributes ?? new UserAttributes()); - var impressionOnlyParams = GetImpressionParams(experiment, variationId); + var impressionOnlyParams = GetImpressionParams(experiment, variationId, flagKey, flagType); var impressionParams = GetImpressionOrConversionParamsWithCommonParams(commonParams, new object[] { impressionOnlyParams }); diff --git a/OptimizelySDK/Event/Builder/Params.cs b/OptimizelySDK/Event/Builder/Params.cs index 8b12f96a..14d0a994 100644 --- a/OptimizelySDK/Event/Builder/Params.cs +++ b/OptimizelySDK/Event/Builder/Params.cs @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +using OptimizelySDK.Event.Entity; using System; namespace OptimizelySDK.Event.Builder @@ -30,6 +31,7 @@ public static class Params public const string ENTITY_ID = "entity_id"; public const string EVENTS = "events"; public const string EXPERIMENT_ID = "experiment_id"; + public const string METADATA = "metadata"; public const string PROJECT_ID = "project_id"; public const string REVISION = "revision"; public const string TIME = "timestamp"; diff --git a/OptimizelySDK/Event/Entity/Decision.cs b/OptimizelySDK/Event/Entity/Decision.cs index c25f280f..dc2d5fa8 100644 --- a/OptimizelySDK/Event/Entity/Decision.cs +++ b/OptimizelySDK/Event/Entity/Decision.cs @@ -23,15 +23,17 @@ public class Decision public string CampaignId { get; private set; } [JsonProperty("experiment_id")] public string ExperimentId { get; private set; } + [JsonProperty("metadata")] + public DecisionMetadata Metadata { get; private set; } [JsonProperty("variation_id")] public string VariationId { get; private set; } - public Decision() {} - public Decision(string campaignId, string experimentId, string variationId) + public Decision(string campaignId, string experimentId, string variationId, DecisionMetadata metadata) { CampaignId = campaignId; ExperimentId = experimentId; + Metadata = metadata; VariationId = variationId; } } diff --git a/OptimizelySDK/Event/Entity/DecisionMetadata.cs b/OptimizelySDK/Event/Entity/DecisionMetadata.cs new file mode 100644 index 00000000..8bc6318b --- /dev/null +++ b/OptimizelySDK/Event/Entity/DecisionMetadata.cs @@ -0,0 +1,20 @@ + +namespace OptimizelySDK.Event.Entity +{ + /// <summary> + /// DecisionMetadata captures additional information regarding the decision + /// </summary> + public class DecisionMetadata + { + public string FlagType { get; private set; } + public string FlagKey { get; private set; } + public string VariationKey { get; private set; } + + public DecisionMetadata(string flagKey, string flagType, string variationKey = null) + { + FlagType = flagType; + FlagKey = flagKey; + VariationKey = variationKey; + } + } +} diff --git a/OptimizelySDK/Event/Entity/ImpressionEvent.cs b/OptimizelySDK/Event/Entity/ImpressionEvent.cs index 7b3777a1..d8226f97 100644 --- a/OptimizelySDK/Event/Entity/ImpressionEvent.cs +++ b/OptimizelySDK/Event/Entity/ImpressionEvent.cs @@ -28,6 +28,7 @@ public class ImpressionEvent : UserEvent public VisitorAttribute[] VisitorAttributes { get; private set; } public Experiment Experiment { get; set; } + public DecisionMetadata Metadata { get; set; } public Variation Variation { get; set; } public bool? BotFiltering { get; set; } @@ -42,6 +43,7 @@ public class Builder public VisitorAttribute[] VisitorAttributes; private Experiment Experiment; private Variation Variation; + private DecisionMetadata Metadata; private bool? BotFiltering; public Builder WithUserId(string userId) @@ -65,6 +67,13 @@ public Builder WithExperiment(Experiment experiment) return this; } + public Builder WithMetadata(DecisionMetadata metadata) + { + Metadata = metadata; + + return this; + } + public Builder WithVisitorAttributes(VisitorAttribute[] visitorAttributes) { VisitorAttributes = visitorAttributes; @@ -102,6 +111,7 @@ public ImpressionEvent Build() impressionEvent.VisitorAttributes = VisitorAttributes; impressionEvent.UserId = UserId; impressionEvent.Variation = Variation; + impressionEvent.Metadata = Metadata; impressionEvent.BotFiltering = BotFiltering; return impressionEvent; diff --git a/OptimizelySDK/Event/EventFactory.cs b/OptimizelySDK/Event/EventFactory.cs index 7097ea03..f91e2fcf 100644 --- a/OptimizelySDK/Event/EventFactory.cs +++ b/OptimizelySDK/Event/EventFactory.cs @@ -115,7 +115,8 @@ private static Visitor CreateVisitor(ImpressionEvent impressionEvent) { Decision decision = new Decision(impressionEvent.Experiment?.LayerId, impressionEvent.Experiment?.Id, - impressionEvent.Variation?.Id); + impressionEvent.Variation?.Id, + impressionEvent.Metadata); SnapshotEvent snapshotEvent = new SnapshotEvent.Builder() .WithUUID(impressionEvent.UUID) diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs index 2733c673..f8bc243b 100644 --- a/OptimizelySDK/Event/UserEventFactory.cs +++ b/OptimizelySDK/Event/UserEventFactory.cs @@ -21,10 +21,12 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, Experiment activatedExperiment, string variationId, string userId, - UserAttributes userAttributes) + UserAttributes userAttributes, + string flagKey, + string flagType) { Variation variation = projectConfig.GetVariationFromId(activatedExperiment?.Key, variationId); - return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes); + return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes, flagKey, flagType); } /// <summary> @@ -35,12 +37,16 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, /// <param name="variation">The variation entity</param> /// <param name="userId">The user Id</param> /// <param name="userAttributes">The user's attributes</param> + /// <param name="flagKey">experiment key or feature key</param> + /// <param name="flagType">experiment or featureDecision source </param> /// <returns>ImpressionEvent instance</returns> public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, Experiment activatedExperiment, Variation variation, string userId, - UserAttributes userAttributes) + UserAttributes userAttributes, + string flagKey, + string flagType) { var eventContext = new EventContext.Builder() @@ -50,10 +56,14 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, .WithRevision(projectConfig.Revision) .Build(); + var variationKey = variation?.Key; + var metadata = new DecisionMetadata(flagKey, flagType, variationKey); + return new ImpressionEvent.Builder() .WithEventContext(eventContext) .WithBotFilteringEnabled(projectConfig.BotFiltering) .WithExperiment(activatedExperiment) + .WithMetadata(metadata) .WithUserId(userId) .WithVariation(variation) .WithVisitorAttributes(EventFactory.BuildAttributeList(userAttributes, projectConfig)) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index e9353384..0bef0b8a 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -252,7 +252,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes, config); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, experimentKey, "experiment"); return variation; } @@ -470,17 +470,18 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri bool featureEnabled = false; var sourceInfo = new Dictionary<string, string>(); var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); + var variation = decision.Variation; - if (decision.Variation != null) + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decision.Source); + + if (variation != null) { - var variation = decision.Variation; featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) { sourceInfo["experimentKey"] = decision.Experiment.Key; sourceInfo["variationKey"] = variation.Key; - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config); } else { @@ -684,11 +685,12 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// <param name="userId">The user ID</param> /// <param name="userAttributes">The user's attributes</param> private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes, ProjectConfig config) + UserAttributes userAttributes, ProjectConfig config, + string flagKey, string flagType) { if (experiment.IsExperimentRunning) { - var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes); + var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes, flagKey, flagType); EventProcessor.Process(userEvent); Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 6b25e27e..9964d942 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -86,6 +86,7 @@ <Compile Include="Entity\ForcedVariation.cs" /> <Compile Include="Entity\Group.cs" /> <Compile Include="Entity\IdKeyEntity.cs" /> + <Compile Include="Event\Entity\DecisionMetadata.cs" /> <Compile Include="OptimizelyJSON.cs" /> <Compile Include="Entity\Rollout.cs" /> <Compile Include="Entity\TrafficAllocation.cs" /> diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index f51edb66..0b56c2e2 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -44,6 +44,12 @@ public interface ProjectConfig /// </summary> string Revision { get; set; } + + /// <summary> + /// SendFlagDecisions determines whether impressions events are sent for ALL decision types. + /// </summary> + bool SendFlagDecisions { get; set; } + /// <summary> /// Allow Anonymize IP by truncating the last block of visitors' IP address. /// </summary> From 314f13bd14717361806ff8087fb0e4a63398340c Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 25 Sep 2020 17:39:33 +0500 Subject: [PATCH 02/15] added check that if SendFlagDecision is true then send the event --- OptimizelySDK/Event/UserEventFactory.cs | 19 ++++++++++++------- OptimizelySDK/Optimizely.cs | 6 ++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs index f8bc243b..22296c1c 100644 --- a/OptimizelySDK/Event/UserEventFactory.cs +++ b/OptimizelySDK/Event/UserEventFactory.cs @@ -1,4 +1,5 @@ -using OptimizelySDK.Entity; +using OptimizelySDK.Bucketing; +using OptimizelySDK.Entity; using OptimizelySDK.Event.Entity; namespace OptimizelySDK.Event @@ -48,13 +49,17 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, string flagKey, string flagType) { - + if ((flagType == FeatureDecision.DECISION_SOURCE_ROLLOUT || variation == null) && !projectConfig.SendFlagDecisions) + { + return null; + } + var eventContext = new EventContext.Builder() - .WithProjectId(projectConfig.ProjectId) - .WithAccountId(projectConfig.AccountId) - .WithAnonymizeIP(projectConfig.AnonymizeIP) - .WithRevision(projectConfig.Revision) - .Build(); + .WithProjectId(projectConfig.ProjectId) + .WithAccountId(projectConfig.AccountId) + .WithAnonymizeIP(projectConfig.AnonymizeIP) + .WithRevision(projectConfig.Revision) + .Build(); var variationKey = variation?.Key; var metadata = new DecisionMetadata(flagKey, flagType, variationKey); diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 0bef0b8a..5380198a 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -478,6 +478,8 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri { featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + // This information is only necessary for feature tests. + // For rollouts experiments and variations are an implementation detail only. if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) { sourceInfo["experimentKey"] = decision.Experiment.Key; @@ -691,6 +693,10 @@ private void SendImpressionEvent(Experiment experiment, Variation variation, str if (experiment.IsExperimentRunning) { var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes, flagKey, flagType); + if (userEvent == null) + { + return; + } EventProcessor.Process(userEvent); Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); From 1bf73ea031df3af23b257ee7fc44d4fd9aabe28d Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 25 Sep 2020 21:35:39 +0500 Subject: [PATCH 03/15] - Added unit tests of featureDecision, - Removed extra log, - Added metadata --- .../OptimizelySDK.Net35.csproj | 3 + .../OptimizelySDK.Net40.csproj | 3 + .../OptimizelySDK.NetStandard16.csproj | 4 + .../EventTests/EventEntitiesTest.cs | 2 +- .../EventTests/EventFactoryTest.cs | 208 ++++++++++++++++-- .../EventTests/UserEventFactoryTest.cs | 4 +- OptimizelySDK.Tests/OptimizelyTest.cs | 6 - OptimizelySDK.Tests/ProjectConfigTest.cs | 2 + OptimizelySDK/Event/Builder/EventBuilder.cs | 12 +- OptimizelySDK/Event/Entity/Decision.cs | 2 +- .../Event/Entity/DecisionMetadata.cs | 23 +- OptimizelySDK/Event/EventFactory.cs | 2 +- OptimizelySDK/Optimizely.cs | 38 ++-- 13 files changed, 254 insertions(+), 55 deletions(-) diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 6d97bc2d..e5730209 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -266,6 +266,9 @@ <Compile Include="..\OptimizelySDK\Event\Entity\Visitor.cs"> <Link>Event\Entity\Visitor.cs</Link> </Compile> + <Compile Include="..\OptimizelySDK\Event\Entity\DecisionMetadata.cs"> + <Link>Event\Entity\DecisionMetadata.cs</Link> + </Compile> <Compile Include="..\OptimizelySDK\Event\Entity\VisitorAttribute.cs"> <Link>Event\Entity\VisitorAttribute.cs</Link> </Compile> diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 83639901..5ce0521a 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -280,6 +280,9 @@ <Compile Include="..\OptimizelySDK\Event\Entity\VisitorAttribute.cs"> <Link>Event\Entity\VisitorAttribute.cs</Link> </Compile> + <Compile Include="..\OptimizelySDK\Event\Entity\DecisionMetadata.cs"> + <Link>Event\Entity\DecisionMetadata.cs</Link> + </Compile> <Compile Include="..\OptimizelySDK\Event\EventFactory.cs"> <Link>Event\EventFactory.cs</Link> </Compile> diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index a38425a0..7d8e0fde 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -24,6 +24,7 @@ <Compile Include="..\OptimizelySDK\AudienceConditions\OrCondition.cs" /> <Compile Include="..\OptimizelySDK\Entity\Attribute.cs" /> <Compile Include="..\OptimizelySDK\Entity\Audience.cs" /> + <Compile Include="..\OptimizelySDK\Entity\DecisionMetadata.cs" /> <Compile Include="..\OptimizelySDK\Entity\Entity.cs" /> <Compile Include="..\OptimizelySDK\Entity\Event.cs" /> <Compile Include="..\OptimizelySDK\Entity\EventTags.cs" /> @@ -121,6 +122,9 @@ <Compile Include="..\OptimizelySDK\Event\Entity\VisitorAttribute.cs"> <Link>VisitorAttribute.cs</Link> </Compile> + <Compile Include="..\OptimizelySDK\Event\Entity\DecisionMetadata.cs"> + <Link>DecisionMetadata.cs</Link> + </Compile> <Compile Include="..\OptimizelySDK\Event\Entity\Decision.cs"> <Link>DecisionEvent.cs</Link> </Compile> diff --git a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs index 15c4e28d..6463974c 100644 --- a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs @@ -104,7 +104,7 @@ public void TestImpressionEventEqualsSerializedPayload() .WithTimeStamp(timeStamp) .WithEventTags(null) .Build(); - + var metadata = new DecisionMetadata("experiment", "7716830082"); var decision = new Decision("7719770039", "7716830082", "77210100090"); var snapshot = new Snapshot(events: new SnapshotEvent[] { snapshotEvent }, decisions: new Decision[] { decision }); diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index 338d36c1..5534a451 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -27,6 +27,24 @@ public void Setup() Config = DatafileProjectConfig.Create(TestData.Datafile, logger, new ErrorHandler.NoOpErrorHandler()); } + [Test] + public void TestCreateImpressionEventReturnsNullWhenSendFlagDecisionsIsFalseAndIsRollout() + { + Config.SendFlagDecisions = false; + var impressionEvent = UserEventFactory.CreateImpressionEvent( + Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, null, "test_feature", "rollout"); + Assert.IsNull(impressionEvent); + } + + [Test] + public void TestCreateImpressionEventReturnsNullWhenSendFlagDecisionsIsFalseAndVariationIsNull() + { + Config.SendFlagDecisions = false; + var impressionEvent = UserEventFactory.CreateImpressionEvent( + Config, Config.GetExperimentFromKey("test_experiment"), variation: null, TestUserId, null, "test_experiment", "experiment"); + Assert.IsNull(impressionEvent); + } + [Test] public void TestCreateImpressionEventNoAttributes() { @@ -45,7 +63,12 @@ public void TestCreateImpressionEventNoAttributes() new Dictionary<string, object> { { "campaign_id", "7719770039" }, { "experiment_id", "7716830082" }, - { "variation_id", "7722370027" } + { "variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } } } } }, @@ -93,7 +116,7 @@ public void TestCreateImpressionEventNoAttributes() { "Content-Type", "application/json" } }); var impressionEvent = UserEventFactory.CreateImpressionEvent( - Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, null); + Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, null, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); @@ -120,7 +143,13 @@ public void TestCreateImpressionEventWithAttributes() new Dictionary<string, object> { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -183,7 +212,7 @@ public void TestCreateImpressionEventWithAttributes() { "company", "Optimizely" } }; // - var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), variationId, TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), variationId, TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); @@ -213,7 +242,13 @@ public void TestCreateImpressionEventWithTypedAttributes() { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -298,7 +333,7 @@ public void TestCreateImpressionEventWithTypedAttributes() {"integer_key", 15 }, {"double_key", 3.14 } }; - var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); @@ -328,7 +363,13 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload() { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -413,7 +454,128 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload() { "nan", double.NaN }, { "invalid_num_value", Math.Pow(2, 53) + 2 }, }; - var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); + var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); + + TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); + + Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent)); + } + + [Test] + public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout() + { + var guid = Guid.NewGuid(); + var timeStamp = TestData.SecondsSince1970(); + + var payloadParams = new Dictionary<string, object> + { + { "visitors", new object[] + { + new Dictionary<string, object>() + { + { "snapshots", new object[] + { + new Dictionary<string, object> + { + { "decisions", new object[] + { + new Dictionary<string, object> + { + {"campaign_id", null }, + {"experiment_id", null }, + {"variation_id", null }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "rollout" }, + { "FlagKey", "test_feature" }, + { "VariationKey", null } + } + } + } + } + }, + { "events", new object[] + { + new Dictionary<string, object> + { + {"entity_id", null }, + {"timestamp", timeStamp }, + {"uuid", guid }, + {"key", "campaign_activated" } + } + } + } + } + } + }, + {"attributes", new object[] + { + new Dictionary<string, object> + { + {"entity_id", "7723280020" }, + {"key", "device_type" }, + {"type", "custom" }, + {"value", "iPhone"} + }, + new Dictionary<string, object> + { + {"entity_id", "323434545" }, + {"key", "boolean_key" }, + {"type", "custom" }, + {"value", true} + }, + new Dictionary<string, object> + { + {"entity_id", "808797686" }, + {"key", "double_key" }, + {"type", "custom" }, + {"value", 3.14} + }, + new Dictionary<string, object> + { + {"entity_id", ControlAttributes.BOT_FILTERING_ATTRIBUTE}, + {"key", ControlAttributes.BOT_FILTERING_ATTRIBUTE}, + {"type", "custom" }, + {"value", true } + } + } + }, + { "visitor_id", TestUserId } + } + } + }, + {"project_id", "7720880029" }, + {"account_id", "1592310167" }, + {"enrich_decisions", true} , + {"client_name", "csharp-sdk" }, + {"client_version", Optimizely.SDK_VERSION }, + {"revision", "15" }, + {"anonymize_ip", false} + }; + + var expectedLogEvent = new LogEvent("https://logx.optimizely.com/v1/events", + payloadParams, + "POST", + new Dictionary<string, string> + { + { "Content-Type", "application/json" } + }); + + var userAttributes = new UserAttributes + { + { "device_type", "iPhone" }, + { "boolean_key", true }, + { "double_key", 3.14 }, + { "", "Android" }, + { "null", null }, + { "objects", new object() }, + { "arrays", new string[] { "a", "b", "c" } }, + { "negative_infinity", double.NegativeInfinity }, + { "positive_infinity", double.PositiveInfinity }, + { "nan", double.NaN }, + { "invalid_num_value", Math.Pow(2, 53) + 2 }, + }; + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, null, variation: null, TestUserId, userAttributes, "test_feature", "rollout"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); @@ -1333,7 +1495,13 @@ public void TestCreateImpressionEventWithBucketingIDAttribute() { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -1403,7 +1571,7 @@ public void TestCreateImpressionEventWithBucketingIDAttribute() { "company", "Optimizely" }, {ControlAttributes.BUCKETING_ID_ATTRIBUTE, "variation" } }; - var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); @@ -1433,7 +1601,13 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile() { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -1499,7 +1673,7 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile() botFilteringEnabledConfig.BotFiltering = true; var experiment = botFilteringEnabledConfig.GetExperimentFromKey("test_experiment"); - var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringEnabledConfig, experiment, "7722370027", TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringEnabledConfig, experiment, "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); @@ -1529,7 +1703,13 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile() { {"campaign_id", "7719770039" }, {"experiment_id", "7716830082" }, - {"variation_id", "7722370027" } + {"variation_id", "7722370027" }, + { "metadata", new Dictionary<string, object> { + { "FlagType", "experiment" }, + { "FlagKey", "test_experiment" }, + { "VariationKey", "control" } + } + } } } }, @@ -1588,7 +1768,7 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile() botFilteringDisabledConfig.BotFiltering = null; var experiment = botFilteringDisabledConfig.GetExperimentFromKey("test_experiment"); - var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringDisabledConfig, experiment, "7722370027", TestUserId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringDisabledConfig, experiment, "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); diff --git a/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs index 47300ca7..ce9bceda 100644 --- a/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs @@ -32,7 +32,7 @@ public void ImpressionEventTest() var variation = Config.GetVariationFromId(experiment.Key, "77210100090"); var userId = TestUserId; - var impressionEvent = UserEventFactory.CreateImpressionEvent(projectConfig, experiment, variation, userId, null); + var impressionEvent = UserEventFactory.CreateImpressionEvent(projectConfig, experiment, variation, userId, null, "test_experiment", "experiment"); Assert.AreEqual(Config.ProjectId, impressionEvent.Context.ProjectId); Assert.AreEqual(Config.Revision, impressionEvent.Context.Revision); @@ -58,7 +58,7 @@ public void ImpressionEventTestWithAttributes() { "company", "Optimizely" } }; - var impressionEvent = UserEventFactory.CreateImpressionEvent(projectConfig, experiment, variation, userId, userAttributes); + var impressionEvent = UserEventFactory.CreateImpressionEvent(projectConfig, experiment, variation, userId, userAttributes, "test_experiment", "experiment"); Assert.AreEqual(Config.ProjectId, impressionEvent.Context.ProjectId); Assert.AreEqual(Config.Revision, impressionEvent.Context.Revision); diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 26cb54e9..59afe18a 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -395,7 +395,6 @@ public void TestActivateNoAudienceNoAttributes() LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } @@ -430,7 +429,6 @@ public void TestActivateWithAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null.")); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -450,7 +448,6 @@ public void TestActivateWithNullAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -490,7 +487,6 @@ public void TestActivateWithTypedAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -683,7 +679,6 @@ public void TestInvalidDispatchImpressionEvent() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); // Need to see how error handler can be verified. LoggerMock.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once); @@ -1079,7 +1074,6 @@ public void TestActivateNoAudienceNoAttributesAfterSetForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index 63121246..35f9b6d8 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -63,6 +63,8 @@ public void TestInit() Assert.AreEqual("7720880029", Config.ProjectId); // Check Revision Assert.AreEqual("15", Config.Revision); + // Check SendFlagDecision + Assert.IsTrue(Config.SendFlagDecisions); // Check Group ID Map var expectedGroupId = CreateDictionary("7722400015", Config.GetGroup("7722400015")); diff --git a/OptimizelySDK/Event/Builder/EventBuilder.cs b/OptimizelySDK/Event/Builder/EventBuilder.cs index 8f24390b..2d31baf3 100644 --- a/OptimizelySDK/Event/Builder/EventBuilder.cs +++ b/OptimizelySDK/Event/Builder/EventBuilder.cs @@ -123,16 +123,11 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string } private Dictionary<string, object> GetImpressionParams(Experiment experiment, - string variationId, - string flagKey, - string flagType) + string variationId) { var impressionEvent = new Dictionary<string, object>(); - string variationKey = variationId != null ? experiment.VariationIdToVariationMap[variationId].Key : null; - var metadata = new DecisionMetadata(flagKey, flagType, variationKey); - var decisions = new object[] { new Dictionary<string, object> @@ -140,7 +135,6 @@ private Dictionary<string, object> GetImpressionParams(Experiment experiment, { Params.CAMPAIGN_ID, experiment.LayerId }, { Params.EXPERIMENT_ID, experiment.Id }, { Params.VARIATION_ID, variationId }, - { Params.METADATA, metadata } } }; @@ -224,11 +218,11 @@ private Dictionary<string, object> GetImpressionOrConversionParamsWithCommonPara /// <param name="userAttributes">associative array of attributes for the user</param> /// <returns>LogEvent object to be sent to dispatcher</returns> public virtual LogEvent CreateImpressionEvent(ProjectConfig config, Experiment experiment, string variationId, - string userId, UserAttributes userAttributes, string flagKey, string flagType) + string userId, UserAttributes userAttributes) { var commonParams = GetCommonParams(config, userId, userAttributes ?? new UserAttributes()); - var impressionOnlyParams = GetImpressionParams(experiment, variationId, flagKey, flagType); + var impressionOnlyParams = GetImpressionParams(experiment, variationId); var impressionParams = GetImpressionOrConversionParamsWithCommonParams(commonParams, new object[] { impressionOnlyParams }); diff --git a/OptimizelySDK/Event/Entity/Decision.cs b/OptimizelySDK/Event/Entity/Decision.cs index dc2d5fa8..32b30212 100644 --- a/OptimizelySDK/Event/Entity/Decision.cs +++ b/OptimizelySDK/Event/Entity/Decision.cs @@ -29,7 +29,7 @@ public class Decision public string VariationId { get; private set; } public Decision() {} - public Decision(string campaignId, string experimentId, string variationId, DecisionMetadata metadata) + public Decision(string campaignId, string experimentId, string variationId, DecisionMetadata metadata = null) { CampaignId = campaignId; ExperimentId = experimentId; diff --git a/OptimizelySDK/Event/Entity/DecisionMetadata.cs b/OptimizelySDK/Event/Entity/DecisionMetadata.cs index 8bc6318b..9ce28943 100644 --- a/OptimizelySDK/Event/Entity/DecisionMetadata.cs +++ b/OptimizelySDK/Event/Entity/DecisionMetadata.cs @@ -1,4 +1,22 @@ - +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Newtonsoft.Json; + namespace OptimizelySDK.Event.Entity { /// <summary> @@ -6,8 +24,11 @@ namespace OptimizelySDK.Event.Entity /// </summary> public class DecisionMetadata { + [JsonProperty("flag_type")] public string FlagType { get; private set; } + [JsonProperty("flag_key")] public string FlagKey { get; private set; } + [JsonProperty("variation_key")] public string VariationKey { get; private set; } public DecisionMetadata(string flagKey, string flagType, string variationKey = null) diff --git a/OptimizelySDK/Event/EventFactory.cs b/OptimizelySDK/Event/EventFactory.cs index f91e2fcf..211a801b 100644 --- a/OptimizelySDK/Event/EventFactory.cs +++ b/OptimizelySDK/Event/EventFactory.cs @@ -120,7 +120,7 @@ private static Visitor CreateVisitor(ImpressionEvent impressionEvent) { SnapshotEvent snapshotEvent = new SnapshotEvent.Builder() .WithUUID(impressionEvent.UUID) - .WithEntityId(impressionEvent.Experiment.LayerId) + .WithEntityId(impressionEvent.Experiment?.LayerId) .WithKey(ACTIVATE_EVENT_KEY) .WithTimeStamp(impressionEvent.Timestamp) .Build(); diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 5380198a..9b2b868a 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -690,29 +690,27 @@ private void SendImpressionEvent(Experiment experiment, Variation variation, str UserAttributes userAttributes, ProjectConfig config, string flagKey, string flagType) { - if (experiment.IsExperimentRunning) + if (experiment != null && !experiment.IsExperimentRunning) { - var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes, flagKey, flagType); - if (userEvent == null) - { - return; - } - EventProcessor.Process(userEvent); - Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); - - // Kept For backwards compatibility. - // This notification is deprecated and the new DecisionNotifications - // are sent via their respective method calls. - if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Activate) > 0) - { - var impressionEvent = EventFactory.CreateLogEvent(userEvent, Logger); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, - userAttributes, variation, impressionEvent); - } + Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation."); } - else + + var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, flagType); + if (userEvent == null) { - Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation."); + return; + } + EventProcessor.Process(userEvent); + //Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); + + // Kept For backwards compatibility. + // This notification is deprecated and the new DecisionNotifications + // are sent via their respective method calls. + if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Activate) > 0) + { + var impressionEvent = EventFactory.CreateLogEvent(userEvent, Logger); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, + userAttributes, variation, impressionEvent); } } From d82558dff8b8e79906f2bccdfbff8fc1d3165bf3 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 25 Sep 2020 21:48:08 +0500 Subject: [PATCH 04/15] Updated headers --- .../EventTests/EventEntitiesTest.cs | 20 ++++++++++++++++++- .../EventTests/EventFactoryTest.cs | 19 +++++++++++++++++- .../EventTests/UserEventFactoryTest.cs | 19 +++++++++++++++++- OptimizelySDK.Tests/TestData.json | 2 +- OptimizelySDK/Event/Builder/EventBuilder.cs | 2 +- OptimizelySDK/Event/Builder/Params.cs | 2 +- OptimizelySDK/Event/Entity/Decision.cs | 2 +- OptimizelySDK/Event/Entity/ImpressionEvent.cs | 2 +- OptimizelySDK/Event/EventFactory.cs | 2 +- OptimizelySDK/Event/UserEventFactory.cs | 18 ++++++++++++++++- 10 files changed, 78 insertions(+), 10 deletions(-) diff --git a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs index 6463974c..6ceca9b1 100644 --- a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs @@ -1,4 +1,22 @@ -using System; +/** + * + * Copyright 2019-2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index 5534a451..9c5ae4e2 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -1,4 +1,21 @@ -using System; +/** + * + * Copyright 2019-2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; using System.Collections.Generic; using Moq; using NUnit.Framework; diff --git a/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs index ce9bceda..80acd1a6 100644 --- a/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs @@ -1,4 +1,21 @@ -using Moq; +/** + * + * Copyright 2019-2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Moq; using NUnit.Framework; using OptimizelySDK.Config; using OptimizelySDK.Entity; diff --git a/OptimizelySDK.Tests/TestData.json b/OptimizelySDK.Tests/TestData.json index b8b72fbc..9724b0b4 100644 --- a/OptimizelySDK.Tests/TestData.json +++ b/OptimizelySDK.Tests/TestData.json @@ -565,7 +565,7 @@ "key": "integer_variable", "type": "integer", "defaultValue": "7" - } + } ] }, { diff --git a/OptimizelySDK/Event/Builder/EventBuilder.cs b/OptimizelySDK/Event/Builder/EventBuilder.cs index 2d31baf3..8b574983 100644 --- a/OptimizelySDK/Event/Builder/EventBuilder.cs +++ b/OptimizelySDK/Event/Builder/EventBuilder.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019, Optimizely + * Copyright 2017-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/OptimizelySDK/Event/Builder/Params.cs b/OptimizelySDK/Event/Builder/Params.cs index 14d0a994..f3b47688 100644 --- a/OptimizelySDK/Event/Builder/Params.cs +++ b/OptimizelySDK/Event/Builder/Params.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017, 2019, Optimizely + * Copyright 2017, 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/OptimizelySDK/Event/Entity/Decision.cs b/OptimizelySDK/Event/Entity/Decision.cs index 32b30212..08657a33 100644 --- a/OptimizelySDK/Event/Entity/Decision.cs +++ b/OptimizelySDK/Event/Entity/Decision.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/OptimizelySDK/Event/Entity/ImpressionEvent.cs b/OptimizelySDK/Event/Entity/ImpressionEvent.cs index d8226f97..5d525b2d 100644 --- a/OptimizelySDK/Event/Entity/ImpressionEvent.cs +++ b/OptimizelySDK/Event/Entity/ImpressionEvent.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/OptimizelySDK/Event/EventFactory.cs b/OptimizelySDK/Event/EventFactory.cs index 211a801b..63482a8b 100644 --- a/OptimizelySDK/Event/EventFactory.cs +++ b/OptimizelySDK/Event/EventFactory.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs index 22296c1c..3593b0d8 100644 --- a/OptimizelySDK/Event/UserEventFactory.cs +++ b/OptimizelySDK/Event/UserEventFactory.cs @@ -1,4 +1,20 @@ -using OptimizelySDK.Bucketing; +/** + * + * Copyright 2019-2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using OptimizelySDK.Entity; using OptimizelySDK.Event.Entity; From 8183bcc7bc7d0f17b91f22f17fbe44e0bd234d9e Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Mon, 28 Sep 2020 03:06:36 +0500 Subject: [PATCH 05/15] passing null variation as variable --- OptimizelySDK.Tests/EventTests/EventFactoryTest.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index 9c5ae4e2..e2f506ce 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -57,8 +57,9 @@ public void TestCreateImpressionEventReturnsNullWhenSendFlagDecisionsIsFalseAndI public void TestCreateImpressionEventReturnsNullWhenSendFlagDecisionsIsFalseAndVariationIsNull() { Config.SendFlagDecisions = false; + Variation variation = null; var impressionEvent = UserEventFactory.CreateImpressionEvent( - Config, Config.GetExperimentFromKey("test_experiment"), variation: null, TestUserId, null, "test_experiment", "experiment"); + Config, Config.GetExperimentFromKey("test_experiment"), variation, TestUserId, null, "test_experiment", "experiment"); Assert.IsNull(impressionEvent); } @@ -592,7 +593,9 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout( { "nan", double.NaN }, { "invalid_num_value", Math.Pow(2, 53) + 2 }, }; - var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, null, variation: null, TestUserId, userAttributes, "test_feature", "rollout"); + Variation variation = null; + + var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, null, variation, TestUserId, userAttributes, "test_feature", "rollout"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); From 94a582185028ce569da1c85a2705662a1a9aa656 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Mon, 28 Sep 2020 03:34:12 +0500 Subject: [PATCH 06/15] unit test fix converted to snakecase --- .../EventTests/EventFactoryTest.cs | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index e2f506ce..7002fd51 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -82,10 +82,11 @@ public void TestCreateImpressionEventNoAttributes() { "campaign_id", "7719770039" }, { "experiment_id", "7716830082" }, { "variation_id", "7722370027" }, - { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "metadata", + new Dictionary<string, object> { + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } } @@ -163,9 +164,9 @@ public void TestCreateImpressionEventWithAttributes() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -262,9 +263,9 @@ public void TestCreateImpressionEventWithTypedAttributes() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -383,9 +384,9 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -504,10 +505,10 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout( {"experiment_id", null }, {"variation_id", null }, { "metadata", new Dictionary<string, object> { - { "FlagType", "rollout" }, - { "FlagKey", "test_feature" }, - { "VariationKey", null } - } + { "flag_type", "rollout" }, + { "flag_key", "test_feature" }, + { "variation_key", null } + } } } } @@ -597,7 +598,7 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout( var impressionEvent = UserEventFactory.CreateImpressionEvent(Config, null, variation, TestUserId, userAttributes, "test_feature", "rollout"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); - + TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent)); @@ -1517,9 +1518,9 @@ public void TestCreateImpressionEventWithBucketingIDAttribute() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -1623,9 +1624,9 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -1695,7 +1696,7 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile() var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringEnabledConfig, experiment, "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); - + TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent)); @@ -1725,9 +1726,9 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "FlagType", "experiment" }, - { "FlagKey", "test_experiment" }, - { "VariationKey", "control" } + { "flag_type", "experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -1790,7 +1791,7 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile() var impressionEvent = UserEventFactory.CreateImpressionEvent(botFilteringDisabledConfig, experiment, "7722370027", TestUserId, userAttributes, "test_experiment", "experiment"); var logEvent = EventFactory.CreateLogEvent(impressionEvent, Logger); - + TestData.ChangeGUIDAndTimeStamp(expectedLogEvent.Params, impressionEvent.Timestamp, Guid.Parse(impressionEvent.UUID)); Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent)); @@ -1880,7 +1881,7 @@ public void TestCreateConversionEventWhenBotFilteringIsProvidedInDatafile() var conversionEvent = UserEventFactory.CreateConversionEvent(botFilteringEnabledConfig, "purchase", TestUserId, userAttributes, null); var logEvent = EventFactory.CreateLogEvent(conversionEvent, Logger); - + TestData.ChangeGUIDAndTimeStamp(expectedEvent.Params, conversionEvent.Timestamp, Guid.Parse(conversionEvent.UUID)); Assert.IsTrue(TestData.CompareObjects(expectedEvent, logEvent)); @@ -1962,7 +1963,7 @@ public void TestCreateConversionEventWhenBotFilteringIsNotProvidedInDatafile() botFilteringDisabledConfig.BotFiltering = null; var conversionEvent = UserEventFactory.CreateConversionEvent(botFilteringDisabledConfig, "purchase", TestUserId, userAttributes, null); - var logEvent = EventFactory.CreateLogEvent(conversionEvent, Logger); + var logEvent = EventFactory.CreateLogEvent(conversionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedEvent.Params, conversionEvent.Timestamp, Guid.Parse(conversionEvent.UUID)); @@ -1986,7 +1987,7 @@ public void TestCreateConversionEventWhenEventUsedInMultipleExp() "111130", new Variation{Id="111131", Key="variation"} } }; - + var payloadParams = new Dictionary<string, object> { {"client_version", Optimizely.SDK_VERSION}, @@ -2187,7 +2188,7 @@ public void TestCreateConversionEventRemovesInvalidAttributesFromPayload() {"7716830082", new Variation{Id="7722370027", Key="control"} } }; var conversionEvent = UserEventFactory.CreateConversionEvent(Config, "purchase", TestUserId, userAttributes, null); - var logEvent = EventFactory.CreateLogEvent(conversionEvent, Logger); + var logEvent = EventFactory.CreateLogEvent(conversionEvent, Logger); TestData.ChangeGUIDAndTimeStamp(expectedEvent.Params, conversionEvent.Timestamp, Guid.Parse(conversionEvent.UUID)); Assert.IsTrue(TestData.CompareObjects(expectedEvent, logEvent)); From 315aa55059f638a3d058213115e9fbdf61d4bc74 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Mon, 28 Sep 2020 20:06:10 +0500 Subject: [PATCH 07/15] removed invalid path decision metadata from cs16proj --- OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 7d8e0fde..868c1d8c 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -24,7 +24,6 @@ <Compile Include="..\OptimizelySDK\AudienceConditions\OrCondition.cs" /> <Compile Include="..\OptimizelySDK\Entity\Attribute.cs" /> <Compile Include="..\OptimizelySDK\Entity\Audience.cs" /> - <Compile Include="..\OptimizelySDK\Entity\DecisionMetadata.cs" /> <Compile Include="..\OptimizelySDK\Entity\Entity.cs" /> <Compile Include="..\OptimizelySDK\Entity\Event.cs" /> <Compile Include="..\OptimizelySDK\Entity\EventTags.cs" /> From 823bdb26584ab68f0ed111640c287306ac080fbb Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Tue, 29 Sep 2020 13:23:30 +0500 Subject: [PATCH 08/15] Added test to check sendFlagDecision is false in case if it's missing in datafile. --- OptimizelySDK.Tests/ProjectConfigTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index 35f9b6d8..98d38dec 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -417,6 +417,14 @@ public void TestInit() Assert.IsTrue(TestData.CompareObjects(expectedRolloutIdMap, Config.RolloutIdMap)); } + + [Test] + public void TestIfSendFlagDecisionKeyIsMissingItShouldReturnFalse() + { + var tempConfig = DatafileProjectConfig.Create(TestData.SimpleABExperimentsDatafile, LoggerMock.Object, ErrorHandlerMock.Object); + Assert.IsFalse(tempConfig.SendFlagDecisions); + } + [Test] public void TestGetAccountId() { From 719eb73740c09fb000d31183d5314f30f5330015 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Wed, 30 Sep 2020 13:10:05 +0500 Subject: [PATCH 09/15] reverted changes made in this file --- OptimizelySDK/Event/Builder/EventBuilder.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/OptimizelySDK/Event/Builder/EventBuilder.cs b/OptimizelySDK/Event/Builder/EventBuilder.cs index 8b574983..0f0b41b8 100644 --- a/OptimizelySDK/Event/Builder/EventBuilder.cs +++ b/OptimizelySDK/Event/Builder/EventBuilder.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020, Optimizely + * Copyright 2017-2019, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ using OptimizelySDK.Bucketing; using OptimizelySDK.Entity; -using OptimizelySDK.Event.Entity; using OptimizelySDK.Logger; using OptimizelySDK.Utils; using System; @@ -122,19 +121,18 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string return comonParams; } - private Dictionary<string, object> GetImpressionParams(Experiment experiment, - string variationId) + private Dictionary<string, object> GetImpressionParams(Experiment experiment, string variationId) { var impressionEvent = new Dictionary<string, object>(); - + var decisions = new object[] { new Dictionary<string, object> { { Params.CAMPAIGN_ID, experiment.LayerId }, { Params.EXPERIMENT_ID, experiment.Id }, - { Params.VARIATION_ID, variationId }, + { Params.VARIATION_ID, variationId } } }; From 724a86bccc82bbaf1a82b88799677e3ab01ca2f3 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 2 Oct 2020 04:14:28 +0500 Subject: [PATCH 10/15] Reverted removal of Activating user log and added condition to do not log if experiment is null --- OptimizelySDK.Tests/OptimizelyTest.cs | 6 ++++++ OptimizelySDK/Optimizely.cs | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 59afe18a..4b40e0de 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -395,6 +395,7 @@ public void TestActivateNoAudienceNoAttributes() LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } @@ -429,6 +430,7 @@ public void TestActivateWithAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null.")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -448,6 +450,7 @@ public void TestActivateWithNullAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -487,6 +490,7 @@ public void TestActivateWithTypedAttributes() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -680,6 +684,7 @@ public void TestInvalidDispatchImpressionEvent() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); // Need to see how error handler can be verified. LoggerMock.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once); @@ -1074,6 +1079,7 @@ public void TestActivateNoAudienceNoAttributesAfterSetForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 9b2b868a..3c5c3c3a 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -701,8 +701,11 @@ private void SendImpressionEvent(Experiment experiment, Variation variation, str return; } EventProcessor.Process(userEvent); - //Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); + if (experiment != null) + { + Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); + } // Kept For backwards compatibility. // This notification is deprecated and the new DecisionNotifications // are sent via their respective method calls. From 2aa8e8dc27ddb68de67906bbb422b827e129b279 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Mon, 12 Oct 2020 18:41:08 +0500 Subject: [PATCH 11/15] Renamed flag_type to rule_type and added rule_key --- .../EventTests/EventEntitiesTest.cs | 2 +- .../EventTests/EventFactoryTest.cs | 30 ++++++++++++------- .../Event/Entity/DecisionMetadata.cs | 11 ++++--- OptimizelySDK/Event/UserEventFactory.cs | 24 ++++++++++----- OptimizelySDK/Optimizely.cs | 4 +-- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs index 6ceca9b1..10e5c3e4 100644 --- a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs @@ -122,7 +122,7 @@ public void TestImpressionEventEqualsSerializedPayload() .WithTimeStamp(timeStamp) .WithEventTags(null) .Build(); - var metadata = new DecisionMetadata("experiment", "7716830082"); + var metadata = new DecisionMetadata("experiment", "experiment_key", "7716830082"); var decision = new Decision("7719770039", "7716830082", "77210100090"); var snapshot = new Snapshot(events: new SnapshotEvent[] { snapshotEvent }, decisions: new Decision[] { decision }); diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index 7002fd51..00308aa7 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -84,7 +84,8 @@ public void TestCreateImpressionEventNoAttributes() { "variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } } @@ -164,7 +165,8 @@ public void TestCreateImpressionEventWithAttributes() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } @@ -263,7 +265,8 @@ public void TestCreateImpressionEventWithTypedAttributes() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } @@ -384,9 +387,10 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, - { "flag_key", "test_experiment" }, - { "variation_key", "control" } + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, + { "flag_key", "test_experiment" }, + { "variation_key", "control" } } } } @@ -505,9 +509,10 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout( {"experiment_id", null }, {"variation_id", null }, { "metadata", new Dictionary<string, object> { - { "flag_type", "rollout" }, + { "rule_type", "" }, + { "rule_key", "" }, { "flag_key", "test_feature" }, - { "variation_key", null } + { "variation_key", "" } } } } @@ -1518,7 +1523,8 @@ public void TestCreateImpressionEventWithBucketingIDAttribute() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } @@ -1624,7 +1630,8 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } @@ -1726,7 +1733,8 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile() {"experiment_id", "7716830082" }, {"variation_id", "7722370027" }, { "metadata", new Dictionary<string, object> { - { "flag_type", "experiment" }, + { "rule_type", "experiment" }, + { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" } } diff --git a/OptimizelySDK/Event/Entity/DecisionMetadata.cs b/OptimizelySDK/Event/Entity/DecisionMetadata.cs index 9ce28943..551eb5a0 100644 --- a/OptimizelySDK/Event/Entity/DecisionMetadata.cs +++ b/OptimizelySDK/Event/Entity/DecisionMetadata.cs @@ -24,17 +24,20 @@ namespace OptimizelySDK.Event.Entity /// </summary> public class DecisionMetadata { - [JsonProperty("flag_type")] - public string FlagType { get; private set; } [JsonProperty("flag_key")] public string FlagKey { get; private set; } + [JsonProperty("rule_key")] + public string RuleKey { get; private set; } + [JsonProperty("rule_type")] + public string RuleType { get; private set; } [JsonProperty("variation_key")] public string VariationKey { get; private set; } - public DecisionMetadata(string flagKey, string flagType, string variationKey = null) + public DecisionMetadata(string flagKey, string ruleKey, string ruleType, string variationKey = "") { - FlagType = flagType; FlagKey = flagKey; + RuleKey = ruleKey; + RuleType = ruleType; VariationKey = variationKey; } } diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs index 3593b0d8..6eac5b2e 100644 --- a/OptimizelySDK/Event/UserEventFactory.cs +++ b/OptimizelySDK/Event/UserEventFactory.cs @@ -40,10 +40,10 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, string userId, UserAttributes userAttributes, string flagKey, - string flagType) + string ruleType) { Variation variation = projectConfig.GetVariationFromId(activatedExperiment?.Key, variationId); - return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes, flagKey, flagType); + return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes, flagKey, ruleType); } /// <summary> @@ -55,7 +55,7 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, /// <param name="userId">The user Id</param> /// <param name="userAttributes">The user's attributes</param> /// <param name="flagKey">experiment key or feature key</param> - /// <param name="flagType">experiment or featureDecision source </param> + /// <param name="ruleType">experiment or featureDecision source </param> /// <returns>ImpressionEvent instance</returns> public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, Experiment activatedExperiment, @@ -63,13 +63,13 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, string userId, UserAttributes userAttributes, string flagKey, - string flagType) + string ruleType) { - if ((flagType == FeatureDecision.DECISION_SOURCE_ROLLOUT || variation == null) && !projectConfig.SendFlagDecisions) + if ((ruleType == FeatureDecision.DECISION_SOURCE_ROLLOUT || variation == null) && !projectConfig.SendFlagDecisions) { return null; } - + var eventContext = new EventContext.Builder() .WithProjectId(projectConfig.ProjectId) .WithAccountId(projectConfig.AccountId) @@ -77,8 +77,16 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, .WithRevision(projectConfig.Revision) .Build(); - var variationKey = variation?.Key; - var metadata = new DecisionMetadata(flagKey, flagType, variationKey); + var variationKey = ""; + var ruleKey = ""; + var finalRuleType = ""; + if (variation != null) + { + variationKey = variation.Key; + ruleKey = activatedExperiment.Key; + finalRuleType = ruleType; + } + var metadata = new DecisionMetadata(flagKey, ruleKey, finalRuleType, variationKey); return new ImpressionEvent.Builder() .WithEventContext(eventContext) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 3c5c3c3a..41c08312 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -688,14 +688,14 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// <param name="userAttributes">The user's attributes</param> private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, UserAttributes userAttributes, ProjectConfig config, - string flagKey, string flagType) + string flagKey, string ruleType) { if (experiment != null && !experiment.IsExperimentRunning) { Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation."); } - var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, flagType); + var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, ruleType); if (userEvent == null) { return; From e5a4ae84f282ca351478817cd503aadc0231c89a Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Tue, 13 Oct 2020 19:23:31 +0500 Subject: [PATCH 12/15] Added decision to rollout --- OptimizelySDK/Optimizely.cs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 41c08312..216ff080 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -252,7 +252,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes, config, experimentKey, "experiment"); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, "experiment"); return variation; } @@ -471,8 +471,13 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri var sourceInfo = new Dictionary<string, string>(); var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); var variation = decision.Variation; + var decisionSource = FeatureDecision.DECISION_SOURCE_ROLLOUT; + if (decisionSource != null) + { + decisionSource = decision.Source; + } - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decision.Source); + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); if (variation != null) { @@ -686,6 +691,23 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// <param name="variation">The variation entity</param> /// <param name="userId">The user ID</param> /// <param name="userAttributes">The user's attributes</param> + /// <param name="ruleType">It can either be experiment in case impression event is sent from activate or it's feature-test or rollout</param> + private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, + UserAttributes userAttributes, ProjectConfig config, + string ruleType) + { + SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType); + } + + /// <summary> + /// Sends impression event. + /// </summary> + /// <param name="experiment">The experiment</param> + /// <param name="variation">The variation entity</param> + /// <param name="userId">The user ID</param> + /// <param name="userAttributes">The user's attributes</param> + /// <param name="flagKey">It can either be experiment key in case if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout</param> + /// <param name="ruleType">It can either be experiment in case impression event is sent from activate or it's feature-test or rollout</param> private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, UserAttributes userAttributes, ProjectConfig config, string flagKey, string ruleType) From e8db3cb002718ac13c928d1796e8ff2a465e4bf6 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Tue, 13 Oct 2020 19:34:59 +0500 Subject: [PATCH 13/15] when variation is null then ruleType will be rollout --- OptimizelySDK/Event/UserEventFactory.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs index 6eac5b2e..ff78d495 100644 --- a/OptimizelySDK/Event/UserEventFactory.cs +++ b/OptimizelySDK/Event/UserEventFactory.cs @@ -79,14 +79,12 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig, var variationKey = ""; var ruleKey = ""; - var finalRuleType = ""; if (variation != null) { variationKey = variation.Key; ruleKey = activatedExperiment.Key; - finalRuleType = ruleType; } - var metadata = new DecisionMetadata(flagKey, ruleKey, finalRuleType, variationKey); + var metadata = new DecisionMetadata(flagKey, ruleKey, ruleType, variationKey); return new ImpressionEvent.Builder() .WithEventContext(eventContext) From 6c8d8f1140e0f0887e80c1774e81b8901659faa3 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Tue, 13 Oct 2020 20:40:42 +0500 Subject: [PATCH 14/15] unit test fix --- OptimizelySDK.Tests/EventTests/EventFactoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index 00308aa7..eb53073c 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -509,7 +509,7 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayloadRollout( {"experiment_id", null }, {"variation_id", null }, { "metadata", new Dictionary<string, object> { - { "rule_type", "" }, + { "rule_type", "rollout" }, { "rule_key", "" }, { "flag_key", "test_feature" }, { "variation_key", "" } From 8c13a0eb14dfaa882c65013e3590579dd8e583bd Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Wed, 14 Oct 2020 01:52:39 +0500 Subject: [PATCH 15/15] Refact: made experiment constant refact decision source condition --- OptimizelySDK/Optimizely.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 216ff080..cfc1a186 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -100,6 +100,8 @@ public static String SDK_TYPE public const string EVENT_KEY = "Event Key"; public const string FEATURE_KEY = "Feature Key"; public const string VARIABLE_KEY = "Variable Key"; + private const string SOURCE_TYPE_EXPERIMENT = "experiment"; + public bool Disposed { get; private set; } /// <summary> @@ -252,7 +254,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes, config, "experiment"); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, SOURCE_TYPE_EXPERIMENT); return variation; } @@ -471,11 +473,7 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri var sourceInfo = new Dictionary<string, string>(); var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); var variation = decision.Variation; - var decisionSource = FeatureDecision.DECISION_SOURCE_ROLLOUT; - if (decisionSource != null) - { - decisionSource = decision.Source; - } + var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);