Skip to content

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

Merged
merged 16 commits into from
Jul 8, 2020

Conversation

yasirfolio3
Copy link
Contributor

Summary

  • Added and updated missing audience evaluation logs.

Issues

https://optimizely.atlassian.net/browse/OASIS-6545

@coveralls
Copy link

coveralls commented Jun 29, 2020

Coverage Status

Coverage decreased (-0.003%) to 98.797% when pulling ed1315c on yasir/audit-audience-evaluation into bc78734 on master.

@msohailhussain msohailhussain requested a review from jaeopt June 30, 2020 19:06
@msohailhussain msohailhussain changed the title refact(audience-logs): Added and refactored audience evaluation logs. (WIP) refact(audience-logs): Added and refactored audience evaluation logs. Jun 30, 2020
@msohailhussain msohailhussain removed their assignment Jun 30, 2020
@msohailhussain
Copy link
Contributor

I have done my first pass, please address.

@msohailhussain msohailhussain marked this pull request as ready for review June 30, 2020 19:07
@msohailhussain msohailhussain requested a review from a team as a code owner June 30, 2020 19:07
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.

@@ -21,6 +21,8 @@ class Utils {
// from auto-generated variable OPTIMIZELYSDKVERSION
static var sdkVersion: String = OPTIMIZELYSDKVERSION

private static var jsonEncoder = JSONEncoder()
Copy link
Contributor

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

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.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Where it's needed?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Why removed prettySrc

Copy link
Contributor Author

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

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

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

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor

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

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

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

Choose a reason for hiding this comment

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

Suggested change
enum evaluationLogType: String {
enum EvaluationLogType: String {

Comment on lines 93 to 96
var finalLoggingKey = experiment.key
if let key = loggingKey {
finalLoggingKey = key
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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

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

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

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.

Comment on lines 57 to 58
case userAttributeNilValue(_ condition: String)
case nilAttributeValue(_ condition: String, _ key: String)
Copy link
Contributor

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

@yasirfolio3 yasirfolio3 Jul 2, 2020

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.

Copy link
Contributor

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

@yasirfolio3 yasirfolio3 removed their assignment Jul 2, 2020
Copy link
Contributor

@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 fixes suggested.
Can you add test cases for all decision error messages?
Those tests will be a good spec for decision-error messages.

Comment on lines 132 to 133
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."
Copy link
Contributor

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

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

Copy link
Contributor

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

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var stringRepresentation: String
var stringRepresentation: String = ""

Comment on lines 75 to 76
// initializing with empty value before using self to avoid constructor warnings
self.stringRepresentation = ""
Copy link
Contributor

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

Comment on lines 88 to 89
// initializing with empty value before using self to avoid constructor warnings
self.stringRepresentation = ""
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove these too

Copy link
Contributor

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

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

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.

Copy link
Contributor Author

@yasirfolio3 yasirfolio3 Jul 8, 2020

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.

@yasirfolio3 yasirfolio3 removed their assignment Jul 8, 2020
Copy link
Contributor

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

Error logging validation tests look great.
I suggest some changes for error-type checking.

Comment on lines +214 to +218
HandlerRegistryService.shared.binders.property?.removeAll()
let binder: Binder = Binder<OPTLogger>(service: OPTLogger.self).to { () -> OPTLogger? in
return self.mockLogger
}
HandlerRegistryService.shared.registerBinding(binder: binder)
Copy link
Contributor

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.

Copy link
Contributor

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.

Comment on lines 347 to 353
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)
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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

It now all looks good!
Thanks for taking care of all the detailed fixes

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 jaeopt merged commit 4ccb419 into master Jul 8, 2020
@jaeopt jaeopt deleted the yasir/audit-audience-evaluation branch July 8, 2020 21:20
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.

5 participants