diff --git a/.swiftformat b/.swiftformat index 2ab9f74..f5b95dd 100644 --- a/.swiftformat +++ b/.swiftformat @@ -35,6 +35,7 @@ --enable semicolons --semicolons never --disable sortedImports +--disable sortImports --importgrouping testable-bottom --enable spaceAroundOperators --operatorfunc spaced diff --git a/.swiftlint.yml b/.swiftlint.yml index 29287ee..ea4ccda 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -16,6 +16,7 @@ opt_in_rules: disabled_rules: - opening_brace + - non_optional_string_data_conversion # configurable rules can be customized from this configuration file closing_brace: error diff --git a/Sources/Clickstream/AWSClickstreamPlugin+ClientBehavior.swift b/Sources/Clickstream/AWSClickstreamPlugin+ClientBehavior.swift index 0288957..7f98b1b 100644 --- a/Sources/Clickstream/AWSClickstreamPlugin+ClientBehavior.swift +++ b/Sources/Clickstream/AWSClickstreamPlugin+ClientBehavior.swift @@ -69,7 +69,7 @@ extension AWSClickstreamPlugin { } func registerGlobalProperties(_ properties: AnalyticsProperties) { - properties.forEach { key, value in + for (key, value) in properties { analyticsClient.addGlobalAttribute(value, forKey: key) } } diff --git a/Sources/Clickstream/Dependency/Clickstream/Analytics/AnalyticsClient.swift b/Sources/Clickstream/Dependency/Clickstream/Analytics/AnalyticsClient.swift index e20d932..72df60c 100644 --- a/Sources/Clickstream/Dependency/Clickstream/Analytics/AnalyticsClient.swift +++ b/Sources/Clickstream/Dependency/Clickstream/Analytics/AnalyticsClient.swift @@ -29,6 +29,7 @@ class AnalyticsClient: AnalyticsClientBehaviour { private(set) var simpleUserAttributes: [String: Any] = [:] private let clickstream: ClickstreamContext private(set) var userId: String? + let attributeLock = NSLock() var autoRecordClient: AutoRecordEventClient init(clickstream: ClickstreamContext, @@ -45,6 +46,7 @@ class AnalyticsClient: AnalyticsClientBehaviour { } func addGlobalAttribute(_ attribute: AttributeValue, forKey key: String) { + attributeLock.lock() let eventError = EventChecker.checkAttribute( currentNumber: globalAttributes.count, key: key, @@ -54,9 +56,11 @@ class AnalyticsClient: AnalyticsClientBehaviour { } else { globalAttributes[key] = attribute } + attributeLock.unlock() } func addUserAttribute(_ attribute: AttributeValue, forKey key: String) { + attributeLock.lock() let eventError = EventChecker.checkUserAttribute(currentNumber: allUserAttributes.count, key: key, value: attribute) @@ -72,14 +76,19 @@ class AnalyticsClient: AnalyticsClientBehaviour { userAttribute["set_timestamp"] = Date().millisecondsSince1970 allUserAttributes[key] = userAttribute } + attributeLock.unlock() } func removeGlobalAttribute(forKey key: String) { + attributeLock.lock() globalAttributes[key] = nil + attributeLock.unlock() } func removeUserAttribute(forKey key: String) { + attributeLock.lock() allUserAttributes[key] = nil + attributeLock.unlock() } func updateUserId(_ id: String?) { @@ -87,7 +96,9 @@ class AnalyticsClient: AnalyticsClientBehaviour { userId = id UserDefaultsUtil.saveCurrentUserId(storage: clickstream.storage, userId: userId) if let newUserId = id, !newUserId.isEmpty { + attributeLock.lock() allUserAttributes = JsonObject() + attributeLock.unlock() let userInfo = UserDefaultsUtil.getNewUserInfo(storage: clickstream.storage, userId: newUserId) // swiftlint:disable force_cast clickstream.userUniqueId = userInfo["user_unique_id"] as! String @@ -105,7 +116,9 @@ class AnalyticsClient: AnalyticsClientBehaviour { } func updateUserAttributes() { + attributeLock.lock() UserDefaultsUtil.updateUserAttributes(storage: clickstream.storage, userAttributes: allUserAttributes) + attributeLock.unlock() } // MARK: - Event recording @@ -130,6 +143,9 @@ class AnalyticsClient: AnalyticsClientBehaviour { } func record(_ event: ClickstreamEvent) throws { + if event.eventType != Event.PresetEvent.CLICKSTREAM_ERROR{ + attributeLock.lock() + } for (key, attribute) in globalAttributes { event.addGlobalAttribute(attribute, forKey: key) } @@ -147,6 +163,9 @@ class AnalyticsClient: AnalyticsClientBehaviour { event.setUserAttribute(simpleUserAttributes) } try eventRecorder.save(event) + if event.eventType != Event.PresetEvent.CLICKSTREAM_ERROR{ + attributeLock.unlock() + } } func recordEventError(_ eventError: EventChecker.EventError) { @@ -167,6 +186,7 @@ class AnalyticsClient: AnalyticsClientBehaviour { } func getSimpleUserAttributes() -> [String: Any] { + attributeLock.lock() simpleUserAttributes = [:] simpleUserAttributes[Event.ReservedAttribute.USER_FIRST_TOUCH_TIMESTAMP] = allUserAttributes[Event.ReservedAttribute.USER_FIRST_TOUCH_TIMESTAMP] @@ -174,6 +194,7 @@ class AnalyticsClient: AnalyticsClientBehaviour { simpleUserAttributes[Event.ReservedAttribute.USER_ID] = allUserAttributes[Event.ReservedAttribute.USER_ID] } + attributeLock.unlock() return simpleUserAttributes } } diff --git a/Sources/Clickstream/Dependency/Clickstream/Event/ClickstreamEvent.swift b/Sources/Clickstream/Dependency/Clickstream/Event/ClickstreamEvent.swift index 301d9c7..27569d2 100644 --- a/Sources/Clickstream/Dependency/Clickstream/Event/ClickstreamEvent.swift +++ b/Sources/Clickstream/Dependency/Clickstream/Event/ClickstreamEvent.swift @@ -21,6 +21,7 @@ class ClickstreamEvent: AnalyticsPropertiesModel { private(set) lazy var attributes: [String: AttributeValue] = [:] private(set) lazy var items: [ClickstreamAttribute] = [] private(set) lazy var userAttributes: [String: Any] = [:] + let attributeLock = NSLock() let systemInfo: SystemInfo let netWorkType: String @@ -72,7 +73,11 @@ class ClickstreamEvent: AnalyticsPropertiesModel { } func setUserAttribute(_ attributes: [String: Any]) { - userAttributes = attributes + attributeLock.lock() + for attr in attributes { + userAttributes[attr.key] = attr.value + } + attributeLock.unlock() } func attribute(forKey key: String) -> AttributeValue? { @@ -109,9 +114,11 @@ class ClickstreamEvent: AnalyticsPropertiesModel { if !items.isEmpty { event["items"] = items } + attributeLock.lock() if !userAttributes.isEmpty { event["user"] = userAttributes } + attributeLock.unlock() event["attributes"] = getAttributeObject(from: attributes) return event } diff --git a/Tests/ClickstreamTests/Clickstream/ClickstreamEventTest.swift b/Tests/ClickstreamTests/Clickstream/ClickstreamEventTest.swift index dbb8c73..71f40fb 100644 --- a/Tests/ClickstreamTests/Clickstream/ClickstreamEventTest.swift +++ b/Tests/ClickstreamTests/Clickstream/ClickstreamEventTest.swift @@ -8,6 +8,7 @@ @testable import Clickstream import Foundation import XCTest + class ClickstreamEventTest: XCTestCase { let testAppId = "testAppId" let storage = ClickstreamContextStorage(userDefaults: UserDefaults.standard) diff --git a/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift b/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift index 4b74edf..e914dc9 100644 --- a/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift +++ b/Tests/ClickstreamTests/Clickstream/EventRecorderTest.swift @@ -183,6 +183,20 @@ class EventRecorderTest: XCTestCase { XCTAssertEqual("carl", (userAttributes["_user_name"] as! JsonObject)["value"] as! String) } + func testModifyGlobalUserAttributesWillNotAffectTheEventUserAttributes() throws { + var userInfo = JsonObject() + let currentTimeStamp = Date().millisecondsSince1970 + var userNameInfo = JsonObject() + userNameInfo["value"] = "carl" + userNameInfo["set_timestamp"] = currentTimeStamp + userInfo["_user_name"] = userNameInfo + clickstreamEvent.setUserAttribute(userInfo) + userInfo = JsonObject() + userNameInfo["value"] = "mike" + userInfo["_user_name"] = userNameInfo + XCTAssertEqual((clickstreamEvent.userAttributes["_user_name"] as! JsonObject)["value"] as! String, "carl") + } + func testRecordEventWithInvalidAttribute() throws { clickstreamEvent.addGlobalAttribute(Decimal.nan, forKey: "invalidDecimal") try eventRecorder.save(clickstreamEvent) diff --git a/Tests/ClickstreamTests/IntegrationTest.swift b/Tests/ClickstreamTests/IntegrationTest.swift index 22c3e87..9143921 100644 --- a/Tests/ClickstreamTests/IntegrationTest.swift +++ b/Tests/ClickstreamTests/IntegrationTest.swift @@ -333,6 +333,26 @@ class IntegrationTest: XCTestCase { XCTAssertEqual(1, eventCount) } + func testModifyUserAttributesInMulitThread() throws { + for number in 1 ... 100 { + Task { + ClickstreamAnalytics.addUserAttributes([ + "_user_age": 21, + "isFirstOpen": true, + "score": 85.2, + "_user_name": "name\(number)" + ]) + } + Task { + ClickstreamAnalytics.setUserId("userId\(number)") + } + Task { + ClickstreamAnalytics.recordEvent("testEvent\(number)") + } + } + Thread.sleep(forTimeInterval: 1) + } + // MARK: - Objc test func testRecordEventForObjc() throws {