Skip to content
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

Merged
merged 15 commits into from
Oct 20, 2020

Conversation

yasirfolio3
Copy link
Contributor

Summary

  • Send events for ALL decision types.
  • Add Metadata field to EventBatch.Decisions to capture flag type, key and variation key.

Test summary

  • Added unit tests.

@coveralls
Copy link

coveralls commented Oct 1, 2020

Coverage Status

Coverage increased (+0.04%) to 98.831% when pulling 397a51d on yasir/flag-decisions into b1d0962 on master.

@@ -42,6 +42,7 @@ struct Project: Codable, Equatable {
var typedAudiences: [Audience]?
var featureFlags: [FeatureFlag]
var botFiltering: Bool?
var sendFlagDecisions: Bool?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not nullable.

Copy link
Contributor Author

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.

Copy link
Contributor

@msohailhussain msohailhussain left a 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.
Copy link
Contributor

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)
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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,
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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!

@msohailhussain msohailhussain marked this pull request as ready for review October 5, 2020 22:03
@msohailhussain msohailhussain requested a review from a team as a code owner October 5, 2020 22:03
@msohailhussain msohailhussain changed the title DO NOT REVIEW: feat(flag-decisions): Add support for sending flag decisions along with decision metadata. feat(flag-decisions): Add support for sending flag decisions along with decision metadata. Oct 5, 2020
Copy link

@pawels-optimizely pawels-optimizely left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Collaborator

@jaeopt jaeopt left a 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.

@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Required since logger in not equatable
// Required since logger is not equatable

Comment on lines 183 to 185
func sendFlagDecisions() -> Bool {
return project.sendFlagDecisions ?? false
}
Copy link
Collaborator

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.

Suggested change
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)? {
Copy link
Collaborator

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.

Suggested change
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)? {
Copy link
Collaborator

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.

Suggested change
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)? {
Copy link
Collaborator

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.

Suggested change
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
Copy link
Collaborator

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?

Copy link
Contributor Author

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)?
Copy link
Collaborator

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,
Copy link
Collaborator

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)
Copy link
Collaborator

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, "")
Copy link
Collaborator

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.

Copy link
Collaborator

@jaeopt jaeopt left a 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
Copy link
Collaborator

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.

Copy link
Collaborator

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).

@yasirfolio3 yasirfolio3 removed their assignment Oct 13, 2020
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix indentation.

Copy link
Contributor

@msohailhussain msohailhussain left a 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.

msohailhussain
msohailhussain previously approved these changes Oct 13, 2020
Copy link
Contributor

@msohailhussain msohailhussain left a 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.

Copy link
Collaborator

@jaeopt jaeopt left a 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

Copy link
Collaborator

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")
Copy link
Collaborator

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?

Comment on lines 783 to 795

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
Copy link
Collaborator

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?

jaeopt
jaeopt previously approved these changes Oct 14, 2020
Copy link
Collaborator

@jaeopt jaeopt left a 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 {
Copy link
Collaborator

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)

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

Copy link

@pawels-optimizely pawels-optimizely left a 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.

@yasirfolio3
Copy link
Contributor Author

looks good, one nit comment, I hope it ran through FSC tests.

suggested changes made, Have also verified it with fsc flag-decision tests.

Copy link
Contributor

@msohailhussain msohailhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link

@pawels-optimizely pawels-optimizely left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Collaborator

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@pawels-optimizely pawels-optimizely merged commit 3190808 into master Oct 20, 2020
@pawels-optimizely pawels-optimizely deleted the yasir/flag-decisions branch October 20, 2020 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants