Skip to content

[FSSDK-11036] fix: event tags support nested objects #570

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
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading