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 @@
Event\Entity\Visitor.cs
+
+ Event\Entity\DecisionMetadata.cs
+
Event\Entity\VisitorAttribute.cs
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 @@
Event\Entity\VisitorAttribute.cs
+
+ Event\Entity\DecisionMetadata.cs
+
Event\EventFactory.cs
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index a38425a0..868c1d8c 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -121,6 +121,9 @@
VisitorAttribute.cs
+
+ DecisionMetadata.cs
+
DecisionEvent.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 @@
Event\Entity\Decision.cs
+
+
+ Event\Entity\DecisionMetadata.cs
Event\Entity\EventBatch.cs
diff --git a/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs b/OptimizelySDK.Tests/EventTests/EventEntitiesTest.cs
index 15c4e28d..10e5c3e4 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;
@@ -104,7 +122,7 @@ public void TestImpressionEventEqualsSerializedPayload()
.WithTimeStamp(timeStamp)
.WithEventTags(null)
.Build();
-
+ 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 338d36c1..eb53073c 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;
@@ -27,6 +44,25 @@ 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;
+ Variation variation = null;
+ var impressionEvent = UserEventFactory.CreateImpressionEvent(
+ Config, Config.GetExperimentFromKey("test_experiment"), variation, TestUserId, null, "test_experiment", "experiment");
+ Assert.IsNull(impressionEvent);
+ }
+
[Test]
public void TestCreateImpressionEventNoAttributes()
{
@@ -45,7 +81,14 @@ public void TestCreateImpressionEventNoAttributes()
new Dictionary {
{ "campaign_id", "7719770039" },
{ "experiment_id", "7716830082" },
- { "variation_id", "7722370027" }
+ { "variation_id", "7722370027" },
+ { "metadata",
+ new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ } }
}
}
},
@@ -93,7 +136,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 +163,14 @@ public void TestCreateImpressionEventWithAttributes()
new Dictionary {
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -183,7 +233,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 +263,14 @@ public void TestCreateImpressionEventWithTypedAttributes()
{
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -298,7 +355,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 +385,14 @@ public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload()
{
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -413,9 +477,133 @@ 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
+ {
+ { "visitors", new object[]
+ {
+ new Dictionary()
+ {
+ { "snapshots", new object[]
+ {
+ new Dictionary
+ {
+ { "decisions", new object[]
+ {
+ new Dictionary
+ {
+ {"campaign_id", null },
+ {"experiment_id", null },
+ {"variation_id", null },
+ { "metadata", new Dictionary {
+ { "rule_type", "rollout" },
+ { "rule_key", "" },
+ { "flag_key", "test_feature" },
+ { "variation_key", "" }
+ }
+ }
+ }
+ }
+ },
+ { "events", new object[]
+ {
+ new Dictionary
+ {
+ {"entity_id", null },
+ {"timestamp", timeStamp },
+ {"uuid", guid },
+ {"key", "campaign_activated" }
+ }
+ }
+ }
+ }
+ }
+ },
+ {"attributes", new object[]
+ {
+ new Dictionary
+ {
+ {"entity_id", "7723280020" },
+ {"key", "device_type" },
+ {"type", "custom" },
+ {"value", "iPhone"}
+ },
+ new Dictionary
+ {
+ {"entity_id", "323434545" },
+ {"key", "boolean_key" },
+ {"type", "custom" },
+ {"value", true}
+ },
+ new Dictionary
+ {
+ {"entity_id", "808797686" },
+ {"key", "double_key" },
+ {"type", "custom" },
+ {"value", 3.14}
+ },
+ new Dictionary
+ {
+ {"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
+ {
+ { "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 },
+ };
+ 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));
Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent));
@@ -1333,7 +1521,14 @@ public void TestCreateImpressionEventWithBucketingIDAttribute()
{
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -1403,7 +1598,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 +1628,14 @@ public void TestCreateImpressionEventWhenBotFilteringIsProvidedInDatafile()
{
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -1499,9 +1701,9 @@ 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));
Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent));
@@ -1529,7 +1731,14 @@ public void TestCreateImpressionEventWhenBotFilteringIsNotProvidedInDatafile()
{
{"campaign_id", "7719770039" },
{"experiment_id", "7716830082" },
- {"variation_id", "7722370027" }
+ {"variation_id", "7722370027" },
+ { "metadata", new Dictionary {
+ { "rule_type", "experiment" },
+ { "rule_key", "test_experiment" },
+ { "flag_key", "test_experiment" },
+ { "variation_key", "control" }
+ }
+ }
}
}
},
@@ -1588,9 +1797,9 @@ 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));
Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent));
@@ -1680,7 +1889,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));
@@ -1762,7 +1971,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));
@@ -1786,7 +1995,7 @@ public void TestCreateConversionEventWhenEventUsedInMultipleExp()
"111130", new Variation{Id="111131", Key="variation"}
}
};
-
+
var payloadParams = new Dictionary
{
{"client_version", Optimizely.SDK_VERSION},
@@ -1987,7 +2196,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));
diff --git a/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/UserEventFactoryTest.cs
index 47300ca7..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;
@@ -32,7 +49,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 +75,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..4b40e0de 100644
--- a/OptimizelySDK.Tests/OptimizelyTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyTest.cs
@@ -395,7 +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);
+ 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 +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);
+ 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 +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);
+ LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once);
Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation));
}
@@ -683,8 +683,8 @@ 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);
+ 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()), Times.Once);
@@ -1079,7 +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);
+ 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..98d38dec 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"));
@@ -415,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()
{
diff --git a/OptimizelySDK.Tests/TestData.json b/OptimizelySDK.Tests/TestData.json
index 11f3decb..9724b0b4 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
///
public string Revision { get; set; }
+ ///
+ /// SendFlagDecisions determines whether impressions events are sent for ALL decision types.
+ ///
+ public bool SendFlagDecisions { get; set; }
+
///
/// Allow Anonymize IP by truncating the last block of visitors' IP address.
///
diff --git a/OptimizelySDK/Event/Builder/Params.cs b/OptimizelySDK/Event/Builder/Params.cs
index 8b12f96a..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.
@@ -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..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.
@@ -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 = null)
{
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..551eb5a0
--- /dev/null
+++ b/OptimizelySDK/Event/Entity/DecisionMetadata.cs
@@ -0,0 +1,44 @@
+/**
+ *
+ * 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
+{
+ ///
+ /// DecisionMetadata captures additional information regarding the decision
+ ///
+ public class DecisionMetadata
+ {
+ [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 ruleKey, string ruleType, string variationKey = "")
+ {
+ FlagKey = flagKey;
+ RuleKey = ruleKey;
+ RuleType = ruleType;
+ VariationKey = variationKey;
+ }
+ }
+}
diff --git a/OptimizelySDK/Event/Entity/ImpressionEvent.cs b/OptimizelySDK/Event/Entity/ImpressionEvent.cs
index 7b3777a1..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.
@@ -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..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.
@@ -115,11 +115,12 @@ 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)
- .WithEntityId(impressionEvent.Experiment.LayerId)
+ .WithEntityId(impressionEvent.Experiment?.LayerId)
.WithKey(ACTIVATE_EVENT_KEY)
.WithTimeStamp(impressionEvent.Timestamp)
.Build();
diff --git a/OptimizelySDK/Event/UserEventFactory.cs b/OptimizelySDK/Event/UserEventFactory.cs
index 2733c673..ff78d495 100644
--- a/OptimizelySDK/Event/UserEventFactory.cs
+++ b/OptimizelySDK/Event/UserEventFactory.cs
@@ -1,4 +1,21 @@
-using OptimizelySDK.Entity;
+/**
+ *
+ * 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;
namespace OptimizelySDK.Event
@@ -21,10 +38,12 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig,
Experiment activatedExperiment,
string variationId,
string userId,
- UserAttributes userAttributes)
+ UserAttributes userAttributes,
+ string flagKey,
+ string ruleType)
{
Variation variation = projectConfig.GetVariationFromId(activatedExperiment?.Key, variationId);
- return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes);
+ return CreateImpressionEvent(projectConfig, activatedExperiment, variation, userId, userAttributes, flagKey, ruleType);
}
///
@@ -35,25 +54,43 @@ public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig,
/// The variation entity
/// The user Id
/// The user's attributes
+ /// experiment key or feature key
+ /// experiment or featureDecision source
/// ImpressionEvent instance
public static ImpressionEvent CreateImpressionEvent(ProjectConfig projectConfig,
Experiment activatedExperiment,
Variation variation,
string userId,
- UserAttributes userAttributes)
+ UserAttributes userAttributes,
+ string flagKey,
+ string ruleType)
{
+ if ((ruleType == 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 = "";
+ var ruleKey = "";
+ if (variation != null)
+ {
+ variationKey = variation.Key;
+ ruleKey = activatedExperiment.Key;
+ }
+ var metadata = new DecisionMetadata(flagKey, ruleKey, ruleType, 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..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; }
///
@@ -252,7 +254,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us
return null;
}
- SendImpressionEvent(experiment, variation, userId, userAttributes, config);
+ SendImpressionEvent(experiment, variation, userId, userAttributes, config, SOURCE_TYPE_EXPERIMENT);
return variation;
}
@@ -470,17 +472,21 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri
bool featureEnabled = false;
var sourceInfo = new Dictionary();
var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
+ var variation = decision.Variation;
+ var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
- if (decision.Variation != null)
+ SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
+
+ if (variation != null)
{
- var variation = decision.Variation;
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;
sourceInfo["variationKey"] = variation.Key;
- SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config);
}
else
{
@@ -683,28 +689,51 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK
/// The variation entity
/// The user ID
/// The user's attributes
+ /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
private void SendImpressionEvent(Experiment experiment, Variation variation, string userId,
- UserAttributes userAttributes, ProjectConfig config)
+ UserAttributes userAttributes, ProjectConfig config,
+ string ruleType)
{
- if (experiment.IsExperimentRunning)
+ SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType);
+ }
+
+ ///
+ /// Sends impression event.
+ ///
+ /// The experiment
+ /// The variation entity
+ /// The user ID
+ /// The user's attributes
+ /// 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
+ /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
+ private void SendImpressionEvent(Experiment experiment, Variation variation, string userId,
+ UserAttributes userAttributes, ProjectConfig config,
+ string flagKey, string ruleType)
+ {
+ if (experiment != null && !experiment.IsExperimentRunning)
{
- var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes);
- EventProcessor.Process(userEvent);
- Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}.");
+ Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation.");
+ }
+
+ var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, ruleType);
+ if (userEvent == null)
+ {
+ return;
+ }
+ EventProcessor.Process(userEvent);
- // 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);
- }
+ if (experiment != null)
+ {
+ Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}.");
}
- else
+ // 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)
{
- Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation.");
+ var impressionEvent = EventFactory.CreateLogEvent(userEvent, Logger);
+ NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId,
+ userAttributes, variation, impressionEvent);
}
}
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 @@
+
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
///
string Revision { get; set; }
+
+ ///
+ /// SendFlagDecisions determines whether impressions events are sent for ALL decision types.
+ ///
+ bool SendFlagDecisions { get; set; }
+
///
/// Allow Anonymize IP by truncating the last block of visitors' IP address.
///