Skip to content

Commit 4ccb419

Browse files
authored
refact(audience-logs): Added and refactored audience evaluation logs. (#336)
1 parent bc78734 commit 4ccb419

File tree

13 files changed

+722
-116
lines changed

13 files changed

+722
-116
lines changed

Sources/Data Model/Audience/AttributeValue.swift

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -113,16 +113,17 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
113113

114114
extension AttributeValue {
115115

116-
func isExactMatch(with target: Any) throws -> Bool {
117-
guard let targetValue = AttributeValue(value: target) else {
118-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
116+
func isExactMatch(with target: Any, condition: String = "", name: String = "") throws -> Bool {
117+
118+
if !self.isValidForExactMatcher() || (self.doubleValue?.isInfinite ?? false) {
119+
throw OptimizelyError.evaluateAttributeInvalidCondition(condition)
119120
}
120121

121-
guard self.isComparable(with: targetValue) else {
122-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
122+
guard let targetValue = AttributeValue(value: target), self.isComparable(with: targetValue) else {
123+
throw OptimizelyError.evaluateAttributeInvalidType(condition, target, name)
123124
}
124125

125-
try checkValidAttributeNumber(target)
126+
try checkValidAttributeNumber(target, condition: condition, name: name)
126127

127128
// same type and same value
128129
if self == targetValue {
@@ -136,45 +137,47 @@ extension AttributeValue {
136137

137138
return false
138139
}
139-
140-
func isSubstring(of target: Any) throws -> Bool {
140+
141+
func isSubstring(of target: Any, condition: String = "", name: String = "") throws -> Bool {
142+
141143
guard case .string(let value) = self else {
142-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
144+
throw OptimizelyError.evaluateAttributeInvalidCondition(condition)
143145
}
144146

145147
guard let targetStr = target as? String else {
146-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
148+
throw OptimizelyError.evaluateAttributeInvalidType(condition, target, name)
147149
}
148150

149151
return targetStr.contains(value)
150152
}
151153

152-
func isGreater(than target: Any) throws -> Bool {
153-
guard let targetValue = AttributeValue(value: target) else {
154-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
154+
func isGreater(than target: Any, condition: String = "", name: String = "") throws -> Bool {
155+
156+
guard let currentDouble = self.doubleValue, currentDouble.isFinite else {
157+
throw OptimizelyError.evaluateAttributeInvalidCondition(condition)
155158
}
156159

157-
guard let currentDouble = self.doubleValue,
160+
guard let targetValue = AttributeValue(value: target),
158161
let targetDouble = targetValue.doubleValue else {
159-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
162+
throw OptimizelyError.evaluateAttributeInvalidType(condition, target, name)
160163
}
161164

162-
try checkValidAttributeNumber(target)
163-
165+
try checkValidAttributeNumber(target, condition: condition, name: name)
166+
164167
return currentDouble > targetDouble
165168
}
166169

167-
func isLess(than target: Any) throws -> Bool {
168-
guard let targetValue = AttributeValue(value: target) else {
169-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
170+
func isLess(than target: Any, condition: String = "", name: String = "") throws -> Bool {
171+
172+
guard let currentDouble = self.doubleValue, currentDouble.isFinite else {
173+
throw OptimizelyError.evaluateAttributeInvalidCondition(condition)
170174
}
171175

172-
guard let currentDouble = self.doubleValue,
176+
guard let targetValue = AttributeValue(value: target),
173177
let targetDouble = targetValue.doubleValue else {
174-
throw OptimizelyError.evaluateAttributeInvalidType(prettySrc(#function, target: target))
178+
throw OptimizelyError.evaluateAttributeInvalidType(condition, target, name)
175179
}
176-
177-
try checkValidAttributeNumber(target)
180+
try checkValidAttributeNumber(target, condition: condition, name: name)
178181

179182
return currentDouble < targetDouble
180183
}
@@ -214,7 +217,7 @@ extension AttributeValue {
214217
}
215218
}
216219

217-
func checkValidAttributeNumber(_ number: Any?, caller: String = #function) throws {
220+
func checkValidAttributeNumber(_ number: Any?, condition: String, name: String, caller: String = #function) throws {
218221
// check range for any value types (Int, Int64, Double, Float...)
219222
// do not check value range for string types
220223

@@ -231,7 +234,17 @@ extension AttributeValue {
231234

232235
// valid range: [-2^53, 2^53]
233236
if abs(num) > pow(2, 53) {
234-
throw OptimizelyError.evaluateAttributeValueOutOfRange(prettySrc(caller, target: number))
237+
throw OptimizelyError.evaluateAttributeValueOutOfRange(condition, name)
238+
}
239+
}
240+
241+
func isValidForExactMatcher() -> Bool {
242+
switch (self) {
243+
case (.string): return true
244+
case (.int): return true
245+
case (.double): return true
246+
case (.bool): return true
247+
default: return false
235248
}
236249
}
237250

Sources/Data Model/Audience/UserAttribute.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -24,7 +24,8 @@ struct UserAttribute: Codable, Equatable {
2424
var type: String?
2525
var match: String?
2626
var value: AttributeValue?
27-
27+
var stringRepresentation: String = ""
28+
2829
enum CodingKeys: String, CodingKey {
2930
case name
3031
case type
@@ -71,6 +72,7 @@ struct UserAttribute: Codable, Equatable {
7172
self.type = try container.decodeIfPresent(String.self, forKey: .type)
7273
self.match = try container.decodeIfPresent(String.self, forKey: .match)
7374
self.value = try container.decodeIfPresent(AttributeValue.self, forKey: .value)
75+
self.stringRepresentation = Utils.getConditionString(conditions: self)
7476
} catch {
7577
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Faild to decode User Attribute)"))
7678
}
@@ -81,6 +83,7 @@ struct UserAttribute: Codable, Equatable {
8183
self.type = type
8284
self.match = match
8385
self.value = value
86+
self.stringRepresentation = Utils.getConditionString(conditions: self)
8487
}
8588
}
8689

@@ -92,47 +95,51 @@ extension UserAttribute {
9295

9396
// invalid type - parsed for forward compatibility only (but evaluation fails)
9497
if typeSupported == nil {
95-
throw OptimizelyError.userAttributeInvalidType(self.type ?? "empty")
98+
throw OptimizelyError.userAttributeInvalidType(stringRepresentation)
9699
}
97100

98101
// invalid match - parsed for forward compatibility only (but evaluation fails)
99102
guard let matchFinal = matchSupported else {
100-
throw OptimizelyError.userAttributeInvalidMatch(self.match ?? "empty")
103+
throw OptimizelyError.userAttributeInvalidMatch(stringRepresentation)
101104
}
102105

103106
guard let nameFinal = name else {
104-
throw OptimizelyError.userAttributeInvalidFormat("empty name in condition")
107+
throw OptimizelyError.userAttributeInvalidName(stringRepresentation)
105108
}
106109

107110
let attributes = attributes ?? OptimizelyAttributes()
108111

109-
let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
110-
112+
let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
113+
111114
if matchFinal != .exists {
115+
if !attributes.keys.contains(nameFinal) {
116+
throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal)
117+
}
118+
112119
if value == nil {
113-
throw OptimizelyError.userAttributeInvalidFormat("missing value (\(nameFinal)) in condition)")
120+
throw OptimizelyError.userAttributeNilValue(stringRepresentation)
114121
}
115122

116123
if rawAttributeValue == nil {
117-
throw OptimizelyError.evaluateAttributeInvalidFormat("no attribute value for (\(nameFinal))")
124+
throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal)
118125
}
119126
}
120127

121128
switch matchFinal {
122129
case .exists:
123130
return !(rawAttributeValue is NSNull || rawAttributeValue == nil)
124131
case .exact:
125-
return try value!.isExactMatch(with: rawAttributeValue!)
132+
return try value!.isExactMatch(with: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
126133
case .substring:
127-
return try value!.isSubstring(of: rawAttributeValue!)
134+
return try value!.isSubstring(of: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
128135
case .lt:
129136
// user attribute "less than" this condition value
130137
// so evaluate if this condition value "isGreater" than the user attribute value
131-
return try value!.isGreater(than: rawAttributeValue!)
138+
return try value!.isGreater(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
132139
case .gt:
133140
// user attribute "greater than" this condition value
134141
// so evaluate if this condition value "isLess" than the user attribute value
135-
return try value!.isLess(than: rawAttributeValue!)
142+
return try value!.isLess(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
136143
}
137144
}
138145

Sources/Data Model/Project.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -42,6 +42,24 @@ struct Project: Codable, Equatable {
4242
var typedAudiences: [Audience]?
4343
var featureFlags: [FeatureFlag]
4444
var botFiltering: Bool?
45+
46+
let logger = OPTLoggerFactory.getLogger()
47+
48+
// Required since logger in not decodable
49+
enum CodingKeys: String, CodingKey {
50+
// V2
51+
case version, projectId, experiments, audiences, groups, attributes, accountId, events, revision
52+
// V3
53+
case anonymizeIP
54+
// V4
55+
case rollouts, typedAudiences, featureFlags, botFiltering
56+
}
57+
58+
// Required since logger in not equatable
59+
static func ==(lhs: Project, rhs: Project) -> Bool {
60+
return lhs.version == rhs.version && lhs.projectId == rhs.projectId && lhs.experiments == rhs.experiments &&
61+
lhs.audiences == rhs.audiences && lhs.groups == rhs.groups && lhs.attributes == rhs.attributes && lhs.accountId == rhs.accountId && lhs.events == rhs.events && lhs.revision == rhs.revision && lhs.anonymizeIP == rhs.anonymizeIP && lhs.rollouts == rhs.rollouts && lhs.typedAudiences == rhs.typedAudiences && lhs.featureFlags == rhs.featureFlags && lhs.botFiltering == rhs.botFiltering
62+
}
4563
}
4664

4765
extension Project: ProjectProtocol {
@@ -50,8 +68,13 @@ extension Project: ProjectProtocol {
5068
guard let audience = getAudience(id: audienceId) else {
5169
throw OptimizelyError.conditionNoMatchingAudience(audienceId)
5270
}
71+
logger.d { () -> String in
72+
return LogMessage.audienceEvaluationStarted(audienceId, Utils.getConditionString(conditions: audience.conditions)).description
73+
}
5374

54-
return try audience.evaluate(project: self, attributes: attributes)
75+
let result = try audience.evaluate(project: self, attributes: attributes)
76+
logger.d(.audienceEvaluationResult(audienceId, result.description))
77+
return result
5578
}
5679

5780
}

Sources/Extensions/Array+Extension.swift

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,8 @@ extension Array where Element == ThrowableCondition {
3030
}
3131

3232
for eval in self {
33-
do {
34-
if try eval() == false {
35-
return false
36-
}
37-
} catch let error as OptimizelyError {
38-
throw OptimizelyError.conditionInvalidFormat("AND with invalid items [\(error.reason)]")
33+
if try eval() == false {
34+
return false
3935
}
4036
}
4137

@@ -55,9 +51,8 @@ extension Array where Element == ThrowableCondition {
5551
}
5652

5753
if let error = foundError {
58-
throw OptimizelyError.conditionInvalidFormat("OR with invalid items [\(error.reason)]")
54+
throw error
5955
}
60-
6156
return false
6257
}
6358

@@ -67,13 +62,7 @@ extension Array where Element == ThrowableCondition {
6762
throw OptimizelyError.conditionInvalidFormat("NOT with empty items")
6863
}
6964

70-
var error: OptimizelyError!
71-
do {
72-
let result = try eval()
73-
return !result
74-
} catch let err as OptimizelyError {
75-
error = OptimizelyError.conditionInvalidFormat("NOT with invalid items [\(err.reason)]")
76-
}
77-
throw error
65+
let result = try eval()
66+
return !result
7867
}
7968
}

Sources/Implementation/DefaultDecisionService.swift

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,17 @@ class DefaultDecisionService: OPTDecisionService {
8080
return bucketedVariation
8181
}
8282

83-
func isInExperiment(config: ProjectConfig, experiment: Experiment, userId: String, attributes: OptimizelyAttributes) -> Bool {
83+
func isInExperiment(config: ProjectConfig, experiment: Experiment, userId: String, attributes: OptimizelyAttributes, logType: Constants.EvaluationLogType = .experiment, loggingKey: String? = nil) -> Bool {
8484

8585
var result = true // success as default (no condition, etc)
86+
let evType = logType.rawValue
87+
let finalLoggingKey = loggingKey ?? experiment.key
8688

8789
do {
8890
if let conditions = experiment.audienceConditions {
91+
logger.d { () -> String in
92+
return LogMessage.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: conditions)).description
93+
}
8994
switch conditions {
9095
case .array(let arrConditions):
9196
if arrConditions.count > 0 {
@@ -100,23 +105,25 @@ class DefaultDecisionService: OPTDecisionService {
100105
result = true
101106
}
102107
}
103-
// backward compatibility with audiencIds list
108+
// backward compatibility with audienceIds list
104109
else if experiment.audienceIds.count > 0 {
105110
var holder = [ConditionHolder]()
106111
holder.append(.logicalOp(.or))
107112
for id in experiment.audienceIds {
108113
holder.append(.leaf(.audienceId(id)))
109114
}
110-
115+
logger.d { () -> String in
116+
return LogMessage.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: holder)).description
117+
}
111118
result = try holder.evaluate(project: config.project, attributes: attributes)
112119
}
113120
} catch {
114-
logger.i(error as? OptimizelyError, source: "isInExperiment(experiment: \(experiment.key), userId: \(userId))")
121+
logger.i(error as? OptimizelyError)
115122
result = false
116123
}
117124

118-
logger.i(.audienceEvaluationResultCombined(experiment.key, result.description))
119-
125+
logger.i(.audienceEvaluationResultCombined(evType, finalLoggingKey, result.description))
126+
120127
return result
121128
}
122129

@@ -193,31 +200,28 @@ class DefaultDecisionService: OPTDecisionService {
193200

194201
// Evaluate all rollout rules except for last one
195202
for index in 0..<rolloutRules.count.advanced(by: -1) {
203+
let loggingKey = index + 1
196204
let experiment = rolloutRules[index]
197-
if isInExperiment(config: config, experiment: experiment, userId: userId, attributes: attributes) {
198-
logger.d(.userMeetsConditionsForTargetingRule(userId, index + 1))
199-
205+
if isInExperiment(config: config, experiment: experiment, userId: userId, attributes: attributes, logType: .rolloutRule, loggingKey: "\(loggingKey)") {
206+
logger.d(.userMeetsConditionsForTargetingRule(userId, loggingKey))
200207
if let variation = bucketer.bucketExperiment(config: config, experiment: experiment, bucketingId: bucketingId) {
201-
logger.d(.userBucketedIntoTargetingRule(userId, index + 1))
202-
208+
logger.d(.userBucketedIntoTargetingRule(userId, loggingKey))
203209
return variation
204210
}
205-
logger.d(.userNotBucketedIntoTargetingRule(userId, index + 1))
211+
logger.d(.userNotBucketedIntoTargetingRule(userId, loggingKey))
206212
break
207213
} else {
208-
logger.d(.userDoesntMeetConditionsForTargetingRule(userId, index + 1))
214+
logger.d(.userDoesntMeetConditionsForTargetingRule(userId, loggingKey))
209215
}
210216
}
211217
// Evaluate fall back rule / last rule now
212218
let experiment = rolloutRules[rolloutRules.count - 1]
213219

214-
if isInExperiment(config: config, experiment: experiment, userId: userId, attributes: attributes) {
220+
if isInExperiment(config: config, experiment: experiment, userId: userId, attributes: attributes, logType: .rolloutRule, loggingKey: "Everyone Else") {
215221
if let variation = bucketer.bucketExperiment(config: config, experiment: experiment, bucketingId: bucketingId) {
216222
logger.d(.userBucketedIntoEveryoneTargetingRule(userId))
217223

218224
return variation
219-
} else {
220-
logger.d(.userNotBucketedIntoEveryoneTargetingRule(userId))
221225
}
222226
}
223227

0 commit comments

Comments
 (0)