Skip to content

Commit

Permalink
[FSSDK-11036] fix: event tags support nested objects (#570)
Browse files Browse the repository at this point in the history
* fix: nested event tag support added
* fix: update simulator matrix
  • Loading branch information
muzahidul-opti authored Feb 4, 2025
1 parent b3651f6 commit a5cdf42
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 39 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

integration_tests:
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@master
uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@muzahid/fix-nested-event-tag
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
Expand All @@ -47,7 +47,7 @@ jobs:
unittests:
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@muzahid/fix-nested-event-tag

prepare_for_release:
runs-on: macos-13
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,27 @@ jobs:
# - see "https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md" for installed macOS, xcode and simulator versions.
include:
- os: 16.1
device: "iPhone 12"
device: "iPhone 14"
scheme: "OptimizelySwiftSDK-iOS"
test_sdk: "iphonesimulator"
platform: "iOS Simulator"
os_type: "iOS"
simulator_xcode_version: 14.1
- os: 15.5
device: "iPhone 12"
- os: 16.2
device: "iPhone 14"
scheme: "OptimizelySwiftSDK-iOS"
test_sdk: "iphonesimulator"
platform: "iOS Simulator"
os_type: "iOS"
simulator_xcode_version: 13.4.1
- os: 15.5
simulator_xcode_version: 14.2
- os: 16.4
# good to have tests with older OS versions, but it looks like this is min OS+xcode versions supported by github actions
device: "iPad Air (4th generation)"
device: "iPad Air (5th generation)"
scheme: "OptimizelySwiftSDK-iOS"
test_sdk: "iphonesimulator"
platform: "iOS Simulator"
os_type: "iOS"
simulator_xcode_version: 13.4.1
simulator_xcode_version: 14.3.1
- os: 16.1
device: "Apple TV"
scheme: "OptimizelySwiftSDK-tvOS"
Expand Down Expand Up @@ -85,7 +85,7 @@ jobs:
# - to find pre-installed xcode version, run this:
##ls /Applications/
# - to find supported simulator os versions, run this (and find simulator with non-error "datapath")
##xcrun simctl list --json devices
xcrun simctl list --json devices
# switch to the target xcode version
sudo xcode-select -switch /Applications/Xcode_$SIMULATOR_XCODE_VERSION.app
Expand Down
55 changes: 53 additions & 2 deletions Sources/Data Model/Audience/AttributeValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
import Foundation

enum AttributeValue: Codable, Equatable, CustomStringConvertible {
typealias AttrArray = Array<AttributeValue>
typealias AttrDictionary = [String : AttributeValue]

case string(String)
case int(Int64) // supported value range [-2^53, 2^53]
case double(Double)
case bool(Bool)
// not defined in datafile schema, but required for forward compatiblity (see Nikhil's doc)
case array(AttrArray)
case dictionary(AttrDictionary)
case others

var description: String {
Expand All @@ -34,6 +38,10 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
return "int(\(value))"
case .bool(let value):
return "bool(\(value))"
case .array(let value):
return "array(\(value))"
case .dictionary(let value):
return "dictionary(\(value))"
case .others:
return "others"
}
Expand Down Expand Up @@ -63,6 +71,18 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
self = .bool(boolValue)
return
}

if let arrValue = value as? [Any] {
let attr = arrValue.compactMap { AttributeValue(value: $0) }
self = .array(attr)
return
}

if let dicValue = value as? [String : Any] {
let attr = dicValue.compactMapValues { AttributeValue(value: $0) }
self = .dictionary(attr)
return
}

return nil
}
Expand All @@ -87,7 +107,18 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
return
}

// accept all other types (null, {}, []) for forward compatibility support
if let value = try? container.decode(AttrArray.self) {
self = .array(value)
return
}

if let value = try? container.decode(AttrDictionary.self) {
self = .dictionary(value)
return
}


// accept all other types (null) for forward compatibility support
self = .others
}

Expand All @@ -103,6 +134,10 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
try container.encode(value)
case .bool(let value):
try container.encode(value)
case .array(let value):
try container.encode(value)
case .dictionary(let value):
try container.encode(value.mapValues { $0 })
case .others:
return
}
Expand Down Expand Up @@ -135,6 +170,14 @@ extension AttributeValue {
return true
}

if case .array(let selfArr) = self, case .array(let targetArr) = targetValue {
return selfArr == targetArr
}

if case .dictionary(let selfDict) = self, case .dictionary(let targetDict) = targetValue {
return selfDict == targetDict
}

return false
}

Expand Down Expand Up @@ -227,6 +270,10 @@ extension AttributeValue {
return String(value)
case .bool(let value):
return String(value)
case .array(let value):
return String(describing: value)
case .dictionary(let value):
return String(describing: value)
case .others:
return "UNKNOWN"
}
Expand All @@ -240,6 +287,8 @@ extension AttributeValue {
case (.double, .int): return true
case (.double, .double): return true
case (.bool, .bool): return true
case (.array, .array): return true
case (.dictionary, .dictionary): return true
default: return false
}
}
Expand Down Expand Up @@ -271,6 +320,8 @@ extension AttributeValue {
case (.int): return true
case (.double): return true
case (.bool): return true
case (.array): return true
case (.dictionary): return true
default: return false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class BatchEventBuilderTests_EventTags: XCTestCase {

extension BatchEventBuilderTests_EventTags {

func testEventTagsWhenInvalidType() {
func testEventTagsWhenArrayType() {
let eventKey = "event_single_targeted_exp"
let eventTags: [String: Any] = ["browser": "chrome",
"future": [1, 2, 3]]
Expand All @@ -87,7 +87,8 @@ extension BatchEventBuilderTests_EventTags {
let tags = de["tags"] as! [String: Any]

XCTAssertEqual(tags["browser"] as! String, "chrome")
XCTAssertNil(tags["future"])
XCTAssertNotNil(tags["future"])
XCTAssertEqual(tags["future"] as? [Int], [1, 2, 3])
}

func testEventTagsWhenTooBigNumbers() {
Expand Down Expand Up @@ -316,6 +317,55 @@ extension BatchEventBuilderTests_EventTags {
XCTAssertEqual(de["value"] as! Double, 32, "value must be valid for value")
}


func testNestedTag() {
let properties: [String: Any] = [
"category": "shoes",
"Text": "value",
"nested": [
"foot": "value",
"mouth": "mouth_value"
],
"stringArray": ["a", "b", "c"],
"intArray": [1, 2, 3],
"doubleArray": [1.0, 2.0, 3.0],
"boolAray": [false, true, false, true],
]
let eventKey = "event_single_targeted_exp"
let eventTags: [String: Any] = ["browser": "chrome",
"v1": Int8(10),
"v2": Int16(20),
"v3": Int32(30),
"revenue": Int64(40),
"value": Float(32),
"$opt_event_properties": properties]

try! optimizely.track(eventKey: eventKey, userId: userId, attributes: nil, eventTags: eventTags)

let de = getDispatchEvent(dispatcher: eventDispatcher)!
let tags = de["tags"] as! [String: Any]

XCTAssertEqual(tags["browser"] as! String, "chrome")
XCTAssertEqual(tags["v1"] as! Int, 10)
XCTAssertEqual(tags["v2"] as! Int, 20)
XCTAssertEqual(tags["v3"] as! Int, 30)
XCTAssertEqual(tags["revenue"] as! Int, 40)
XCTAssertEqual(tags["value"] as! Double, 32)
XCTAssertEqual(de["revenue"] as! Int, 40, "value must be valid for revenue")
XCTAssertEqual(de["value"] as! Double, 32, "value must be valid for value")

XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["category"] as! String, "shoes")
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["nested"] as! [String : String], ["foot": "value", "mouth": "mouth_value"])

XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["stringArray"] as! [String], ["a", "b", "c"])
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["intArray"] as! [Int], [1, 2, 3])
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["doubleArray"] as! [Double], [1, 2, 3])
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["boolAray"] as! [Bool], [false, true, false, true])


}


func testEventTagsWithRevenueAndValue_toJSON() {

// valid revenue/value types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,17 @@ class DecisionServiceTests_Experiments: XCTestCase {
],
[
"id": kAudienceIdExactInvalidValue,
"conditions": [ "type": "custom_attribute", "name": "age", "match": "exact", "value": ["invalid"] ],
"conditions": [ "type": "custom_attribute", "name": "age", "match": "exact", "value": ["invalid" : nil] ],
"name": "age"
],
[
"id": kAudienceIdGtInvalidValue,
"conditions": [ "type": "custom_attribute", "name": "age", "match": "gt", "value": ["invalid"] ],
"conditions": [ "type": "custom_attribute", "name": "age", "match": "gt", "value": ["invalid" : nil] ],
"name": "age"
],
[
"id": kAudienceIdLtInvalidValue,
"conditions": [ "type": "custom_attribute", "name": "age", "match": "lt", "value": ["invalid"] ],
"conditions": [ "type": "custom_attribute", "name": "age", "match": "lt", "value": ["invalid" : nil] ],
"name": "age"
],
[
Expand Down Expand Up @@ -565,7 +565,7 @@ extension DecisionServiceTests_Experiments {
}

func testDoesMeetAudienceConditionsWithExactMatchAndInvalidValue() {
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"exact\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"exact\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)

experiment = try! OTUtils.model(from: sampleExperimentData)
Expand All @@ -575,8 +575,6 @@ extension DecisionServiceTests_Experiments {
result = self.decisionService.doesMeetAudienceConditions(config: config,
experiment: experiment,
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result

XCTAssert(MockLogger.logFound)
XCTAssertFalse(result)
}

Expand Down Expand Up @@ -613,7 +611,7 @@ extension DecisionServiceTests_Experiments {
}

func testDoesMeetAudienceConditionsWithGreaterMatchAndInvalidValue() {
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"gt\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"gt\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)

experiment = try! OTUtils.model(from: sampleExperimentData)
Expand All @@ -623,7 +621,6 @@ extension DecisionServiceTests_Experiments {
result = self.decisionService.doesMeetAudienceConditions(config: config,
experiment: experiment,
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result

XCTAssert(MockLogger.logFound)
XCTAssertFalse(result)
}
Expand All @@ -645,7 +642,7 @@ extension DecisionServiceTests_Experiments {
}

func testDoesMeetAudienceConditionsWithLessMatchAndInvalidValue() {
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"lt\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"lt\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)

experiment = try! OTUtils.model(from: sampleExperimentData)
Expand All @@ -655,7 +652,6 @@ extension DecisionServiceTests_Experiments {
result = self.decisionService.doesMeetAudienceConditions(config: config,
experiment: experiment,
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result

XCTAssert(MockLogger.logFound)
XCTAssertFalse(result)
}
Expand Down
37 changes: 24 additions & 13 deletions Tests/OptimizelyTests-DataModel/AttributeValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,27 @@ class AttributeValueTests: XCTestCase {
XCTAssert(model2 == AttributeValue.int(Int64(value)))
}

func testDecodeSuccessWithInvalidType() {
let value = ["invalid type"]
func testDecodeSuccessWithArrayType() {
let value = ["array type"]

let model = try! OTUtils.getAttributeValueFromNative(value)

XCTAssert(model == AttributeValue.others)

let model2 = AttributeValue(value: value)
XCTAssertNil(model2)
XCTAssertEqual(model, model2)
}

func testEncodeDecodeWithDictionaryType() {
let value: [String: Any] = [
"string": "stringvalue",
"double": 13.0,
"bool": true,
"array": ["a", "b", "c"]
]
let model = AttributeValue(value: value)

let encoded = try! OTUtils.getAttributeValueFromNative(value)
print("hello")
XCTAssertEqual(encoded, model)
}

func testDecodeSuccessWithInvalidTypeNil() {
Expand Down Expand Up @@ -275,7 +287,7 @@ extension AttributeValueTests {
}

func testEncodeJSON5() {
let modelGiven = [AttributeValue.others]
let modelGiven = [AttributeValue.array([AttributeValue.bool(true), AttributeValue.string("us"), AttributeValue.double(4.7)])]
XCTAssert(OTUtils.isEqualWithEncodeThenDecode(modelGiven))
}

Expand All @@ -301,18 +313,17 @@ extension AttributeValueTests {
XCTAssert(model == AttributeValue.bool(valueBool))
XCTAssert(model.description == "bool(\(valueBool))")

let valueOther = [3]
model = try! OTUtils.getAttributeValueFromNative(valueOther)
XCTAssert(model == AttributeValue.others)
XCTAssert(model.description == "others")

let values = [3.0]
model = try! OTUtils.getAttributeValueFromNative(values)
XCTAssert(model == AttributeValue(value: values))
XCTAssert(model.description == "array([double(3.0)])")

let valueInteger = Int64(100)
model = AttributeValue(value: valueInteger)!
XCTAssert(model.description == "int(\(valueInteger))")

let modelOptional = AttributeValue(value: valueOther)
XCTAssertNil(modelOptional)
let modelOptional = AttributeValue(value: values)
XCTAssertNotNil(modelOptional)
}

func testStringValue() {
Expand Down
Loading

0 comments on commit a5cdf42

Please sign in to comment.