-
Notifications
You must be signed in to change notification settings - Fork 32
refact(audience-logs): Added and refactored audience evaluation logs. #336
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
Conversation
I have done my first pass, please address. |
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.
Sources/Utils/Utils.swift
Outdated
@@ -21,6 +21,8 @@ class Utils { | |||
// from auto-generated variable OPTIMIZELYSDKVERSION | |||
static var sdkVersion: String = OPTIMIZELYSDKVERSION | |||
|
|||
private static var jsonEncoder = JSONEncoder() |
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 am not comfortable to define here, while it's part of only one method.
@@ -80,12 +80,23 @@ class DefaultDecisionService: OPTDecisionService { | |||
return bucketedVariation | |||
} | |||
|
|||
func isInExperiment(config: ProjectConfig, experiment: Experiment, userId: String, attributes: OptimizelyAttributes) -> Bool { | |||
func isInExperiment(config: ProjectConfig, experiment: Experiment, userId: String, attributes: OptimizelyAttributes, isRule: Bool? = nil, loggingKey: String? = nil) -> 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.
isRule doesn't make sense? I think if you are copying from python, then it's an enum which indiciats either its evaluating for rolloutRule or 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.
Please revise.
let logger = OPTLoggerFactory.getLogger() | ||
|
||
// Required since logger in not decodable | ||
enum CodingKeys: String, CodingKey { |
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.
Where it's needed?
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.
Adding let logger = OPTLoggerFactory.getLogger()
caused the struct to throw error since logger does not confirm to Codable
and Equatable
protocols. The solution is to tell the struct explicitly which properties will confirm to Codable
and Equatable
.
@@ -231,7 +256,7 @@ extension AttributeValue { | |||
|
|||
// valid range: [-2^53, 2^53] | |||
if abs(num) > pow(2, 53) { | |||
throw OptimizelyError.evaluateAttributeValueOutOfRange(prettySrc(caller, target: number)) | |||
throw OptimizelyError.evaluateAttributeValueOutOfRange(condition, name) |
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 removed prettySrc
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.
Updated it as per python since it also doesn't log the source of the error.
@@ -214,7 +229,17 @@ extension AttributeValue { | |||
} | |||
} | |||
|
|||
func checkValidAttributeNumber(_ number: Any?, caller: String = #function) throws { | |||
func isValidForExactMatcher() -> 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.
Please move this method after checkvalidattributenumber
Sources/Utils/Utils.swift
Outdated
@@ -128,7 +126,7 @@ class Utils { | |||
} | |||
|
|||
static func getConditionString<T: Encodable>(conditions: T) -> String { | |||
if let jsonData = try? self.jsonEncoder.encode(conditions), let jsonString = String(data: jsonData, encoding: .utf8) { | |||
if let jsonData = try? JSONEncoder().encode(conditions), let jsonString = String(data: jsonData, encoding: .utf8) { |
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 is expensive to initialize JSONEncoder on everycall, I think you may keep static
inside the func. @jaeopt do you have any suggestion?
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.
declaring static property inside a method throws this error: Static properties may only be declared on a type
, solution to this is declaring private static let jsonEncoder = JSONEncoder()
inside the class like i had implemented earlier.
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.
declaring static property inside a method throws this error: Static properties may only be declared on a type
, Workaround to this is declaring the property inside the class, similar to how it was done in the first commit.
@@ -18,6 +18,11 @@ import Foundation | |||
|
|||
class DefaultDecisionService: OPTDecisionService { | |||
|
|||
enum evaluationLogType: 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.
Don't we have separate file for enums?
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 with a lot of details! A few errors should be fixed.
We also need more attention to avoid logging overhead for non-debug mode.
@@ -42,6 +42,23 @@ struct Project: Codable, Equatable { | |||
var typedAudiences: [Audience]? | |||
var featureFlags: [FeatureFlag] | |||
var botFiltering: Bool? | |||
let logger = OPTLoggerFactory.getLogger() |
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 you add a space above to make it clear from the other JSON fields?
@@ -18,6 +18,11 @@ import Foundation | |||
|
|||
class DefaultDecisionService: OPTDecisionService { | |||
|
|||
enum evaluationLogType: 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.
enum evaluationLogType: String { | |
enum EvaluationLogType: String { |
var finalLoggingKey = experiment.key | ||
if let key = loggingKey { | ||
finalLoggingKey = key | ||
} |
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.
var finalLoggingKey = experiment.key | |
if let key = loggingKey { | |
finalLoggingKey = key | |
} | |
let finalLoggingKey = loggingKey ?? experiment.key |
|
||
do { | ||
if let conditions = experiment.audienceConditions { | ||
logger.d(.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: conditions))) |
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.
logger.d(.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: conditions))) | |
logger.d { | |
return .evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: conditions))) | |
} |
"getConditionsString" will be expensive. Let's avoid this for normal flow (logLevel < .debug).
func d(_ message: () -> String) { |
@@ -100,23 +112,23 @@ class DefaultDecisionService: OPTDecisionService { | |||
result = true | |||
} | |||
} | |||
// backward compatibility with audiencIds list | |||
// backward compatibility with audiencIds list |
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.
nit: fix aligment
else if experiment.audienceIds.count > 0 { | ||
var holder = [ConditionHolder]() | ||
holder.append(.logicalOp(.or)) | ||
for id in experiment.audienceIds { | ||
holder.append(.leaf(.audienceId(id))) | ||
} | ||
|
||
logger.d(.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: holder))) |
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 to logger.d{} for all non-trivial messages.
@@ -90,49 +90,55 @@ extension UserAttribute { | |||
|
|||
func evaluate(attributes: OptimizelyAttributes?) throws -> Bool { | |||
|
|||
let conditionString = Utils.getConditionString(conditions: self) |
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 conditionString buidling is expensive. Let's remove this from normal flow (non-error).
We can build it in each error case.
case userAttributeNilValue(_ condition: String) | ||
case nilAttributeValue(_ condition: String, _ key: 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.
Do we need both of these? Looks like redundant. If so, let's use "userAttributeNilValue" to be consistent.
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.
both serve different purposes:
userAttributeNilValue
= Audience condition (\(condition)) evaluated to UNKNOWN because of null value.
nilAttributeValue
= Audience condition (\(condition)) evaluated to UNKNOWN because a null value was passed for user attribute (\(key)).
} | ||
} | ||
|
||
switch matchFinal { | ||
case .exists: | ||
return !(rawAttributeValue is NSNull || rawAttributeValue == nil) | ||
case .exact: | ||
return try value!.isExactMatch(with: rawAttributeValue!) | ||
return try value!.isExactMatch(with: rawAttributeValue!, condition: conditionString, name: nameFinal) |
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 case is tough. How can we avoid "conditionString" buiding overhead?
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.
Tried several ideas to evaluate conditionString
just once for each UserAttribute
. The problem i faced is that we are dealing with a struct which is a value type and to have a lazy or mutable property inside it, we would have to mark evaluate
method as mutating
.
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.
One possible fix is to create "conditionsString" when UserAttribute is instantiated by datafile parsing (just like we did for "FeatureVariable.swift" for JSON type conversion. Then we can avoid "Utils.getConditionString".
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 fixes suggested.
Can you add test cases for all decision error messages?
Those tests will be a good spec for decision-error messages.
case .nilAttributeValue(let condition, let key): message = "Audience condition (\(condition)) evaluated to UNKNOWN because a null value was passed for user attribute (\(key))." | ||
case .userAttributeInvalidName(let condition): message = "Audience condition (\(condition)) evaluated to UNKNOWN because of invalid attribute name." |
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 see we need both. Can we change the order of these two lines so "userAttribute" and "attribute" errors can be grouped?
} | ||
} | ||
|
||
switch matchFinal { | ||
case .exists: | ||
return !(rawAttributeValue is NSNull || rawAttributeValue == nil) | ||
case .exact: | ||
return try value!.isExactMatch(with: rawAttributeValue!) | ||
return try value!.isExactMatch(with: rawAttributeValue!, condition: conditionString, name: nameFinal) |
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.
One possible fix is to create "conditionsString" when UserAttribute is instantiated by datafile parsing (just like we did for "FeatureVariable.swift" for JSON type conversion. Then we can avoid "Utils.getConditionString".
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-logging?
Other changes look good. A couple of small changes suggested.
@@ -24,6 +24,7 @@ struct UserAttribute: Codable, Equatable { | |||
var type: String? | |||
var match: String? | |||
var value: AttributeValue? | |||
var stringRepresentation: 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.
nit: add a space to differentiate from JSON fields
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.
var stringRepresentation: String | |
var stringRepresentation: String = "" |
// initializing with empty value before using self to avoid constructor warnings | ||
self.stringRepresentation = "" |
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.
Let's remove these by setting default value above
// initializing with empty value before using self to avoid constructor warnings | ||
self.stringRepresentation = "" |
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 these too
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 need test cases that validate the expected log messages printed out (instead of validating error types). Can you fix the test cases. See my comments for details.
} catch { | ||
tmpError = error | ||
} | ||
XCTAssertTrue(tmpError != nil) | ||
XCTAssertEqual("[Optimizely][Error] " + tmpError!.localizedDescription, OptimizelyError.evaluateAttributeInvalidType(conditionString, invalidValue, attributeKey).localizedDescription) |
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.
These tests validate expected error "type"s, but not testing if the log messages are actually printed out.
We throw errors. At some point, SDK is expected to catch it and prints out the log message.
We need validate that the expected error messages are actually logged.
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.
Hi @jaeopt , I have added new tests in decision service to verify if thrown errors are logged properly using MockLogger
. i have also kept the newly added tests for UserAttribute
and AttributeValue
to test error handling but have removed assertion of log messages from them.
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.
Error logging validation tests look great.
I suggest some changes for error-type checking.
HandlerRegistryService.shared.binders.property?.removeAll() | ||
let binder: Binder = Binder<OPTLogger>(service: OPTLogger.self).to { () -> OPTLogger? in | ||
return self.mockLogger | ||
} | ||
HandlerRegistryService.shared.registerBinding(binder: binder) |
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 can remove these lines.
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.
Jae, DataStore binder sets its own logger which is causing problem here. All logs go there that's why reset it here.
var tmpError: Error? | ||
do { | ||
_ = try attr.isExactMatch(with: invalidValue, condition: conditionString, name: attributeKey) | ||
_ = try attr.isExactMatch(with: invalidValue) | ||
} catch { | ||
tmpError = error | ||
} | ||
XCTAssertEqual("[Optimizely][Error] " + tmpError!.localizedDescription, OptimizelyError.evaluateAttributeInvalidCondition(conditionString).localizedDescription) | ||
XCTAssertNotNil(tmpError) |
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 change these non-nil checking to specific error type checking?
var tmpError: OptimizelyError? ... } catch { tmpError = error as? OptimizelyError } ... if case . evaluateAttributeInvalidCondition = tmpError { XCTAssert(true) } else { XCTAssert(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.
I removed specific error type checking here since we are doing the same in the new decisionService 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.
I understand they're redundant. but it's better to have type checking here. Without them, these tests look incomplete.
Recovering your old XCTAssertEqual will also work.
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 now all looks good!
Thanks for taking care of all the detailed fixes
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
Issues
https://optimizely.atlassian.net/browse/OASIS-6545