Skip to content

Commit 2fcbc11

Browse files
authored
feat: add ODP integration (#455)
This PR adds a support for Optimizely Data Platform (ODP) integration to Full Stack. With this extension, clients may not need to pre-determine and include user segments in attributes. SDK can fetch user segments from the ODP server for the current user. - Add a new public API to OptimizelyUserContext (fetchQualifiedSegments). - Add a ODP server interface (segments and events). - Add a new audience type (qualified). - Add VuidManager and support vuid-based decisions before userId is available.
1 parent 3db2e1b commit 2fcbc11

File tree

70 files changed

+5860
-571
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+5860
-571
lines changed

DemoSwiftApp/AppDelegate.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
130130
@unknown default:
131131
print("Optimizely SDK initiliazation failed with unknown result")
132132
}
133+
133134
self.startWithRootViewController()
134135

135136
// For sample codes for APIs, see "Samples/SamplesForAPI.swift"
136137
//SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely)
137138
//SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely)
139+
//SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely)
138140
}
139141
}
140142

DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@
555555
isa = PBXProject;
556556
attributes = {
557557
LastSwiftUpdateCheck = 1230;
558-
LastUpgradeCheck = 1250;
558+
LastUpgradeCheck = 1320;
559559
ORGANIZATIONNAME = Optimizely;
560560
TargetAttributes = {
561561
252D7DEC21C8800800134A7A = {

DemoSwiftApp/Samples/SamplesForAPI.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import Foundation
1818
import Optimizely
19+
import UIKit
1920

2021
class SamplesForAPI {
2122

@@ -146,17 +147,17 @@ class SamplesForAPI {
146147

147148
// (1) set a forced decision for a flag
148149

149-
var success = user.setForcedDecision(context: context1, decision: forced1)
150+
_ = user.setForcedDecision(context: context1, decision: forced1)
150151
decision = user.decide(key: "flag-1")
151152

152153
// (2) set a forced decision for an ab-test rule
153154

154-
success = user.setForcedDecision(context: context2, decision: forced2)
155+
_ = user.setForcedDecision(context: context2, decision: forced2)
155156
decision = user.decide(key: "flag-1")
156157

157158
// (3) set a forced variation for a delivery rule
158159

159-
success = user.setForcedDecision(context: context3,
160+
_ = user.setForcedDecision(context: context3,
160161
decision: forced3)
161162
decision = user.decide(key: "flag-1")
162163

@@ -167,8 +168,8 @@ class SamplesForAPI {
167168

168169
// (5) remove forced variations
169170

170-
success = user.removeForcedDecision(context: context2)
171-
success = user.removeAllForcedDecisions()
171+
_ = user.removeForcedDecision(context: context2)
172+
_ = user.removeAllForcedDecisions()
172173
}
173174

174175
// MARK: - OptimizelyConfig
@@ -260,6 +261,32 @@ class SamplesForAPI {
260261

261262
}
262263

264+
// MARK: - AudienceSegments
265+
266+
static func checkAudienceSegments(optimizely: OptimizelyClient) {
267+
// override the default handler if cache size and timeout need to be customized
268+
let optimizely = OptimizelyClient(sdkKey: "VivZyCGPHY369D4z8T9yG", // odp-test
269+
periodicDownloadInterval: 60,
270+
defaultLogLevel: .debug)
271+
optimizely.start { result in
272+
if case .failure(let error) = result {
273+
print("[AudienceSegments] SDK initialization failed: \(error)")
274+
return
275+
}
276+
277+
let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"])
278+
user.fetchQualifiedSegments(options: [.ignoreCache]) { _, error in
279+
guard error == nil else {
280+
print("[AudienceSegments] \(error!.errorDescription!)")
281+
return
282+
}
283+
284+
let decision = user.decide(key: "show_coupon", options: [.includeReasons])
285+
print("[AudienceSegments] decision: \(decision)")
286+
}
287+
}
288+
}
289+
263290
// MARK: - Initializations
264291

265292
static func samplesForInitialization() {

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 585 additions & 35 deletions
Large diffs are not rendered by default.

OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<dict>
55
<key>FILEHEADER</key>
66
<string>
7-
// Copyright 2021, Optimizely, Inc. and contributors
7+
// Copyright 2022, Optimizely, Inc. and contributors
88
//
99
// Licensed under the Apache License, Version 2.0 (the "License");
1010
// you may not use this file except in compliance with the License.

Sources/Data Model/Audience/Audience.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,36 @@ struct Audience: Codable, Equatable, OptimizelyAudience {
7070
try container.encode(conditionHolder, forKey: .conditions)
7171
}
7272

73-
func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
74-
return try conditionHolder.evaluate(project: project, attributes: attributes)
73+
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
74+
return try conditionHolder.evaluate(project: project, user: user)
75+
}
76+
77+
/// Extract all audience segments used in this audience conditions.
78+
/// - Returns: a String array of segment names.
79+
func getSegments() -> [String] {
80+
let segments = getSegments(condition: conditionHolder)
81+
return Array(Set(segments))
82+
}
83+
84+
func getSegments(condition: ConditionHolder) -> [String] {
85+
var segments = [String]()
86+
87+
switch condition {
88+
case .logicalOp:
89+
return []
90+
case .leaf(let leaf):
91+
if case .attribute(let userAttribute) = leaf {
92+
if userAttribute.matchSupported == .qualified, let strValue = userAttribute.value?.stringValue {
93+
segments.append(strValue)
94+
}
95+
}
96+
case .array(let conditions):
97+
conditions.forEach {
98+
segments.append(contentsOf: getSegments(condition: $0))
99+
}
100+
}
101+
102+
return segments
75103
}
76104

77105
}

Sources/Data Model/Audience/ConditionHolder.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ enum ConditionHolder: Codable, Equatable {
6161
}
6262
}
6363

64-
func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
64+
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
6565
switch self {
6666
case .logicalOp:
6767
throw OptimizelyError.conditionInvalidFormat("Logical operation not evaluated")
6868
case .leaf(let conditionLeaf):
69-
return try conditionLeaf.evaluate(project: project, attributes: attributes)
69+
return try conditionLeaf.evaluate(project: project, user: user)
7070
case .array(let conditions):
71-
return try conditions.evaluate(project: project, attributes: attributes)
71+
return try conditions.evaluate(project: project, user: user)
7272
}
7373
}
7474

@@ -111,24 +111,24 @@ extension ConditionHolder {
111111

112112
extension Array where Element == ConditionHolder {
113113

114-
func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
114+
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
115115
guard let firstItem = self.first else {
116116
throw OptimizelyError.conditionInvalidFormat("Empty condition array")
117117
}
118118

119119
switch firstItem {
120120
case .logicalOp(let op):
121-
return try evaluate(op: op, project: project, attributes: attributes)
121+
return try evaluate(op: op, project: project, user: user)
122122
case .leaf:
123123
// special case - no logical operator
124124
// implicit or
125-
return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, attributes: attributes)
125+
return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, user: user)
126126
default:
127127
throw OptimizelyError.conditionInvalidFormat("Invalid first item")
128128
}
129129
}
130130

131-
func evaluate(op: LogicalOp, project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
131+
func evaluate(op: LogicalOp, project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
132132
guard self.count > 0 else {
133133
throw OptimizelyError.conditionInvalidFormat("Empty condition array")
134134
}
@@ -138,7 +138,7 @@ extension Array where Element == ConditionHolder {
138138
// create closure array for delayed evaluations to avoid unnecessary ops
139139
let evalList = itemsAfterOpTrimmed.map { holder -> ThrowableCondition in
140140
return {
141-
return try holder.evaluate(project: project, attributes: attributes)
141+
return try holder.evaluate(project: project, user: user)
142142
}
143143
}
144144

Sources/Data Model/Audience/ConditionLeaf.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ enum ConditionLeaf: Codable, Equatable {
4545
}
4646
}
4747

48-
func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
48+
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
4949
switch self {
5050
case .audienceId(let id):
5151
guard let project = project else {
5252
throw OptimizelyError.conditionCannotBeEvaluated("audienceId: \(id)")
5353
}
5454

55-
return try project.evaluateAudience(audienceId: id, attributes: attributes)
55+
return try project.evaluateAudience(audienceId: id, user: user)
5656
case .attribute(let userAttribute):
57-
return try userAttribute.evaluate(attributes: attributes)
57+
return try userAttribute.evaluate(user: user)
5858
}
5959
}
6060

Sources/Data Model/Audience/UserAttribute.swift

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct UserAttribute: Codable, Equatable {
3737

3838
enum ConditionType: String, Codable {
3939
case customAttribute = "custom_attribute"
40+
case thirdPartyDimension = "third_party_dimension"
4041
}
4142

4243
enum ConditionMatch: String, Codable {
@@ -52,6 +53,7 @@ struct UserAttribute: Codable, Equatable {
5253
case semver_le
5354
case semver_gt
5455
case semver_ge
56+
case qualified
5557
}
5658

5759
var typeSupported: ConditionType? {
@@ -98,7 +100,7 @@ struct UserAttribute: Codable, Equatable {
98100

99101
extension UserAttribute {
100102

101-
func evaluate(attributes: OptimizelyAttributes?) throws -> Bool {
103+
func evaluate(user: OptimizelyUserContext) throws -> Bool {
102104

103105
// invalid type - parsed for forward compatibility only (but evaluation fails)
104106
if typeSupported == nil {
@@ -114,63 +116,77 @@ extension UserAttribute {
114116
throw OptimizelyError.userAttributeInvalidName(stringRepresentation)
115117
}
116118

117-
let attributes = attributes ?? OptimizelyAttributes()
118-
119-
let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
119+
let attributes = user.attributes
120+
let rawValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
120121

121-
if matchFinal != .exists {
122-
if !attributes.keys.contains(nameFinal) {
123-
throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal)
124-
}
122+
if matchFinal == .exists {
123+
return !(rawValue is NSNull || rawValue == nil)
124+
}
125+
126+
// all other matches requires valid value
125127

126-
if value == nil {
127-
throw OptimizelyError.userAttributeNilValue(stringRepresentation)
128-
}
128+
guard let value = value else {
129+
throw OptimizelyError.userAttributeNilValue(stringRepresentation)
130+
}
129131

130-
if rawAttributeValue == nil {
131-
throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal)
132+
if matchFinal == .qualified {
133+
// NOTE: name ("odp.audiences") and type("third_party_dimension") not used
134+
135+
guard case .string(let strValue) = value else {
136+
throw OptimizelyError.evaluateAttributeInvalidCondition(stringRepresentation)
132137
}
138+
return user.isQualifiedFor(segment: strValue)
139+
}
140+
141+
// all other matches requires attribute value
142+
143+
guard attributes.keys.contains(nameFinal) else {
144+
throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal)
133145
}
134146

147+
guard let rawAttributeValue = rawValue else {
148+
throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal)
149+
}
150+
135151
switch matchFinal {
136-
case .exists:
137-
return !(rawAttributeValue is NSNull || rawAttributeValue == nil)
138152
case .exact:
139-
return try value!.isExactMatch(with: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
153+
return try value.isExactMatch(with: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
140154
case .substring:
141-
return try value!.isSubstring(of: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
155+
return try value.isSubstring(of: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
142156
case .lt:
143157
// user attribute "less than" this condition value
144158
// so evaluate if this condition value "isGreater" than the user attribute value
145-
return try value!.isGreater(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
159+
return try value.isGreater(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
146160
case .le:
147161
// user attribute "less than" or equal this condition value
148162
// so evaluate if this condition value "isGreater" than or equal the user attribute value
149-
return try value!.isGreaterOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
163+
return try value.isGreaterOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
150164
case .gt:
151165
// user attribute "greater than" this condition value
152166
// so evaluate if this condition value "isLess" than the user attribute value
153-
return try value!.isLess(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
167+
return try value.isLess(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
154168
case .ge:
155169
// user attribute "greater than or equal" this condition value
156170
// so evaluate if this condition value "isLess" than or equal the user attribute value
157-
return try value!.isLessOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
171+
return try value.isLessOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
158172
// semantic versioning seems unique. the comarison is to compare verion but the passed in version is the target version.
159173
case .semver_eq:
160174
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
161-
return try targetValue.isSemanticVersionEqual(than: value!.stringValue)
175+
return try targetValue.isSemanticVersionEqual(than: value.stringValue)
162176
case .semver_lt:
163177
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
164-
return try targetValue.isSemanticVersionLess(than: value!.stringValue)
178+
return try targetValue.isSemanticVersionLess(than: value.stringValue)
165179
case .semver_le:
166180
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
167-
return try targetValue.isSemanticVersionLessOrEqual(than: value!.stringValue)
181+
return try targetValue.isSemanticVersionLessOrEqual(than: value.stringValue)
168182
case .semver_gt:
169183
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
170-
return try targetValue.isSemanticVersionGreater(than: value!.stringValue)
184+
return try targetValue.isSemanticVersionGreater(than: value.stringValue)
171185
case .semver_ge:
172186
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
173-
return try targetValue.isSemanticVersionGreaterOrEqual(than: value!.stringValue)
187+
return try targetValue.isSemanticVersionGreaterOrEqual(than: value.stringValue)
188+
default:
189+
throw OptimizelyError.userAttributeInvalidMatch(stringRepresentation)
174190
}
175191
}
176192

Sources/Data Model/Integration.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright 2022, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
19+
struct Integration: Codable, Equatable {
20+
var key: String
21+
var host: String?
22+
var publicKey: String?
23+
}

0 commit comments

Comments
 (0)