-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(flag-decisions): Add support for sending flag decisions along with decision metadata. #370
Conversation
@@ -42,6 +42,7 @@ struct Project: Codable, Equatable { | |||
var typedAudiences: [Audience]? | |||
var featureFlags: [FeatureFlag] | |||
var botFiltering: Bool? | |||
var sendFlagDecisions: Bool? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since Project
struct is marked codable, for every value that might not exist, we have to mark it as optional, otherwise decoding a datafile with no sendFlagDecisions
into Project
struct will cause a crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please address.
@@ -177,6 +177,13 @@ extension ProjectConfig { | |||
|
|||
extension ProjectConfig { | |||
|
|||
/** | |||
* Determines whether impressions events are sent for ALL decision types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get sendFlagDecisions values...
//Evaluate in this order: | ||
|
||
//1. Attempt to bucket user into experiment using feature flag. | ||
// Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments | ||
if let pair = getVariationForFeatureExperiment(config: config, featureFlag: featureFlag, userId: userId, attributes: attributes) { | ||
return pair | ||
return (pair.experiment, pair.variation, Constants.DecisionSource.featureTest.rawValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not returning pair
? discuss offline.
|
||
var variationId = "" | ||
var variationKey = "" | ||
if let tmpVariation = variation { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why using it?
variation?.Id should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Value of optional type 'String?' must be unwrapped to a value of type 'String' before assigning it into the struct. This change is there since variation can now be nil too.
sendDecisionNotification(decisionType: .feature, | ||
userId: userId, | ||
attributes: attributes, | ||
source: Constants.DecisionSource.rollout.rawValue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to check.
attributes: attributes, | ||
event: event, | ||
async: false) | ||
if let tmpVariation = variation { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why need to add this condition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variation was non-optional before so we didn't have to unwrap it, but since we have allowed impression event's to be sent with nil variation, this is necessary!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
2. Added tests for rule_key in metadata.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good. See my comments for change requests.
Sources/Data Model/Project.swift
Outdated
@@ -52,13 +53,13 @@ struct Project: Codable, Equatable { | |||
// V3 | |||
case anonymizeIP | |||
// V4 | |||
case rollouts, typedAudiences, featureFlags, botFiltering | |||
case rollouts, typedAudiences, featureFlags, botFiltering, sendFlagDecisions | |||
} | |||
|
|||
// Required since logger in not equatable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Required since logger in not equatable | |
// Required since logger is not equatable |
func sendFlagDecisions() -> Bool { | ||
return project.sendFlagDecisions ?? false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function name misleads as an action. Let's change it to comp variable.
func sendFlagDecisions() -> Bool { | |
return project.sendFlagDecisions ?? false | |
} | |
var sendFlagDecisions: Bool { | |
return project.sendFlagDecisions ?? false | |
} |
} | ||
|
||
func getVariationForFeatureExperiment(config: ProjectConfig, | ||
featureFlag: FeatureFlag, | ||
userId: String, | ||
attributes: OptimizelyAttributes) -> (experiment: Experiment?, variation: Variation?)? { | ||
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove option from Variation.
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { | |
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation, source: String)? { |
@@ -174,7 +173,7 @@ class DefaultDecisionService: OPTDecisionService { | |||
func getVariationForFeatureRollout(config: ProjectConfig, | |||
featureFlag: FeatureFlag, | |||
userId: String, | |||
attributes: OptimizelyAttributes) -> Variation? { | |||
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove option from Variation.
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { | |
attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation, source: String)? { |
@@ -131,7 +131,7 @@ class DefaultDecisionService: OPTDecisionService { | |||
return result | |||
} | |||
|
|||
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment?, variation: Variation?)? { | |||
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove option from Variation.
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? { | |
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation, source: String)? { |
var variationKey = "" | ||
if let tmpVariation = variation { | ||
variationKey = tmpVariation.key | ||
variationId = tmpVariation.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we send a decision event when variation is nil?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes we do now.
@@ -44,6 +44,6 @@ protocol OPTDecisionService { | |||
- Parameter attributes: User attributes | |||
- Returns: The variation assigned to the specified user ID for a feature flag. | |||
*/ | |||
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment?, variation: Variation?)? | |||
func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, userId: String, attributes: OptimizelyAttributes) -> (experiment: Experiment, variation: Variation?, source: String)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above - can we remove option for variationKey?
@@ -372,13 +374,10 @@ open class OptimizelyClient: NSObject { | |||
return false | |||
} | |||
|
|||
let pair = decisionService.getVariationForFeature(config: config, | |||
guard let pair = decisionService.getVariationForFeature(config: config, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even when pair is nil (rollout decision fails even after the last rule), we still need to send a decision event. Check with @msohailhussain
featureFlag: featureFlag, | ||
userId: kUserId, | ||
attributes: kAttributesRolloutAge2Match) | ||
XCTAssertNil(variation) | ||
XCTAssertNil(pair) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add test cases for decision event sent for this nil pair cases?
let metaData = decision["metadata"] as! Dictionary<String, Any> | ||
XCTAssertEqual(metaData["rule_type"] as! String, "experiment") | ||
XCTAssertEqual(metaData["rule_key"] as! String, "ab_running_exp_audience_combo_exact_foo_or_true__and__42_or_4_2") | ||
XCTAssertEqual(metaData["flag_key"] as! String, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test pass? I understand "flag_key" set to same as "rule_key" for activate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor for non-optional variation look great!
A couple of changes requested as the spec was clarified in the meeting today.
- it looks like a dummy event still not sent out when a variation is decided to nil (we also need a test case for this)
- activate should send an event with a valid flag-key for feature-test.
@@ -385,8 +385,7 @@ open class OptimizelyClient: NSObject { | |||
featureEnabled: false) | |||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to send an impression event with a dummy metadata even when pair is null. These lines should be refactored to support it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to change the impression-event from activate call. When the experimentKey is for feature-test (not ab-test) we should send a valid flag-key (instead of copying experiment-key).
@@ -41,7 +41,7 @@ class DefaultDecisionService: OPTDecisionService { | |||
|
|||
// ---- check if the user is forced into a variation ---- | |||
if let variationId = config.getForcedVariation(experimentKey: experiment.key, userId: userId)?.id, | |||
let variation = experiment.getVariation(id: variationId) { | |||
let variation = experiment.getVariation(id: variationId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please fix indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
look good, but please fix indentation issues, empty spaces and i saw some other spacing issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm. @jaeopt please check indentation rules are changed in xcode 12.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of small changes suggested. Other than that, it looks good!
Also I keep getting Swiftlint error on this line ("line is too long"), wondering why you do not see it.
return lhs.version == rhs.version && lhs.projectId == rhs.projectId && lhs.experiments == rhs.experiments && |
@@ -147,6 +147,7 @@ class DefaultDecisionService: OPTDecisionService { | |||
} | |||
|
|||
return nil | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this empty line
@@ -259,7 +259,7 @@ open class OptimizelyClient: NSObject { | |||
variation: variation, | |||
userId: userId, | |||
attributes: attributes, | |||
flagKey: experimentKey, | |||
flagKey: "", | |||
ruleType: "experiment") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have this new ruleType "experiment"? If so, can we add it to DecisionSource enum?
|
||
let event = getFirstEventJSON()! | ||
let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0] | ||
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0] | ||
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0] | ||
|
||
let metaData = decision["metadata"] as! Dictionary<String, Any> | ||
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.rollout.rawValue) | ||
XCTAssertEqual(metaData["rule_key"] as! String, "") | ||
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1") | ||
XCTAssertEqual(metaData["variation_key"] as! String, "") | ||
|
||
self.optimizely.config!.project!.sendFlagDecisions = nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test looks good, but looks like in a wrong place, not related to Notification. Can we move this part to other like BathEventTests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - nit: fix indent
_ = fakeOptimizelyManager.isFeatureEnabled(featureKey: featureKey, userId: userId) | ||
|
||
let result = XCTWaiter.wait(for: [exp], timeout: 0.1) | ||
if result == XCTWaiter.Result.timedOut { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix indentation in all three tests
@@ -290,7 +290,7 @@ class OptimizelyClientTests_Others: XCTestCase { | |||
// set invalid (infinity) to attribute values, which will cause JSONEncoder.encode exception | |||
let attributes = ["testvar": Double.infinity] | |||
|
|||
optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, attributes: attributes) | |||
optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, attributes: attributes, flagKey: experiment.key, ruleType: Constants.DecisionSource.rollout.rawValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't assign experiment.key to flagKey even in tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good, one nit comment, I hope it ran through FSC tests.
suggested changes made, Have also verified it with fsc flag-decision tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Summary
Metadata
field to EventBatch.Decisions to capture flag type, key and variation key.Test summary