diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..3da3cadbd 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -1,11 +1,5 @@ -// -// DefaultSplitClient.swift -// Split -// // Created by Brian Sztamfater on 20/9/17. // Modified by Natalia Stele on 11/10/17. -// -// import Foundation @@ -55,7 +49,7 @@ public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClie } } -// MARK: Events +// MARK: Customers Listeners extension DefaultSplitClient { public func on(event: SplitEvent, execute action: @escaping SplitAction) { @@ -89,14 +83,23 @@ extension DefaultSplitClient { on(event: event, executeTask: task) } - private func on(event: SplitEvent, executeTask task: SplitEventTask) { - if event != .sdkReadyFromCache, - eventsManager.eventAlreadyTriggered(event: event) { - Logger.w("A handler was added for \(event.toString()) on the SDK, " + - "which has already fired and won’t be emitted again. The callback won’t be executed.") + private func onWithMetadata(event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute actionWithMetadata: @escaping SplitActionWithMetadata) { + guard let factory = clientManager?.splitFactory else { return } + let task = SplitEventActionTask(action: actionWithMetadata, event: event.type, runInBackground: runInBackground, factory: factory, queue: queue) + on(event: event.type, executeTask: task) + } + + public func on(event: SplitEvent, executeWithMetadata action: SplitActionWithMetadata?) { + guard let action = action else { return } + onWithMetadata(event: SplitEventWithMetadata(type: event, metadata: nil), runInBackground: true, queue: nil, execute: action) + } + + private func on(event: SplitEvent, executeTask task: SplitEventActionTask) { + if event != .sdkReadyFromCache, eventsManager.eventAlreadyTriggered(event: event) { + Logger.w("A handler was added for \(event.toString()) on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.") return } - eventsManager.register(event: event, task: task) + eventsManager.register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } } @@ -164,7 +167,7 @@ extension DefaultSplitClient { } } -// MARK: Track Events +// MARK: Tracking extension DefaultSplitClient { public func track(trafficType: String, eventType: String) -> Bool { @@ -214,7 +217,7 @@ extension DefaultSplitClient { } } -// MARK: Persistent attributes feature +// MARK: Persistence extension DefaultSplitClient { public func setAttribute(name: String, value: Any) -> Bool { @@ -256,8 +259,7 @@ extension DefaultSplitClient { } private func isValidAttribute(_ value: Any) -> Bool { - return anyValueValidator.isPrimitiveValue(value: value) || - anyValueValidator.isList(value: value) + return anyValueValidator.isPrimitiveValue(value: value) || anyValueValidator.isList(value: value) } private func logInvalidAttribute(name: String) { @@ -266,31 +268,31 @@ extension DefaultSplitClient { } private func attributesStorage() -> AttributesStorage { - return storageContainer.attributesStorage + storageContainer.attributesStorage } } -// MARK: By Sets evaluation +// MARK: By Flagsets extension DefaultSplitClient { public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { - return treatmentManager.getTreatmentsByFlagSet(flagSet: flagSet, attributes: attributes, evaluationOptions: nil) + treatmentManager.getTreatmentsByFlagSet(flagSet: flagSet, attributes: attributes, evaluationOptions: nil) } public func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String] { - return treatmentManager.getTreatmentsByFlagSets(flagSets: flagSets, attributes: attributes, evaluationOptions: nil) + treatmentManager.getTreatmentsByFlagSets(flagSets: flagSets, attributes: attributes, evaluationOptions: nil) } public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult] { - return treatmentManager.getTreatmentsWithConfigByFlagSet(flagSet: flagSet, attributes: attributes, evaluationOptions: nil) + treatmentManager.getTreatmentsWithConfigByFlagSet(flagSet: flagSet, attributes: attributes, evaluationOptions: nil) } public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: SplitResult] { - return treatmentManager.getTreatmentsWithConfigByFlagSets(flagSets: flagSets, attributes: attributes, evaluationOptions: nil) + treatmentManager.getTreatmentsWithConfigByFlagSets(flagSets: flagSets, attributes: attributes, evaluationOptions: nil) } } -// MARK: Flush / Destroy +// MARK: Lifecycle extension DefaultSplitClient { private func syncFlush() { diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 6f90ba7b7..95234cedf 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -1,10 +1,5 @@ -// -// FailHelpers.swift -// Split -// // Created by Javier Avrudsky on 24-Apr-2022. // Copyright © 2022 Split. All rights reserved. -// import Foundation @@ -57,40 +52,38 @@ class FailedClient: SplitClient { func on(event: SplitEvent, execute action: @escaping SplitAction) { } - func on(event: SplitEvent, runInBackground: Bool, - execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, - queue: DispatchQueue, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) {} func track(trafficType: String, eventType: String) -> Bool { - return false + false } func track(trafficType: String, eventType: String, value: Double) -> Bool { - return false + false } func track(eventType: String) -> Bool { - return false + false } func track(eventType: String, value: Double) -> Bool { - return false + false } func setAttribute(name: String, value: Any) -> Bool { - return false + false } func getAttribute(name: String) -> Any? { - return false + false } func setAttributes(_ values: [String: Any]) -> Bool { - return false + false } func getAttributes() -> [String: Any]? { @@ -98,11 +91,11 @@ class FailedClient: SplitClient { } func removeAttribute(name: String) -> Bool { - return false + false } func clearAttributes() -> Bool { - return false + false } func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { @@ -137,33 +130,30 @@ class FailedClient: SplitClient { return [:] } - func setUserConsent(enabled: Bool) { - } + func setUserConsent(enabled: Bool) {} - func flush() { - } + func flush() {} - func destroy() { - } + func destroy() {} func destroy(completion: (() -> Void)?) { completion?() } func track(trafficType: String, eventType: String, properties: [String: Any]?) -> Bool { - return false + false } func track(trafficType: String, eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return false + false } func track(eventType: String, properties: [String: Any]?) -> Bool { - return false + false } func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return false + false } } @@ -173,6 +163,6 @@ class FailedManager: SplitManager { var splitNames: [String] = [] func split(featureName: String) -> SplitView? { - return nil + nil } } diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..c2496d08f 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -65,27 +65,27 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } public func getTreatment(_ split: String, attributes: [String: Any]?) -> String { - return getTreatmentWithConfig(split).treatment + getTreatmentWithConfig(split).treatment } public func getTreatment(_ split: String) -> String { - return getTreatment(split, attributes: nil) + getTreatment(split, attributes: nil) } public func getTreatment(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> String { - return getTreatmentWithConfig(split, attributes: attributes, evaluationOptions: evaluationOptions).treatment + getTreatmentWithConfig(split, attributes: attributes, evaluationOptions: evaluationOptions).treatment } public func getTreatments(splits: [String], attributes: [String: Any]?) -> [String: String] { - return getTreatmentsWithConfig(splits: splits, attributes: nil).mapValues({ $0.treatment }) + getTreatmentsWithConfig(splits: splits, attributes: nil).mapValues({ $0.treatment }) } public func getTreatments(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { - return getTreatmentsWithConfig(splits: splits, attributes: attributes, evaluationOptions: evaluationOptions).mapValues({ $0.treatment }) + getTreatmentsWithConfig(splits: splits, attributes: attributes, evaluationOptions: evaluationOptions).mapValues({ $0.treatment }) } public func getTreatmentWithConfig(_ split: String) -> SplitResult { - return getTreatmentWithConfig(split, attributes: nil) + getTreatmentWithConfig(split, attributes: nil) } public func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?) -> SplitResult { @@ -102,7 +102,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } public func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> SplitResult { - return getTreatmentWithConfig(split, attributes: attributes) + getTreatmentWithConfig(split, attributes: attributes) } public func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?) -> [String: SplitResult] { @@ -121,69 +121,81 @@ public final class LocalhostSplitClient: NSObject, SplitClient { return results } - public func on(event: SplitEvent, runInBackground: Bool, - execute action: @escaping SplitAction) { + public func on(event: SplitEvent, execute action: @escaping SplitAction) { + on(event: event, runInBackground: false, queue: nil, execute: action) + } + + public func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + on(eventWithMetadata: SplitEventWithMetadata(type: event, metadata: nil), runInBackground: false, queue: nil, execute: executeWithMetadata) + } + + public func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { on(event: event, runInBackground: runInBackground, queue: nil, execute: action) } + + private func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + on(eventWithMetadata: SplitEventWithMetadata(type: event, metadata: nil), runInBackground: runInBackground, queue: queue, execute: action) + } public func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { on(event: event, runInBackground: true, queue: queue, execute: action) } - public func on(event: SplitEvent, execute action: @escaping SplitAction) { - on(event: event, runInBackground: false, queue: nil, execute: action) + private func on(eventWithMetadata event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + + guard let factory = clientManger?.splitFactory else { return } + + if let eventsManager = self.eventsManager { + let task = SplitEventActionTask(action: action, event: event.type, runInBackground: runInBackground, factory: factory, queue: queue) + eventsManager.register(event: event, task: task) + } } - private func on(event: SplitEvent, runInBackground: Bool, - queue: DispatchQueue?, execute action: @escaping SplitAction) { + private func on(eventWithMetadata event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitActionWithMetadata) { guard let factory = clientManger?.splitFactory else { return } + if let eventsManager = self.eventsManager { - let task = SplitEventActionTask(action: action, event: event, - runInBackground: runInBackground, - factory: factory, - queue: queue) + let task = SplitEventActionTask(action: action, event: event.type, runInBackground: runInBackground, factory: factory, queue: queue) eventsManager.register(event: event, task: task) } } public func track(trafficType: String, eventType: String) -> Bool { - return true + true } public func track(trafficType: String, eventType: String, value: Double) -> Bool { - return true + true } public func track(eventType: String) -> Bool { - return true + true } public func track(eventType: String, value: Double) -> Bool { - return true + true } public func track(trafficType: String, eventType: String, properties: [String: Any]?) -> Bool { - return true + true } public func track(trafficType: String, eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return true + true } public func track(eventType: String, properties: [String: Any]?) -> Bool { - return true + true } public func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return true + true } - public func setUserConsent(enabled: Bool) { - } + public func setUserConsent(enabled: Bool) {} - public func flush() { - } + public func flush() {} public func destroy() { splitsStorage.destroy() @@ -199,63 +211,61 @@ public final class LocalhostSplitClient: NSObject, SplitClient { extension LocalhostSplitClient { public func setAttribute(name: String, value: Any) -> Bool { - return true + true } public func getAttribute(name: String) -> Any? { - return nil + nil } public func setAttributes(_ values: [String: Any]) -> Bool { - return true + true } public func getAttributes() -> [String: Any]? { - return nil + nil } public func removeAttribute(name: String) -> Bool { - return true + true } public func clearAttributes() -> Bool { - return true + true } } // MARK: TreatmentBySets Feature extension LocalhostSplitClient { public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { - return [String: String]() + [String: String]() } public func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String] { - return [String: String]() + [String: String]() } public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult] { - return [String: SplitResult]() + [String: SplitResult]() } public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: SplitResult] { - return [String: SplitResult]() + [String: SplitResult]() } public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { - return [String: String]() + [String: String]() } public func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] { - return [String: String]() + [String: String]() } - public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, - attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { - return [String: SplitResult]() + public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + [String: SplitResult]() } - public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], - attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { - return [String: SplitResult]() + public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + [String: SplitResult]() } } diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index ff4df2005..2ff8a0f7c 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -1,22 +1,13 @@ -// -// SplitClient.swift -// Split -// // Created by Brian Sztamfater on 18/9/17. -// -// import Foundation -public typealias SplitAction = () -> Void - @objc public protocol SplitClient { - // MARK: Evaluation feature + // MARK: Evaluation func getTreatment(_ split: String, attributes: [String: Any]?) -> String func getTreatment(_ split: String) -> String - @objc(getTreatmentsForSplits:attributes:) func getTreatments(splits: [String], - attributes: [String: Any]?) -> [String: String] + @objc(getTreatmentsForSplits:attributes:) func getTreatments(splits: [String], attributes: [String: Any]?) -> [String: String] func getTreatmentWithConfig(_ split: String) -> SplitResult func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?) -> SplitResult @@ -24,26 +15,27 @@ public typealias SplitAction = () -> Void @objc(getTreatmentsWithConfigForSplits:attributes:) func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?) -> [String: SplitResult] - // MARK: Evaluation with Properties + // MARK: With Properties func getTreatment(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> String - @objc(getTreatmentsForSplits:attributes:evaluationOptions:) func getTreatments(splits: [String], - attributes: [String: Any]?, - evaluationOptions: EvaluationOptions?) -> [String: String] + @objc(getTreatmentsForSplits:attributes:evaluationOptions:) + func getTreatments(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> SplitResult @objc(getTreatmentsWithConfigForSplits:attributes:evaluationOptions:) func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] + // MARK: Customer listeners func on(event: SplitEvent, execute action: @escaping SplitAction) + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) -> Void func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) - // MARK: Track feature + // MARK: Tracking func track(trafficType: String, eventType: String) -> Bool func track(trafficType: String, eventType: String, value: Double) -> Bool func track(eventType: String) -> Bool func track(eventType: String, value: Double) -> Bool - // MARK: Persistent attributes feature + // MARK: Persistence /// Creates or updates the value for the given attribute func setAttribute(name: String, value: Any) -> Bool @@ -63,34 +55,31 @@ public typealias SplitAction = () -> Void /// Clears all attributes stored in the SDK. func clearAttributes() -> Bool - // MARK: Client lifecycle + // MARK: Lifecycle func flush() func destroy() func destroy(completion: (() -> Void)?) - @objc(trackWithTrafficType:eventType:properties:) func track(trafficType: String, - eventType: String, - properties: [String: Any]?) -> Bool + // MARK: With Properties + @objc(trackWithTrafficType:eventType:properties:) + func track(trafficType: String, eventType: String, properties: [String: Any]?) -> Bool - @objc(trackWithTrafficType:eventType:value:properties:) func track(trafficType: String, - eventType: String, - value: Double, - properties: [String: Any]?) -> Bool + @objc(trackWithTrafficType:eventType:value:properties:) + func track(trafficType: String, eventType: String, value: Double, properties: [String: Any]?) -> Bool - @objc(trackWithEventType:properties:) func track(eventType: String, - properties: [String: Any]?) -> Bool + @objc(trackWithEventType:properties:) + func track(eventType: String, properties: [String: Any]?) -> Bool - @objc(trackWithEventType:value:properties:) func track(eventType: String, - value: Double, - properties: [String: Any]?) -> Bool + @objc(trackWithEventType:value:properties:) + func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool - // MARK: Evaluation with flagsets + // MARK: With Flagsets func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String] func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult] func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: SplitResult] - // MARK: Evaluation with flagsets and properties + // MARK: With flagsets and properties func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: String] func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index 8fd45f67f..c02bf5b71 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -73,19 +73,23 @@ class GenericBlockingQueue { // Protocol to allow mocking protocol InternalEventBlockingQueue { - func add(_ item: SplitInternalEvent) - func take() throws -> SplitInternalEvent + func add(_ item: SplitInternalEventWithMetadata) + func take() throws -> SplitInternalEventWithMetadata func stop() } class DefaultInternalEventBlockingQueue: InternalEventBlockingQueue { - let blockingQueue = GenericBlockingQueue() + let blockingQueue = GenericBlockingQueue() func add(_ item: SplitInternalEvent) { + blockingQueue.add(SplitInternalEventWithMetadata(item)) + } + + func add(_ item: SplitInternalEventWithMetadata) { blockingQueue.add(item) } - func take() throws -> SplitInternalEvent { - let value = try blockingQueue.take() + func take() throws -> SplitInternalEventWithMetadata { + let value = try blockingQueue.take() return value } diff --git a/Split/Engine/DefaultTreatmentManager.swift b/Split/Engine/DefaultTreatmentManager.swift index 73594b32e..716191529 100644 --- a/Split/Engine/DefaultTreatmentManager.swift +++ b/Split/Engine/DefaultTreatmentManager.swift @@ -337,8 +337,8 @@ extension DefaultTreatmentManager { } private func isSdkReady() -> Bool { - return eventsManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyFromCache) || - eventsManager.eventAlreadyTriggered(event: SplitEvent.sdkReady) + return eventsManager.eventAlreadyTriggered(event: .sdkReadyFromCache) || + eventsManager.eventAlreadyTriggered(event: .sdkReady) } private func checkAndLogIfDestroyed(logTag: String) -> Bool { diff --git a/Split/Engine/Evaluator.swift b/Split/Engine/Evaluator.swift index 8d2098103..5deaf4fa0 100644 --- a/Split/Engine/Evaluator.swift +++ b/Split/Engine/Evaluator.swift @@ -6,49 +6,10 @@ // import Foundation -// swiftlint:disable function_body_length -struct EvaluationResult { - var treatment: String - var label: String - var changeNumber: Int64? - var configuration: String? - var impressionsDisabled: Bool - - init(treatment: String, label: String, changeNumber: Int64? = nil, configuration: String? = nil, - impressionsDisabled: Bool = false) { - self.treatment = treatment - self.label = label - self.changeNumber = changeNumber - self.configuration = configuration - self.impressionsDisabled = impressionsDisabled - } -} - -struct EvalValues { - let matchValue: Any? - let matchingKey: String - let bucketingKey: String? - let attributes: [String: Any]? - init(matchValue: Any?, matchingKey: String, bucketingKey: String? = nil, attributes: [String: Any]? = nil) { - self.matchValue = matchValue - self.matchingKey = matchingKey - self.bucketingKey = bucketingKey - self.attributes = attributes - } -} - -// Components needed -struct EvalContext { - let evaluator: Evaluator? - let mySegmentsStorage: MySegmentsStorage? - let myLargeSegmentsStorage: MySegmentsStorage? - let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage? -} protocol Evaluator { - func evalTreatment(matchingKey: String, bucketingKey: String?, - splitName: String, attributes: [String: Any]?) throws -> EvaluationResult + func evalTreatment(matchingKey: String, bucketingKey: String?, splitName: String, attributes: [String: Any]?) throws -> EvaluationResult } class DefaultEvaluator: Evaluator { @@ -184,3 +145,42 @@ private extension Split { return self.impressionsDisabled ?? false } } + +//MARK: Components needed +struct EvaluationResult { + var treatment: String + var label: String + var changeNumber: Int64? + var configuration: String? + var impressionsDisabled: Bool + + init(treatment: String, label: String, changeNumber: Int64? = nil, configuration: String? = nil, + impressionsDisabled: Bool = false) { + self.treatment = treatment + self.label = label + self.changeNumber = changeNumber + self.configuration = configuration + self.impressionsDisabled = impressionsDisabled + } +} + +struct EvalValues { + let matchValue: Any? + let matchingKey: String + let bucketingKey: String? + let attributes: [String: Any]? + + init(matchValue: Any?, matchingKey: String, bucketingKey: String? = nil, attributes: [String: Any]? = nil) { + self.matchValue = matchValue + self.matchingKey = matchingKey + self.bucketingKey = bucketingKey + self.attributes = attributes + } +} + +struct EvalContext { + let evaluator: Evaluator? + let mySegmentsStorage: MySegmentsStorage? + let myLargeSegmentsStorage: MySegmentsStorage? + let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage? +} diff --git a/Split/Events/EventMetadataType.swift b/Split/Events/EventMetadataType.swift index da441c028..95d3244fe 100644 --- a/Split/Events/EventMetadataType.swift +++ b/Split/Events/EventMetadataType.swift @@ -3,16 +3,16 @@ import Foundation @objc public class EventMetadata: NSObject { - var type: EventMetadataType - var data: String = "" + public var type: EventMetadataType + public var data: [String] = [] - init(type: EventMetadataType, data: String) { + init(type: EventMetadataType, data: [String]) { self.type = type self.data = data } } -@objc enum EventMetadataType: Int { +@objc public enum EventMetadataType: Int { case FLAGS_UPDATED case FLAGS_KILLED case SEGMENTS_UPDATED diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..537925600 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -20,11 +20,16 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { private let queue = DispatchQueue(label: "split-event-manager-coordinator") private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, - .splitsUpdated, - .splitKilledNotification] + .splitsUpdated, + .splitKilledNotification, + .ruleBasedSegmentsUpdated] ) func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata? = nil) { if !eventsToHandle.contains(event) { return } @@ -33,7 +38,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { self.triggered.insert(event) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event) + manager.notifyInternalEvent(event, metadata: metadata) } } } @@ -62,7 +67,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { manager.start() managers[key] = manager triggered.forEach { - manager.notifyInternalEvent($0) + manager.notifyInternalEvent($0, metadata: nil) } } } @@ -76,5 +81,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { } } - func register(event: SplitEvent, task: SplitEventTask) {} + func register(event: SplitEvent, task: SplitEventActionTask) {} + + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) {} } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index d2561e2d9..55596e428 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -1,17 +1,26 @@ -// -// SplitEvent.swift -// Split -// // Created by Sebastian Arrubia on 4/17/18. -// import Foundation +// All events (internal & external) support metadata. +// Internal errors are propagated to the customer as events "(.sdkError)". The error info will travel as the event metadata. + +@objcMembers public class SplitEventWithMetadata: NSObject { + let type: SplitEvent + let metadata: EventMetadata? + + @objc public init(type: SplitEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } +} + @objc public enum SplitEvent: Int { case sdkReady case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated + case sdkError public func toString() -> String { switch self { @@ -23,6 +32,8 @@ import Foundation return "SDK_READY_TIMED_OUT" case .sdkReadyFromCache: return "SDK_READY_FROM_CACHE" + case .sdkError: + return "SDK_ERROR" } } } diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index be1368d04..906f027bb 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -1,16 +1,17 @@ -// -// SplitEventActionTask.swift -// Split -// // Created by Javier L. Avrudsky on 7/6/18. -// import Foundation +public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (_ metadata: EventMetadata?) -> Void + class SplitEventActionTask: SplitEventTask { + // Private private var eventHandler: SplitAction? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? + var event: SplitEvent var runInBackground: Bool = false var factory: SplitFactory @@ -27,13 +28,25 @@ class SplitEventActionTask: SplitEventTask { self.queue = queue self.factory = factory } + + init(action: @escaping SplitActionWithMetadata, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + self.eventHandlerWithMetadata = action + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory + } func takeQueue() -> DispatchQueue? { defer { queue = nil } return queue } - - func run() { + + func run(_ metadata: EventMetadata?) { eventHandler?() + + if let metadata = metadata { + eventHandlerWithMetadata?(metadata) + } } } diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..147bc8441 100644 --- a/Split/Events/SplitEventTask.swift +++ b/Split/Events/SplitEventTask.swift @@ -11,5 +11,5 @@ protocol SplitEventTask { var event: SplitEvent { get } var runInBackground: Bool { get } func takeQueue() -> DispatchQueue? - func run() + func run(_ metadata: EventMetadata?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index ea64d0279..859ec1d9b 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -9,8 +9,10 @@ import Foundation protocol SplitEventsManager: AnyObject { - func register(event: SplitEvent, task: SplitEventTask) + func register(event: SplitEvent, task: SplitEventActionTask) + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) func notifyInternalEvent(_ event: SplitInternalEvent) + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata?) func start() func stop() func eventAlreadyTriggered(event: SplitEvent) -> Bool @@ -48,18 +50,25 @@ class DefaultSplitEventsManager: SplitEventsManager { } } } + + // MARK: Notifiers + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata? = nil) { + let event = SplitInternalEventWithMetadata(event, metadata: metadata) - func notifyInternalEvent(_ event: SplitInternalEvent) { processQueue.async { [weak self] in if let self = self { - Logger.v("Event \(event) notified") self.eventsQueue.add(event) } } } - func register(event: SplitEvent, task: SplitEventTask) { - let eventName = event.toString() + func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + // MARK: Registers + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) { + let eventName = event.type.toString() processQueue.async { [weak self] in guard let self = self else { return } // If event is already triggered, execute the task @@ -67,10 +76,16 @@ class DefaultSplitEventsManager: SplitEventsManager { self.executeTask(event: event, task: task) return } - self.subscribe(task: task, to: event) + self.subscribe(task: task, to: event.type) } } + + // Method kept for backwards compatibility. Allows registering an event without metadata. + func register(event: SplitEvent, task: SplitEventActionTask) { + register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) + } + // MARK: Flow func start() { dataAccessQueue.sync { if self.isStarted { @@ -128,7 +143,7 @@ class DefaultSplitEventsManager: SplitEventsManager { return isRunning } - private func takeEvent() -> SplitInternalEvent? { + private func takeEvent() -> SplitInternalEventWithMetadata? { do { return try eventsQueue.take() } catch BlockingQueueError.hasBeenStopped { @@ -140,37 +155,37 @@ class DefaultSplitEventsManager: SplitEventsManager { } private func processEvents() { + while isRunning() { - guard let event = takeEvent() else { - return - } - self.triggered.append(event) - switch event { - case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - self.triggerSdkReadyIfNeeded() - - case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, - .splitsLoadedFromCache, .attributesLoadedFromCache: - Logger.v("Event \(event) triggered") - if isTriggered(internal: .splitsLoadedFromCache), - isTriggered(internal: .mySegmentsLoadedFromCache), - isTriggered(internal: .myLargeSegmentsLoadedFromCache), - isTriggered(internal: .attributesLoadedFromCache) { - trigger(event: SplitEvent.sdkReadyFromCache) - } - case .splitKilledNotification: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - case .sdkReadyTimeoutReached: - if !isTriggered(external: .sdkReady) { - trigger(event: SplitEvent.sdkReadyTimedOut) - } + guard let event = takeEvent() else { return } + triggered.append(event.type) + + switch event.type { + case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated, .ruleBasedSegmentsUpdated: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated, metadata: event.metadata) + continue + } + self.triggerSdkReadyIfNeeded() + + case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, + .splitsLoadedFromCache, .attributesLoadedFromCache: + Logger.v("Event \(event) triggered") + if isTriggered(internal: .splitsLoadedFromCache), + isTriggered(internal: .mySegmentsLoadedFromCache), + isTriggered(internal: .myLargeSegmentsLoadedFromCache), + isTriggered(internal: .attributesLoadedFromCache) { + trigger(event: .sdkReadyFromCache) + } + case .splitKilledNotification: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated, metadata: event.metadata) + continue + } + case .sdkReadyTimeoutReached: + if !isTriggered(external: .sdkReady) { + trigger(event: .sdkReadyTimedOut) + } } } } @@ -200,8 +215,12 @@ class DefaultSplitEventsManager: SplitEventsManager { } } - private func trigger(event: SplitEvent) { - let eventName = event.toString() + private func trigger(event: SplitEvent, metadata: EventMetadata? = nil) { + trigger(event: SplitEventWithMetadata(type: event, metadata: metadata)) + } + + private func trigger(event: SplitEventWithMetadata) { + let eventName = event.type.toString() // If executionTimes is zero, maximum executions has been reached if executionTimes(for: eventName) == 0 { @@ -215,14 +234,14 @@ class DefaultSplitEventsManager: SplitEventsManager { Logger.d("Triggering SDK event \(eventName)") // If executionTimes is lower than zero, execute it without limitation - if let subscriptions = getSubscriptions(for: event) { + if let subscriptions = getSubscriptions(for: event.type) { for task in subscriptions { executeTask(event: event, task: task) } } } - private func executeTask(event: SplitEvent, task: SplitEventTask) { + private func executeTask(event: SplitEventWithMetadata, task: SplitEventTask) { let eventName = task.event.toString() @@ -232,7 +251,7 @@ class DefaultSplitEventsManager: SplitEventsManager { let queue = task.takeQueue() ?? DispatchQueue.general queue.async { TimeChecker.logInterval("Running \(eventName) in Background queue \(queue)") - task.run() + task.run(event.metadata) } return } @@ -240,10 +259,10 @@ class DefaultSplitEventsManager: SplitEventsManager { DispatchQueue.main.async { TimeChecker.logInterval("Running event on main: \(eventName)") // UI Updates - task.run() + task.run(event.metadata) } } - + private func isTriggered(internal event: SplitInternalEvent) -> Bool { return triggered.filter { $0 == event }.count > 0 } diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 4c9521204..f4e9a9b19 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -1,15 +1,21 @@ -// -// SplitInternalEvent.swift -// Split -// // Created by Sebastian Arrubia on 4/16/18. -// import Foundation +struct SplitInternalEventWithMetadata: Equatable { + let type: SplitInternalEvent + let metadata: EventMetadata? + + init(_ type: SplitInternalEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } +} + enum SplitInternalEvent { case mySegmentsUpdated case myLargeSegmentsUpdated + case ruleBasedSegmentsUpdated case splitsUpdated case mySegmentsLoadedFromCache case myLargeSegmentsLoadedFromCache diff --git a/Split/FetcherEngine/Refresh/ChangesChecker.swift b/Split/FetcherEngine/Refresh/ChangesChecker.swift index 6c3e0d743..7760e01eb 100644 --- a/Split/FetcherEngine/Refresh/ChangesChecker.swift +++ b/Split/FetcherEngine/Refresh/ChangesChecker.swift @@ -22,6 +22,7 @@ protocol MySegmentsChangesChecker { func mySegmentsHaveChanged(old: SegmentChange, new: SegmentChange) -> Bool func mySegmentsHaveChanged(oldSegments: [Segment], newSegments: [Segment]) -> Bool func mySegmentsHaveChanged(oldSegments: [String], newSegments: [String]) -> Bool + func getSegmentsDiff(oldSegments: [Segment], newSegments: [Segment]) -> [String] } struct DefaultMySegmentsChangesChecker: MySegmentsChangesChecker { @@ -42,5 +43,8 @@ struct DefaultMySegmentsChangesChecker: MySegmentsChangesChecker { return !(oldSegments.count == newSegments.count && oldSegments.sorted() == newSegments.sorted()) } - + + func getSegmentsDiff(oldSegments: [Segment], newSegments: [Segment]) -> [String] { + oldSegments.filter { !Set(newSegments.map { $0.name }).contains($0.name) }.map { $0.name } + } } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index b6fd71c81..89f2c3657 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -130,10 +130,8 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { Logger.i("Fetch from remote not implemented") } - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + func notifyUpdate(_ event: SplitInternalEvent, metadata: EventMetadata? = nil) { + eventsManager.notifyInternalEvent(event, metadata: metadata) } } @@ -183,8 +181,10 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { return } - if result.success, result.featureFlagsUpdated || result.rbsUpdated { - notifyUpdate([.splitsUpdated]) + + if result.success, result.featureFlagsUpdated.count > 0 { + let metadata = EventMetadata(type: .FLAGS_UPDATED, data: result.featureFlagsUpdated) + notifyUpdate(.splitsUpdated, metadata: metadata) } } } @@ -223,9 +223,14 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { mlsTill: myLargeSegmentsStorage.changeNumber, headers: nil) if result.success { - if result.msUpdated || result.mlsUpdated { + if !result.msUpdated.isEmpty || !result.mlsUpdated.isEmpty { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + if !result.msUpdated.isEmpty { + notifyUpdate(.mySegmentsUpdated, metadata: EventMetadata(type: .SEGMENTS_UPDATED, data: result.msUpdated)) + } + if !result.mlsUpdated.isEmpty { + notifyUpdate(.myLargeSegmentsUpdated, metadata: EventMetadata(type: .LARGE_SEGMENTS_UPDATED, data: result.msUpdated)) + } } } } catch { diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 36973b66b..c9c605c8b 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -46,11 +46,16 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { if result.success { if !isSdkReadyTriggered() { // Notifying both to trigger SDK Ready - notifyUpdate([.mySegmentsUpdated]) - notifyUpdate([.myLargeSegmentsUpdated]) - } else if result.msUpdated || result.mlsUpdated { + notifyUpdate(.mySegmentsUpdated, metadata: EventMetadata(type: .SEGMENTS_UPDATED, data: result.msUpdated)) + notifyUpdate(.myLargeSegmentsUpdated, metadata: EventMetadata(type: .LARGE_SEGMENTS_UPDATED, data: result.mlsUpdated)) + } else if !result.msUpdated.isEmpty || !result.mlsUpdated.isEmpty { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + if !result.msUpdated.isEmpty { + notifyUpdate(.mySegmentsUpdated, metadata: EventMetadata(type: .SEGMENTS_UPDATED, data: result.msUpdated)) + } + if !result.mlsUpdated.isEmpty { + notifyUpdate(.myLargeSegmentsUpdated, metadata: EventMetadata(type: .LARGE_SEGMENTS_UPDATED, data: result.mlsUpdated)) + } } return true } @@ -71,8 +76,8 @@ struct SegmentsSyncResult { let success: Bool let msChangeNumber: Int64 let mlsChangeNumber: Int64 - let msUpdated: Bool - let mlsUpdated: Bool + let msUpdated: [String] + let mlsUpdated: [String] } protocol SegmentsSyncHelper { @@ -86,8 +91,8 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { struct FetchResult { let msTill: Int64 let mlsTill: Int64 - let msUpdated: Bool - let mlsUdated: Bool + let msUpdated: [String] + let mlsUpdated: [String] } private let segmentsFetcher: HttpMySegmentsFetcher @@ -165,7 +170,7 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { msChangeNumber: result.msTill, mlsChangeNumber: result.mlsTill, msUpdated: result.msUpdated, - mlsUpdated: result.mlsUdated) + mlsUpdated: result.mlsUpdated) } attemptCount+=1 if attemptCount < maxAttempts { @@ -175,8 +180,8 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { return SegmentsSyncResult(success: false, msChangeNumber: -1, mlsChangeNumber: -1, - msUpdated: false, - mlsUpdated: false) + msUpdated: [], + mlsUpdated: []) } private func fetchUntil(till: Int64?, @@ -210,10 +215,13 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { Logger.d("Checking my large segments update") checkAndUpdate(isChanged: mlsChanged, change: myLargeSegmentsChange, storage: myLargeSegmentsStorage) + let segmentsDiff = changeChecker.getSegmentsDiff(oldSegments: oldChange.segments, newSegments: mySegmentsChange.segments) + let largeSegmentsDiff = changeChecker.getSegmentsDiff(oldSegments: oldLargeChange.segments, newSegments: myLargeSegmentsChange.segments) + return FetchResult(msTill: mySegmentsChange.unwrappedChangeNumber, mlsTill: myLargeSegmentsChange.unwrappedChangeNumber, - msUpdated: msChanged, - mlsUdated: mlsChanged) + msUpdated: segmentsDiff, + mlsUpdated: largeSegmentsDiff) } prevChange = change } diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 28c0a6f9c..014dd1696 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -78,11 +78,14 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { handler(success) } } - - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + + func notifyFlagsUpdate(_ result: SyncResult) { + let metadata = EventMetadata(type: .FLAGS_UPDATED, data: result.featureFlagsUpdated) + notifyUpdate(.splitsUpdated, metadata: metadata) + } + + func notifyUpdate(_ event: SplitInternalEvent, metadata: EventMetadata? = nil) { + eventsManager.notifyInternalEvent(event, metadata: metadata) } func isSdkReadyTriggered() -> Bool { @@ -141,9 +144,8 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) if result.success { - if !isSdkReadyTriggered() || - result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if !isSdkReadyTriggered() || result.featureFlagsUpdated.count > 0 { + notifyFlagsUpdate(result) } resetBackoffCounter() return true @@ -211,14 +213,17 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { } do { + // Try Sync let result = try syncHelper.sync(since: storedChangeNumber, rbSince: storedRbChangeNumber, till: flagsChangeNumber ?? rbsChangeNumber, clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) + + // Success if result.success { - if result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if result.featureFlagsUpdated.count > 0 { + notifyFlagsUpdate(result) } resetBackoffCounter() return true diff --git a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift index 6b5a3e8d2..2cf2b154a 100644 --- a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift +++ b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift @@ -12,8 +12,8 @@ struct SyncResult { let success: Bool let changeNumber: Int64 let rbChangeNumber: Int64? - let featureFlagsUpdated: Bool - let rbsUpdated: Bool + let featureFlagsUpdated: [String] + let rbsUpdated: [String] } class SplitsSyncHelper { @@ -21,8 +21,8 @@ class SplitsSyncHelper { struct FetchResult { let till: Int64 let rbTill: Int64? - let featureFlagsUpdated: Bool - let rbsUpdated: Bool + let featureFlagsUpdated: [String] + let rbsUpdated: [String] } private let splitFetcher: HttpSplitFetcher @@ -163,8 +163,8 @@ class SplitsSyncHelper { return SyncResult(success: false, changeNumber: nextSince, rbChangeNumber: nextRbSince, - featureFlagsUpdated: false, - rbsUpdated: false) + featureFlagsUpdated: [], + rbsUpdated: []) } func fetchUntil(since: Int64, @@ -177,8 +177,8 @@ class SplitsSyncHelper { var firstFetch = true var nextSince = since var nextRbSince = rbSince - var featureFlagsUpdated = false - var rbsUpdated = false + var featureFlagsUpdated: [String] = [] + var rbsUpdated: [String] = [] while true { clearCache = clearCache && firstFetch // Determine which spec version to use and whether to include rbSince @@ -202,13 +202,18 @@ class SplitsSyncHelper { ruleBasedSegmentsStorage.clear() } firstFetch = false - if splitsStorage.update(splitChange: splitChangeProcessor.process(targetingRulesChange.featureFlags)) { - featureFlagsUpdated = true + + // FLAGS PROCESSING + let processedSplits = splitChangeProcessor.process(flagsChange) + if splitsStorage.update(splitChange: processedSplits) { + featureFlagsUpdated = (processedSplits.archivedSplits + processedSplits.activeSplits).compactMap(\.name) } - let processedChange = ruleBasedSegmentsChangeProcessor.process(targetingRulesChange.ruleBasedSegments) - if ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, toRemove: processedChange.toRemove, changeNumber: processedChange.changeNumber) { - rbsUpdated = true + // RULE BASED SEGMENTS PROCESSING + let processedRuleBasedSegmentChange = ruleBasedSegmentsChangeProcessor.process(targetingRulesChange.ruleBasedSegments) + if ruleBasedSegmentsStorage.update(toAdd: processedRuleBasedSegmentChange.toAdd, toRemove: processedRuleBasedSegmentChange.toRemove, changeNumber: processedRuleBasedSegmentChange.changeNumber) { + rbsUpdated += processedRuleBasedSegmentChange.archivedSegments.compactMap(\.name) + rbsUpdated += processedRuleBasedSegmentChange.activeSegments.compactMap(\.name) } Logger.i("Feature flag definitions have been updated") diff --git a/Split/Localhost/LocalhostSynchronizer.swift b/Split/Localhost/LocalhostSynchronizer.swift index 0081bf272..ca31eec5a 100644 --- a/Split/Localhost/LocalhostSynchronizer.swift +++ b/Split/Localhost/LocalhostSynchronizer.swift @@ -41,10 +41,13 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { func stopPeriodicSync() { } - func notifyKilled() { + func notifyKilled(flag: String) { } - func notifyUpdated() { + func notifyUpdated(flags: [String]) { + } + + @objc(notifyUpdatedWithRuleBasedSegments:) func notifyUpdated(ruleBasedSegments segments: [String]) { } func pause() { @@ -74,7 +77,9 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { // Update will remove all records before insert new ones _ = self.featureFlagsStorage.update(splitChange: change) - self.eventsManager.notifyInternalEvent(.splitsUpdated) + // Notify event + let metadata = EventMetadata(type: .FLAGS_UPDATED, data: values.map { $0.name ?? "" } ) + self.eventsManager.notifyInternalEvent(.splitsUpdated, metadata: metadata) } } } diff --git a/Split/Network/Streaming/SseNotificationProcessor.swift b/Split/Network/Streaming/SseNotificationProcessor.swift index 4f0240e84..50fd30d63 100644 --- a/Split/Network/Streaming/SseNotificationProcessor.swift +++ b/Split/Network/Streaming/SseNotificationProcessor.swift @@ -36,18 +36,18 @@ class DefaultSseNotificationProcessor: SseNotificationProcessor { func process(_ notification: IncomingNotification) { Logger.d("Received notification \(notification.type)") switch notification.type { - case .splitUpdate: - processTargetingRuleUpdate(notification) - case .ruleBasedSegmentUpdate: - processTargetingRuleUpdate(notification) - case .mySegmentsUpdate: - processSegmentsUpdate(notification, updateWorker: mySegmentsUpdateWorker) - case .myLargeSegmentsUpdate: - processSegmentsUpdate(notification, updateWorker: myLargeSegmentsUpdateWorker) - case .splitKill: - processSplitKill(notification) - default: - Logger.e("Unknown notification arrived: \(notification.jsonData ?? "null" )") + case .splitUpdate: + processTargetingRuleUpdate(notification) + case .ruleBasedSegmentUpdate: + processTargetingRuleUpdate(notification) + case .mySegmentsUpdate: + processSegmentsUpdate(notification, updateWorker: mySegmentsUpdateWorker) + case .myLargeSegmentsUpdate: + processSegmentsUpdate(notification, updateWorker: myLargeSegmentsUpdateWorker) + case .splitKill: + processSplitKill(notification) + default: + Logger.e("Unknown notification arrived: \(notification.jsonData ?? "null" )") } } diff --git a/Split/Network/Streaming/SyncSegmentsUpdateWorker.swift b/Split/Network/Streaming/SyncSegmentsUpdateWorker.swift index c8ec78ae4..bcedaa113 100644 --- a/Split/Network/Streaming/SyncSegmentsUpdateWorker.swift +++ b/Split/Network/Streaming/SyncSegmentsUpdateWorker.swift @@ -110,7 +110,7 @@ class SegmentsUpdateWorker: UpdateWorker { if segments.count > newSegments.count { mySegmentsStorage.set(SegmentChange(segments: newSegments.asArray()), forKey: key) - synchronizer.notifyUpdate(forKey: key) + synchronizer.notifyUpdate(forKey: key, EventMetadata(type: .SEGMENTS_UPDATED, data: newSegments.asArray() )) telemetryProducer?.recordUpdatesFromSse(type: resource) } } @@ -128,7 +128,7 @@ class SegmentsUpdateWorker: UpdateWorker { if oldSegments.count < newSegments.count { mySegmentsStorage.set(SegmentChange(segments: newSegments.asArray()), forKey: userKey) - synchronizer.notifyUpdate(forKey: userKey) + synchronizer.notifyUpdate(forKey: userKey, EventMetadata(type: .SEGMENTS_UPDATED, data: newSegments.asArray() )) telemetryProducer?.recordUpdatesFromSse(type: .mySegments) } return @@ -171,7 +171,7 @@ class SegmentsUpdateWorker: UpdateWorker { protocol SegmentsSynchronizerWrapper { func fetch(byKey: String, changeNumbers: SegmentsChangeNumber, delay: Int64) - func notifyUpdate(forKey: String) + func notifyUpdate(forKey: String, _ metadata: EventMetadata?) } class MySegmentsSynchronizerWrapper: SegmentsSynchronizerWrapper { @@ -185,12 +185,13 @@ class MySegmentsSynchronizerWrapper: SegmentsSynchronizerWrapper { synchronizer.forceMySegmentsSync(forKey: key, changeNumbers: changeNumbers, delay: delay) } - func notifyUpdate(forKey key: String) { - synchronizer.notifySegmentsUpdated(forKey: key) + func notifyUpdate(forKey key: String, _ metadata: EventMetadata? = nil) { + synchronizer.notifySegmentsUpdated(forKey: key, metadata: metadata) } } class MyLargeSegmentsSynchronizerWrapper: SegmentsSynchronizerWrapper { + private let synchronizer: Synchronizer init(synchronizer: Synchronizer) { @@ -201,8 +202,8 @@ class MyLargeSegmentsSynchronizerWrapper: SegmentsSynchronizerWrapper { synchronizer.forceMySegmentsSync(forKey: key, changeNumbers: changeNumbers, delay: delay) } - func notifyUpdate(forKey key: String) { - synchronizer.notifyLargeSegmentsUpdated(forKey: key) + func notifyUpdate(forKey key: String, _ metadata: EventMetadata? = nil) { + synchronizer.notifyLargeSegmentsUpdated(forKey: key, metadata: metadata) } } diff --git a/Split/Network/Streaming/SyncUpdateWorker.swift b/Split/Network/Streaming/SyncUpdateWorker.swift index 5aede0758..8daade1de 100644 --- a/Split/Network/Streaming/SyncUpdateWorker.swift +++ b/Split/Network/Streaming/SyncUpdateWorker.swift @@ -124,14 +124,11 @@ class SplitsUpdateWorker: UpdateWorker { } /// Process a split update notification - private func processSplitUpdate(payload: String, - compressionType: CompressionType, - previousChangeNumber: Int64, - changeNumber: Int64) -> Bool { + private func processSplitUpdate(payload: String,compressionType: CompressionType, + previousChangeNumber: Int64, + changeNumber: Int64) -> Bool { do { - let split = try self.payloadDecoder.decode( - payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + let split = try payloadDecoder.decode(payload: payload, compressionUtil: decomProvider.decompressor(for: compressionType)) if !allRuleBasedSegmentsExist(in: split) { return false @@ -143,11 +140,14 @@ class SplitsUpdateWorker: UpdateWorker { Logger.v("Split update received: \(change)") - if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { - self.synchronizer.notifyFeatureFlagsUpdated() + let processedFlags = splitChangeProcessor.process(change) + + if splitsStorage.update(splitChange: processedFlags) { + let updatedFlags = (processedFlags.activeSplits + processedFlags.archivedSplits).compactMap(\.name) + synchronizer.notifyFeatureFlagsUpdated(flags: updatedFlags) } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding feature flags payload from notification: \(error)") @@ -157,29 +157,29 @@ class SplitsUpdateWorker: UpdateWorker { /// Process a rule-based segment update notification private func processRuleBasedSegmentUpdate(payload: String, - compressionType: CompressionType, - previousChangeNumber: Int64, - changeNumber: Int64) -> Bool { + compressionType: CompressionType, + previousChangeNumber: Int64, + changeNumber: Int64) -> Bool { do { - let rbs = try self.ruleBasedSegmentsPayloadDecoder.decode( - payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + let rbs = try ruleBasedSegmentsPayloadDecoder.decode(payload: payload, compressionUtil: decomProvider.decompressor(for: compressionType)) let change = RuleBasedSegmentChange(segments: [rbs], - since: previousChangeNumber, - till: changeNumber) + since: previousChangeNumber, + till: changeNumber) Logger.v("RBS update received: \(change)") - let processedChange = ruleBasedSegmentsChangeProcessor.process(change) + let processedSegments = ruleBasedSegmentsChangeProcessor.process(change) - if self.ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, - toRemove: processedChange.toRemove, - changeNumber: processedChange.changeNumber) { - self.synchronizer.notifyFeatureFlagsUpdated() + if ruleBasedSegmentsStorage.update(toAdd: processedSegments.toAdd, + toRemove: processedSegments.toRemove, + changeNumber: processedSegments.changeNumber) { + + let updatedSegments = processedSegments.activeSegments.compactMap(\.name) + processedSegments.archivedSegments.compactMap(\.name) + synchronizer.notifyRuleBasedSegmentsUpdated(segments: updatedSegments) } - - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding rule based segments payload from notification: \(error)") @@ -229,7 +229,7 @@ class SplitKillWorker: UpdateWorker { splitToKill.changeNumber = notification.changeNumber splitToKill.killed = true splitsStorage.updateWithoutChecks(split: splitToKill) - synchronizer.notifySplitKilled() + synchronizer.notifySplitKilled(flag: splitToKill.name ?? "") } } synchronizer.synchronizeSplits(changeNumber: notification.changeNumber) diff --git a/Split/Network/Sync/ByKeyFacade.swift b/Split/Network/Sync/ByKeyFacade.swift index 5f7212bdf..6f6f3ae45 100644 --- a/Split/Network/Sync/ByKeyFacade.swift +++ b/Split/Network/Sync/ByKeyFacade.swift @@ -19,8 +19,8 @@ protocol ByKeyRegistry { protocol ByKeySynchronizer { func loadMySegmentsFromCache(forKey: String) func loadAttributesFromCache(forKey: String) - func notifyMySegmentsUpdated(forKey: String) - func notifyMyLargeSegmentsUpdated(forKey: String) + func notifyMySegmentsUpdated(forKey: String, metadata: EventMetadata?) + func notifyMyLargeSegmentsUpdated(forKey: String, metadata: EventMetadata?) func startSync(forKey key: Key) func startPeriodicSync() func stopPeriodicSync() @@ -77,7 +77,7 @@ class DefaultByKeyFacade: ByKeyFacade { func loadAttributesFromCache(forKey key: String) { doInAll(forMatchingKey: key) { group in group.attributesStorage.loadLocal() - group.eventsManager.notifyInternalEvent(.attributesLoadedFromCache) + group.eventsManager.notifyInternalEvent(.attributesLoadedFromCache, metadata: nil) } TimeChecker.logInterval("Time until attributes loaded from cache") } @@ -120,18 +120,19 @@ class DefaultByKeyFacade: ByKeyFacade { byKeyComponents.value(forKey: key)?.mySegmentsSynchronizer.synchronizeMySegments() } - func notifyMySegmentsUpdated(forKey key: String) { + func notifyMySegmentsUpdated(forKey key: String, metadata: EventMetadata?) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.mySegmentsUpdated) + group.eventsManager.notifyInternalEvent(.mySegmentsUpdated, metadata: metadata) } } - func notifyMyLargeSegmentsUpdated(forKey key: String) { + func notifyMyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.myLargeSegmentsUpdated) + group.eventsManager.notifyInternalEvent(.myLargeSegmentsUpdated, metadata: metadata) } } + // MARK: Cycle func pause() { doInAll { group in group.mySegmentsSynchronizer.pause() @@ -171,6 +172,7 @@ class DefaultByKeyFacade: ByKeyFacade { return byKeyComponents.count == 0 } + // MARK: Helpers private func doInAll(_ action: (ByKeyComponentGroup) -> Void) { let all = byKeyComponents.all for (_, sync) in all { @@ -178,9 +180,7 @@ class DefaultByKeyFacade: ByKeyFacade { } } - private func doInAll(forMatchingKey key: String, - action: (ByKeyComponentGroup) -> Void) { - + private func doInAll(forMatchingKey key: String, action: (ByKeyComponentGroup) -> Void) { byKeyComponents.values(forMatchingKey: key).forEach { group in action(group) } diff --git a/Split/Network/Sync/FeatureFlagsSynchronizer.swift b/Split/Network/Sync/FeatureFlagsSynchronizer.swift index 5434c865a..4dea90220 100644 --- a/Split/Network/Sync/FeatureFlagsSynchronizer.swift +++ b/Split/Network/Sync/FeatureFlagsSynchronizer.swift @@ -14,8 +14,9 @@ protocol FeatureFlagsSynchronizer { func synchronize(changeNumber: Int64?, rbsChangeNumber: Int64?) func startPeriodicSync() func stopPeriodicSync() - func notifyKilled() - func notifyUpdated() + func notifyKilled(flag: String) + func notifyUpdated(flags: [String]) + func notifyUpdated(ruleBasedSegments: [String]) func pause() func resume() func destroy() @@ -78,7 +79,7 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { return } - let splitsStorage = self.storageContainer.splitsStorage + let splitsStorage = storageContainer.splitsStorage DispatchQueue.general.async { let start = Date.nowMillis() self.filterSplitsInCache() @@ -144,12 +145,16 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { periodicSplitsSyncWorker?.stop() } - func notifyKilled() { - splitEventsManager.notifyInternalEvent(.splitKilledNotification) + func notifyKilled(flag: String) { + splitEventsManager.notifyInternalEvent(.splitKilledNotification, metadata: EventMetadata(type: .FLAGS_KILLED, data: [flag])) } - func notifyUpdated() { - splitEventsManager.notifyInternalEvent(.splitsUpdated) + func notifyUpdated(flags: [String]) { + splitEventsManager.notifyInternalEvent(.splitsUpdated, metadata: EventMetadata(type: .FLAGS_UPDATED, data: flags)) + } + + func notifyUpdated(ruleBasedSegments segments: [String]) { + splitEventsManager.notifyInternalEvent(.ruleBasedSegmentsUpdated, metadata: EventMetadata(type: .RULE_BASED_SEGMENTS_UPDATED, data: segments)) } func pause() { diff --git a/Split/Network/Sync/Synchronizer.swift b/Split/Network/Sync/Synchronizer.swift index d3155a193..9805abe9e 100644 --- a/Split/Network/Sync/Synchronizer.swift +++ b/Split/Network/Sync/Synchronizer.swift @@ -23,6 +23,7 @@ protocol Synchronizer: ImpressionLogger { func synchronizeMySegments(forKey key: String) func synchronizeTelemetryConfig() func forceMySegmentsSync(forKey key: String, changeNumbers: SegmentsChangeNumber, delay: Int64) + func startPeriodicFetching() func stopPeriodicFetching() func startRecordingUserData() @@ -30,10 +31,13 @@ protocol Synchronizer: ImpressionLogger { func startRecordingTelemetry() func stopRecordingTelemetry() func pushEvent(event: EventDTO) - func notifyFeatureFlagsUpdated() - func notifySegmentsUpdated(forKey key: String) - func notifyLargeSegmentsUpdated(forKey key: String) - func notifySplitKilled() + + func notifyFeatureFlagsUpdated(flags: [String]) + func notifySplitKilled(flag: String) + func notifyRuleBasedSegmentsUpdated(segments: [String]) + func notifySegmentsUpdated(forKey key: String, metadata: EventMetadata?) + func notifyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata?) + func pause() func resume() func flush() @@ -207,20 +211,24 @@ class DefaultSynchronizer: Synchronizer { } } - func notifyFeatureFlagsUpdated() { - featureFlagsSynchronizer.notifyUpdated() + func notifyFeatureFlagsUpdated(flags: [String]) { + featureFlagsSynchronizer.notifyUpdated(flags: flags) + } + + func notifyRuleBasedSegmentsUpdated(segments: [String]) { + featureFlagsSynchronizer.notifyUpdated(ruleBasedSegments: segments) } - func notifySegmentsUpdated(forKey key: String) { - byKeySynchronizer.notifyMySegmentsUpdated(forKey: key) + func notifySegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { + byKeySynchronizer.notifyMySegmentsUpdated(forKey: key, metadata: metadata) } - func notifyLargeSegmentsUpdated(forKey key: String) { - byKeySynchronizer.notifyMyLargeSegmentsUpdated(forKey: key) + func notifyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { + byKeySynchronizer.notifyMyLargeSegmentsUpdated(forKey: key, metadata: metadata) } - func notifySplitKilled() { - featureFlagsSynchronizer.notifyKilled() + func notifySplitKilled(flag: String) { + featureFlagsSynchronizer.notifyKilled(flag: flag) } func pause() { diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index 04c4b5df3..a38051a20 100644 --- a/SplitTests/Collections/BlockingQueueTest.swift +++ b/SplitTests/Collections/BlockingQueueTest.swift @@ -25,7 +25,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) if local.count == 4 { endExp.fulfill() } @@ -60,7 +60,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) } catch { endExp.fulfill() interrupted = true @@ -105,8 +105,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took: \(event)") + local.append(event.type) + print("Took: \(event.type)") } catch { } } @@ -117,8 +117,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took QA1: \(event)") + local.append(event.type) + print("Took QA1: \(event.type)") } catch { print("\n\n\nERROR!!!!: \(error) \n\n\n") } @@ -129,9 +129,9 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) + local.append(event.type) Thread.sleep(forTimeInterval: 0.3) - print("Took QA2: \(event)") + print("Took QA2: \(event.type)") } catch { } } @@ -142,8 +142,8 @@ class BlockingQueueTest: XCTestCase { do { Thread.sleep(forTimeInterval: 0.5) let event = try queue.take() - local.append(event) - print("Took QA3: \(event)") + local.append(event.type) + print("Took QA3: \(event.type)") } catch { } } diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index 5b792fb3e..1abe94524 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -95,83 +95,77 @@ class InternalSplitClientStub: InternalSplitClient { return ["": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, execute action: @escaping SplitAction) {} + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) {} func track(trafficType: String, eventType: String) -> Bool { - return true + true } func track(trafficType: String, eventType: String, value: Double) -> Bool { - return true + true } func track(eventType: String) -> Bool { - return true + true } func track(eventType: String, value: Double) -> Bool { - return true + true } func track(trafficType: String, eventType: String, properties: [String: Any]?) -> Bool { - return true + true } func track(trafficType: String, eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return true + true } func track(eventType: String, properties: [String: Any]?) -> Bool { - return true + true } func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool { - return true + true } func setAttribute(name: String, value: Any) -> Bool { - return true + true } func getAttribute(name: String) -> Any? { - return nil + nil } func setAttributes(_ values: [String: Any]) -> Bool { - return true + true } func getAttributes() -> [String: Any]? { - return nil + nil } func removeAttribute(name: String) -> Bool { - return true + true } func clearAttributes() -> Bool { - return true + true } - func flush() { - } + func flush() {} - func destroy() { - } - - func destroy(completion: (() -> Void)?) { - } + func destroy() {} - func on(event: SplitEvent, executeTask: SplitEventTask) { + func destroy(completion: (() -> Void)?) {} - } + func on(event: SplitEvent, executeTask: SplitEventTask) {} private func createControlTreatmentsDictionary(splits: [String]) -> [String: T] where T: Any { var result = [String: T]() diff --git a/SplitTests/Fake/Service/ByKeyFacadeMock.swift b/SplitTests/Fake/Service/ByKeyFacadeMock.swift index 32ea6c63e..f56ccd71d 100644 --- a/SplitTests/Fake/Service/ByKeyFacadeMock.swift +++ b/SplitTests/Fake/Service/ByKeyFacadeMock.swift @@ -123,12 +123,16 @@ class ByKeyFacadeMock: ByKeyFacade { } var notifyMySegmentsUpdatedCalled = false - func notifyMySegmentsUpdated(forKey key: String) { + var updatedSegmentsMetadataForKey = [String : EventMetadata?]() + func notifyMySegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { + updatedSegmentsMetadataForKey[key] = metadata notifyMySegmentsUpdatedCalled = true } var notifyMyLargeSegmentsUpdatedCalled = false - func notifyMyLargeSegmentsUpdated(forKey key: String) { + var updatedLargeSegmentsMetadataForKey = [String : EventMetadata?]() + func notifyMyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { + updatedLargeSegmentsMetadataForKey[key] = metadata notifyMyLargeSegmentsUpdatedCalled = true } diff --git a/SplitTests/Fake/Service/ChangesCheckerMock.swift b/SplitTests/Fake/Service/ChangesCheckerMock.swift index 0b6b0f1e5..205da466d 100644 --- a/SplitTests/Fake/Service/ChangesCheckerMock.swift +++ b/SplitTests/Fake/Service/ChangesCheckerMock.swift @@ -11,23 +11,30 @@ import Foundation @testable import Split class MySegmentsChangesCheckerMock: MySegmentsChangesChecker { + var haveChanged = false + var diffSegments: [String] = [] + func mySegmentsHaveChanged(old: SegmentChange, new: SegmentChange) -> Bool { - return haveChanged + haveChanged } func mySegmentsHaveChanged(oldSegments old: [Segment], newSegments new: [Segment]) -> Bool { - return haveChanged + haveChanged } func mySegmentsHaveChanged(oldSegments old: [String], newSegments new: [String]) -> Bool { - return haveChanged + haveChanged + } + + func getSegmentsDiff(oldSegments: [Segment], newSegments: [Segment]) -> [String] { + diffSegments } } struct SplitsChangesCheckerMock: SplitsChangesChecker { var haveChanged = false func splitsHaveChanged(oldChangeNumber: Int64, newChangeNumber: Int64) -> Bool { - return haveChanged + haveChanged } } diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 94cbb8991..26e0d03f7 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -22,9 +22,13 @@ class SplitEventsManagerCoordinatorStub: SplitEventsManagerCoordinator { managers[key] = nil } - func register(event: SplitEvent, task: SplitEventTask) { + func register(event: SplitEvent, task: SplitEventActionTask) { } + + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) {} + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata?) {} var notifiedEvents = Set() func notifyInternalEvent(_ event: SplitInternalEvent) { diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index a19acdcb7..26453c685 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -83,80 +83,75 @@ class SplitClientStub: SplitClient { return ["feature": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} - func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { - } + func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) {} + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) {} + func track(trafficType: String, eventType: String) -> Bool { - return true + true } func track(trafficType: String, eventType: String, value: Double) -> Bool { - return true + true } func track(eventType: String) -> Bool { - return true + true } func track(eventType: String, value: Double) -> Bool { - return true + true } func track(trafficType: String, eventType: String, properties: [String:Any]?) -> Bool { - return true + true } func track(trafficType: String, eventType: String, value: Double, properties: [String:Any]?) -> Bool { - return true + true } func track(eventType: String, properties: [String:Any]?) -> Bool { - return true + true } func track(eventType: String, value: Double, properties: [String:Any]?) -> Bool { - return true + true } func setAttribute(name: String, value: Any) -> Bool { - return true + true } func getAttribute(name: String) -> Any? { - return nil + nil } func setAttributes(_ values: [String: Any]) -> Bool { - return true + true } func getAttributes() -> [String: Any]? { - return nil + nil } func removeAttribute(name: String) -> Bool { - return true + true } func clearAttributes() -> Bool { - return true + true } - func flush() { - } + func flush() {} - func destroy() { - } + func destroy() {} - func destroy(completion: (() -> Void)?) { - } + func destroy(completion: (() -> Void)?) {} } diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index 1132148df..256258dac 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -31,6 +31,8 @@ class SplitEventsManagerMock: SplitEventsManager { var isSdkUpdatedFired = false var isSdkReadyChecked = false + + var metadata: EventMetadata? func notifyInternalEvent(_ event:SplitInternalEvent) { switch event { @@ -52,9 +54,13 @@ class SplitEventsManagerMock: SplitEventsManager { } } - var registeredEvents = [SplitEvent: SplitEventTask]() - func register(event: SplitEvent, task: SplitEventTask) { - registeredEvents[event] = task + var registeredEvents = [SplitEvent: SplitEventActionTask]() + func register(event: SplitEvent, task: SplitEventActionTask) { + register(event: SplitEventWithMetadata(type: event), task: task) + } + + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) { + registeredEvents[event.type] = task } func start() { @@ -79,4 +85,9 @@ class SplitEventsManagerMock: SplitEventsManager { return true } } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata?) { + self.metadata = metadata + notifyInternalEvent(event) + } } diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 1d7685dc3..df390d772 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -15,11 +15,19 @@ class SplitEventsManagerStub: SplitEventsManager { var splitsKilledEventFiredCount = 0 var splitsUpdatedEventFiredCount = 0 var mySegmentsLoadedEventFiredCount = 0 + var metadata: EventMetadata? var mySegmentsLoadedEventExp: XCTestExpectation? var startCalled = false var stopCalled = false func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: EventMetadata? = nil) { + + self.metadata = metadata + switch event { case .mySegmentsLoadedFromCache: mySegmentsLoadedEventFiredCount+=1 @@ -39,8 +47,12 @@ class SplitEventsManagerStub: SplitEventsManager { } } - var registeredEvents = [SplitEvent: SplitEventTask]() - func register(event: SplitEvent, task: SplitEventTask) { + var registeredEvents = [SplitEventWithMetadata: SplitEventActionTask]() + func register(event: SplitEvent, task: SplitEventActionTask) { + register(event: SplitEventWithMetadata(type: event), task: task) + } + + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) { registeredEvents[event] = task } diff --git a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift index 8e01f5f8e..d95510baf 100644 --- a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift +++ b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift @@ -9,7 +9,7 @@ import Foundation @testable import Split class FeatureFlagsSynchronizerStub: FeatureFlagsSynchronizer { - var loadCalled = false + var loadCalled = false func load() { loadCalled = true } @@ -35,14 +35,25 @@ class FeatureFlagsSynchronizerStub: FeatureFlagsSynchronizer { } var notifyKilledCalled = false - func notifyKilled() { + var killedFlag = "" + func notifyKilled(flag: String) { + killedFlag = flag notifyKilledCalled = true } var notifyUpdatedCalled = false - func notifyUpdated() { + var updatedFlags: [String] = [] + func notifyUpdated(flags: [String]) { + updatedFlags = flags notifyUpdatedCalled = true } + + var notifyUpdatedSegmentsCalled = false + var updatedSegments: [String] = [] + func notifyUpdated(ruleBasedSegments: [String]) { + updatedSegments = ruleBasedSegments + notifyUpdatedSegmentsCalled = true + } var pauseCalled = false func pause() { diff --git a/SplitTests/Fake/Streaming/SynchronizerSpy.swift b/SplitTests/Fake/Streaming/SynchronizerSpy.swift index fb6e59353..edead3635 100644 --- a/SplitTests/Fake/Streaming/SynchronizerSpy.swift +++ b/SplitTests/Fake/Streaming/SynchronizerSpy.swift @@ -195,24 +195,39 @@ class SynchronizerSpy: Synchronizer { splitSynchronizer.resume() } - func notifySegmentsUpdated(forKey key: String) { + var updatedSegmentsMetadataForKey = [String : EventMetadata]() + func notifySegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { notifyMySegmentsUpdatedCalled = true - splitSynchronizer.notifySegmentsUpdated(forKey: key) + updatedSegmentsMetadataForKey[key] = metadata + splitSynchronizer.notifySegmentsUpdated(forKey: key, metadata: metadata) } - func notifyLargeSegmentsUpdated(forKey key: String) { + var updatedLargeSegmentsMetadataForKey = [String : EventMetadata]() + func notifyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { + updatedLargeSegmentsMetadataForKey[key] = metadata notifyMyLargeSegmentsUpdatedCalled = true - splitSynchronizer.notifyLargeSegmentsUpdated(forKey: key) + splitSynchronizer.notifyLargeSegmentsUpdated(forKey: key, metadata: metadata) } var notifyFeatureFlagsUpdatedCalled = false - func notifyFeatureFlagsUpdated() { + var updatedFlags: [String] = [] + func notifyFeatureFlagsUpdated(flags: [String]) { + updatedFlags = flags notifyFeatureFlagsUpdatedCalled = true } + + var notifyRuleBasedSegmentsUpdatedCalled = false + var ruleBasedSegmentsUpdated: [String] = [] + func notifyRuleBasedSegmentsUpdated(segments: [String]) { + ruleBasedSegmentsUpdated = segments + notifyRuleBasedSegmentsUpdatedCalled = true + } - func notifySplitKilled() { + var killedFlag = "" + func notifySplitKilled(flag: String) { notifySplitKilledCalled = true - splitSynchronizer.notifySplitKilled() + killedFlag = flag + splitSynchronizer.notifySplitKilled(flag: flag) } func start(forKey key: Key) { diff --git a/SplitTests/Fake/Streaming/SynchronizerStub.swift b/SplitTests/Fake/Streaming/SynchronizerStub.swift index 4bcdba666..11062226a 100644 --- a/SplitTests/Fake/Streaming/SynchronizerStub.swift +++ b/SplitTests/Fake/Streaming/SynchronizerStub.swift @@ -16,6 +16,7 @@ struct ForceMySegmentsParams { } class SynchronizerStub: Synchronizer { + var disableSdkCalled = false var disableEventsCalled = false var disableTelemetryCalled = false @@ -119,16 +120,20 @@ class SynchronizerStub: Synchronizer { } var notifySegmentsUpdatedForKeyCalled = [String: Bool]() - func notifySegmentsUpdated(forKey key: String) { + var updatedSegmentsMetadataForKey = [String: EventMetadata?]() + func notifySegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { notifySegmentsUpdatedForKeyCalled[key] = true + updatedLargeSegmentsMetadataForKey[key] = metadata if let exp = notifyMySegmentsUpdatedExp[key] { exp.fulfill() } } var notifyLargeSegmentsUpdatedForKeyCalled = [String: Bool]() - func notifyLargeSegmentsUpdated(forKey key: String) { + var updatedLargeSegmentsMetadataForKey = [String: EventMetadata?]() + func notifyLargeSegmentsUpdated(forKey key: String, metadata: EventMetadata? = nil) { notifyLargeSegmentsUpdatedForKeyCalled[key] = true + updatedLargeSegmentsMetadataForKey[key] = metadata if let exp = notifyMyLargeSegmentsUpdatedExp[key] { exp.fulfill() } @@ -237,11 +242,23 @@ class SynchronizerStub: Synchronizer { } var notifyFeatureFlagsUpdatedCalled = true - func notifyFeatureFlagsUpdated() { + var updatedFlags: [String] = [] + func notifyFeatureFlagsUpdated(flags: [String]) { + updatedFlags = flags notifyFeatureFlagsUpdatedCalled = true } + + var notifyRuleBasedSegmentsUpdatedCalled = true + var updatedRuleBasedSegments: [String] = [] + func notifyRuleBasedSegmentsUpdated(segments: [String]) { + updatedRuleBasedSegments = segments + notifyRuleBasedSegmentsUpdatedCalled = true + } + - func notifySplitKilled() { + var killedFlag = "" + func notifySplitKilled(flag: String) { + killedFlag = flag notifySplitKilledCalled = true } diff --git a/SplitTests/Helpers/IntegrationHelper.swift b/SplitTests/Helpers/IntegrationHelper.swift index b75db36cf..e5e3a45d9 100644 --- a/SplitTests/Helpers/IntegrationHelper.swift +++ b/SplitTests/Helpers/IntegrationHelper.swift @@ -227,6 +227,8 @@ class IntegrationHelper { return "myLargeSegmentsUpdated" case .myLargeSegmentsLoadedFromCache: return "myLargeSegmentsLoadedFromCache" + case .ruleBasedSegmentsUpdated: + return "ruleBasedSegmentsUpdated" } } diff --git a/SplitTests/Helpers/TestingHelper.swift b/SplitTests/Helpers/TestingHelper.swift index c908d1cd7..59eaef8c5 100644 --- a/SplitTests/Helpers/TestingHelper.swift +++ b/SplitTests/Helpers/TestingHelper.swift @@ -214,7 +214,8 @@ struct TestingHelper { static func buildSegmentsChange(count: Int64 = 5, msAscOrder: Bool = true, mlsAscOrder: Bool = true, - segmentsChanged: Bool = false) -> [AllSegmentsChange] { + segmentsChanged: [String] = []) -> [AllSegmentsChange] { + // Eventualy cn will be greater than the first let baseCn: Int64 = 100 let lastMsCn = baseCn * count + 1 @@ -233,10 +234,13 @@ struct TestingHelper { res.append(newAllSegmentsChange(ms: msSeg, msCn: msC, mls: mlsSeg, mlsCn: mlsC)) } - if segmentsChanged { - msSeg.append("s3") - mlsSeg.append("sl3") + if !segmentsChanged.isEmpty { + segmentsChanged.forEach { segment in + msSeg.append(segment) + } } + msSeg.append("s3") + mlsSeg.append("sl3") res.append(newAllSegmentsChange(ms: msSeg, msCn: lastMsCn, mls: mlsSeg, mlsCn: lastMlsCn)) @@ -261,7 +265,7 @@ struct TestingHelper { static func segmentsSyncResult(_ result: Bool = true, msCn: Int64 = 300, mlsCn: Int64 = 400, - msUpd: Bool = true, mlsUpd: Bool = true) -> SegmentsSyncResult { + msUpd: [String] = ["SegmentTest1"], mlsUpd: [String] = ["LargeSegmentTest1"]) -> SegmentsSyncResult { return SegmentsSyncResult(success: result, msChangeNumber: msCn, mlsChangeNumber: mlsCn, msUpdated: msUpd, mlsUpdated: mlsUpd) diff --git a/SplitTests/Integration/Api/RuleBasedSegmentsIntegrationTest.swift b/SplitTests/Integration/Api/RuleBasedSegmentsIntegrationTest.swift index 7a103fa2e..54d29c320 100644 --- a/SplitTests/Integration/Api/RuleBasedSegmentsIntegrationTest.swift +++ b/SplitTests/Integration/Api/RuleBasedSegmentsIntegrationTest.swift @@ -260,16 +260,24 @@ class RuleBasedSegmentsIntegrationTest: XCTestCase { expectedContents: String ) { let sdkUpdateExpectation = XCTestExpectation(description: "SDK_UPDATE received") + let sdkUpdateExpectationWithMetadata = XCTestExpectation(description: "SDK_UPDATE received with metadata") var sdkUpdatedTriggered = false - client.on(event: SplitEvent.sdkUpdated) { + client.on(event: .sdkUpdated) { sdkUpdatedTriggered = true sdkUpdateExpectation.fulfill() } + + // Test metadata + client.on(event: .sdkUpdated) { metadata in + XCTAssertEqual(metadata!.type, .RULE_BASED_SEGMENTS_UPDATED) + XCTAssertEqual(metadata!.data, ["rbs_test"]) + sdkUpdateExpectationWithMetadata.fulfill() + } streamingBinding?.push(message: "id:a62260de-13bb-11eb-adc1-0242ac120002") // send msg to confirm streaming connection ok streamingBinding?.push(message: change) - wait(for: [sdkUpdateExpectation], timeout: 10) + wait(for: [sdkUpdateExpectation, sdkUpdateExpectationWithMetadata], timeout: 10) let containsExpectedContents = testDatabase!.ruleBasedSegmentDao.getAll().contains { $0.name == expectedContents diff --git a/SplitTests/Integration/Sync/SplitSdkUpdatePollingTest.swift b/SplitTests/Integration/Sync/SplitSdkUpdatePollingTest.swift index 9a37c6761..207299715 100644 --- a/SplitTests/Integration/Sync/SplitSdkUpdatePollingTest.swift +++ b/SplitTests/Integration/Sync/SplitSdkUpdatePollingTest.swift @@ -29,87 +29,21 @@ class SplitSdkUpdatePollingTest: XCTestCase { ] let impExp = XCTestExpectation(description: "impressions") - var impHit: [ImpressionsTest]? override func setUp() { let session = HttpSessionMock() - let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), - streamingHandler: buildStreamingHandler()) + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), streamingHandler: buildStreamingHandler()) httpClient = DefaultHttpClient(session: session, requestManager: reqManager) } - - private func buildTestDispatcher() -> HttpClientTestDispatcher { - - let respData = responseSplitChanges() - var responses = [TestDispatcherResponse]() - for data in respData { - let rData = TargetingRulesChange(featureFlags: data, ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1)) - responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson(rData).utf8))) - } - - return { request in - if request.isSplitEndpoint() { - let index = self.getAndIncrement() - if index < self.spExp.count { - if index > 0 { - self.spExp[index - 1].fulfill() - } - return responses[index] - } else if index == self.spExp.count { - self.spExp[index - 1].fulfill() - } - return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges(since: 99999999, till: 99999999).utf8)) - } - - if request.isMySegmentsEndpoint() { - self.mySegmentsHits+=1 - let hit = self.mySegmentsHits - var json = IntegrationHelper.emptyMySegments - if hit > 2 { - var mySegments = [String]() - for i in 1...hit { - mySegments.append("segment\(i)") - } - - json = IntegrationHelper.buildSegments(regular: mySegments) - return TestDispatcherResponse(code: 200, data: Data(json.utf8)) - } - return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) - } - - if request.isAuthEndpoint() { - return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.dummySseResponse().utf8)) - } - - if request.isImpressionsEndpoint() { - self.impHit = try? TestUtils.impressionsFromHit(request: request) - self.impExp.fulfill() - return TestDispatcherResponse(code: 200) - } - - if request.isEventsEndpoint() { - return TestDispatcherResponse(code: 200) - } - - return TestDispatcherResponse(code: 500) - } - } - - private func buildStreamingHandler() -> TestStreamResponseBindingHandler { - return { request in - self.streamingBinding = TestStreamResponseBinding.createFor(request: request, code: 200) - return self.streamingBinding! - } - } - - // MARK: Test + // MARK: Tests func testSdkReadyOnly() throws { let apiKey = IntegrationHelper.dummyApiKey let trafficType = "client" let sdkReady = XCTestExpectation(description: "SDK READY Expectation") + let sdkUpdate = XCTestExpectation(description: "SDK UPDATE Expectation") let splitConfig: SplitClientConfig = SplitClientConfig() splitConfig.segmentsRefreshRate = 99999 @@ -139,6 +73,7 @@ class SplitSdkUpdatePollingTest: XCTestCase { client.on(event: SplitEvent.sdkUpdated) { sdkUpdatedFired = true + sdkUpdate.fulfill() } wait(for: [sdkReady], timeout: 30) @@ -207,7 +142,46 @@ class SplitSdkUpdatePollingTest: XCTestCase { }) semaphore.wait() } + + func testSdkUpdateSplitsWithMetadata() throws { + let apiKey = IntegrationHelper.dummyApiKey + let trafficType = "client" + + let sdkUpdateWithMetadata = XCTestExpectation(description: "SDK Update With Metadata Expectation") + + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.segmentsRefreshRate = 99999 + splitConfig.featuresRefreshRate = 2 + splitConfig.impressionRefreshRate = 99999 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.trafficType = trafficType + splitConfig.streamingEnabled = false + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() + let key: Key = Key(matchingKey: kMatchingKey, bucketingKey: nil) + let builder = DefaultSplitFactoryBuilder() + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesTest")) + _ = builder.setHttpClient(httpClient) + factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + + let client = factory!.client + + client.on(event: .sdkUpdated) { metadata in + XCTAssertEqual(metadata?.type, .FLAGS_UPDATED) + XCTAssertEqual(metadata?.data, ["test_feature"]) + sdkUpdateWithMetadata.fulfill() + } + + wait(for: [sdkUpdateWithMetadata], timeout: 30) + + let semaphore = DispatchSemaphore(value: 0) + client.destroy(completion: { + _ = semaphore.signal() + }) + semaphore.wait() + } + func testSdkUpdateMySegments() throws { let apiKey = IntegrationHelper.dummyApiKey let trafficType = "client" @@ -263,6 +237,115 @@ class SplitSdkUpdatePollingTest: XCTestCase { semaphore.wait() } + func testSdkUpdateMySegmentsWithMetadata() throws { + let apiKey = IntegrationHelper.dummyApiKey + let trafficType = "client" + + let sdkReady = XCTestExpectation(description: "SDK READY Expectation") + let sdkUpdateWithMetadata = XCTestExpectation(description: "SDK Update With Metadata Expectation") + + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.segmentsRefreshRate = 2 + splitConfig.featuresRefreshRate = 999999 + splitConfig.impressionRefreshRate = 999999 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.trafficType = trafficType + splitConfig.streamingEnabled = false + splitConfig.logLevel = .verbose + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() + + let key: Key = Key(matchingKey: kMatchingKey, bucketingKey: nil) + let builder = DefaultSplitFactoryBuilder() + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesTest")) + _ = builder.setHttpClient(httpClient) + factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + let client = factory!.client + + client.on(event: .sdkUpdated) { metadata in + if metadata?.type == .SEGMENTS_UPDATED { + XCTAssertEqual(metadata?.data, ["segment1", "segment2", "segment3"]) + sdkUpdateWithMetadata.fulfill() + } + } + + wait(for: [sdkUpdateWithMetadata], timeout: 40) + + // wait for sdk update + ThreadUtils.delay(seconds: 1.0) + + let semaphore = DispatchSemaphore(value: 0) + client.destroy(completion: { + _ = semaphore.signal() + }) + semaphore.wait() + } + + //MARK: Testing Helpers + private func buildTestDispatcher() -> HttpClientTestDispatcher { + + let respData = responseSplitChanges() + var responses = [TestDispatcherResponse]() + for data in respData { + let rData = TargetingRulesChange(featureFlags: data, ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1)) + responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson(rData).utf8))) + } + + return { request in + if request.isSplitEndpoint() { + let index = self.getAndIncrement() + if index < self.spExp.count { + if index > 0 { + self.spExp[index - 1].fulfill() + } + return responses[index] + } else if index == self.spExp.count { + self.spExp[index - 1].fulfill() + } + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges(since: 99999999, till: 99999999).utf8)) + } + + if request.isMySegmentsEndpoint() { + self.mySegmentsHits+=1 + let hit = self.mySegmentsHits + var json = IntegrationHelper.emptyMySegments + if hit > 2 { + var mySegments = [String]() + for i in 1...hit { + mySegments.append("segment\(i)") + } + + json = IntegrationHelper.buildSegments(regular: mySegments) + return TestDispatcherResponse(code: 200, data: Data(json.utf8)) + } + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + + if request.isAuthEndpoint() { + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.dummySseResponse().utf8)) + } + + if request.isImpressionsEndpoint() { + self.impHit = try? TestUtils.impressionsFromHit(request: request) + self.impExp.fulfill() + return TestDispatcherResponse(code: 200) + } + + if request.isEventsEndpoint() { + return TestDispatcherResponse(code: 200) + } + + return TestDispatcherResponse(code: 500) + } + } + + private func buildStreamingHandler() -> TestStreamResponseBindingHandler { + return { request in + self.streamingBinding = TestStreamResponseBinding.createFor(request: request, code: 200) + return self.streamingBinding! + } + } + private func responseSplitChanges() -> [SplitChange] { var changes = [SplitChange]() diff --git a/SplitTests/Integration/streaming/StreamingMySegmentsSyncTest.swift b/SplitTests/Integration/streaming/StreamingMySegmentsSyncTest.swift index 67ac38424..c3c6da64e 100644 --- a/SplitTests/Integration/streaming/StreamingMySegmentsSyncTest.swift +++ b/SplitTests/Integration/streaming/StreamingMySegmentsSyncTest.swift @@ -70,7 +70,7 @@ class StreamingMySegmentsSyncTest: XCTestCase { splitConfig.impressionRefreshRate = 999999 splitConfig.sdkReadyTimeOut = 60000 splitConfig.eventsPushRate = 999999 - splitConfig.logLevel = .verbose + splitConfig.logLevel = .error let key: Key = Key(matchingKey: userKey) let builder = DefaultSplitFactoryBuilder() @@ -93,6 +93,22 @@ class StreamingMySegmentsSyncTest: XCTestCase { client.on(event: SplitEvent.sdkReadyTimedOut) { sdkReadyExpectation.fulfill() } + + var segmentsMetadataExecuted = false + var largeSegmentsMetadataExecuted = false + + // Test with metadata + client.on(event: .sdkUpdated) { metadata in + if metadata!.type == .SEGMENTS_UPDATED { + XCTAssert(metadata!.data == ["new_segment"]) + segmentsMetadataExecuted = true + } else if metadata!.type == .LARGE_SEGMENTS_UPDATED { + if let dataArr = metadata?.data as? [String] { + XCTAssert(dataArr.allSatisfy { ["new_large_segment", "ls1", "ls2"].contains($0) }) + largeSegmentsMetadataExecuted = true + } + } + } wait(for: [sdkReadyExpectation, sseExp], timeout: expTimeout) @@ -127,6 +143,12 @@ class StreamingMySegmentsSyncTest: XCTestCase { XCTAssertEqual(inResult, treatmentFirst) XCTAssertEqual("on", treatmentSec) XCTAssertEqual("on", treatmentOld) + + if type == .mySegmentsUpdate { + XCTAssertTrue(segmentsMetadataExecuted) + } else if type == .myLargeSegmentsUpdate { + XCTAssertTrue(largeSegmentsMetadataExecuted) + } let semaphore = DispatchSemaphore(value: 0) client.destroy(completion: { diff --git a/SplitTests/Integration/streaming/StreamingSplitKillTest.swift b/SplitTests/Integration/streaming/StreamingSplitKillTest.swift index 26e405131..50e25d1b4 100644 --- a/SplitTests/Integration/streaming/StreamingSplitKillTest.swift +++ b/SplitTests/Integration/streaming/StreamingSplitKillTest.swift @@ -27,17 +27,18 @@ class StreamingSplitKillTest: XCTestCase { var exp2: XCTestExpectation! var exp3: XCTestExpectation! var exp4: XCTestExpectation! + var exp5: XCTestExpectation! override func setUp() { expIndex = 1 let session = HttpSessionMock() - let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), - streamingHandler: buildStreamingHandler()) + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), streamingHandler: buildStreamingHandler()) httpClient = DefaultHttpClient(session: session, requestManager: reqManager) loadChanges() } - func testSplitKill() { + // MARK: Tests + func testSplitKill() throws { let splitConfig: SplitClientConfig = SplitClientConfig() splitConfig.featuresRefreshRate = 9999 splitConfig.segmentsRefreshRate = 9999 @@ -55,7 +56,7 @@ class StreamingSplitKillTest: XCTestCase { .setConfig(splitConfig).build()! let client = factory.client - let expTimeout: TimeInterval = 5 + let expTimeout: TimeInterval = 5 let sdkReadyExpectation = XCTestExpectation(description: "SDK READY Expectation") exp1 = XCTestExpectation(description: "Exp1") @@ -63,11 +64,11 @@ class StreamingSplitKillTest: XCTestCase { exp3 = XCTestExpectation(description: "Exp3") exp4 = XCTestExpectation(description: "Exp4") - client.on(event: SplitEvent.sdkReady) { + client.on(event: .sdkReady) { sdkReadyExpectation.fulfill() } - client.on(event: SplitEvent.sdkReadyTimedOut) { + client.on(event: .sdkReadyTimedOut) { IntegrationHelper.tlog("TIMEOUT") } @@ -80,28 +81,25 @@ class StreamingSplitKillTest: XCTestCase { let splitName = "workm" let treatmentReady = client.getTreatment(splitName) - streamingBinding?.push(message: - StreamingIntegrationHelper.splitKillMessagge(splitName: splitName, defaultTreatment: "conta", - timestamp: numbers[splitsChangesHits], - changeNumber: numbers[splitsChangesHits])) + streamingBinding?.push(message: StreamingIntegrationHelper.splitKillMessagge(splitName: splitName, defaultTreatment: "conta", + timestamp: numbers[splitsChangesHits], + changeNumber: numbers[splitsChangesHits])) wait(for: [exp2], timeout: expTimeout) waitForUpdate(secs: 1) let treatmentKill = client.getTreatment(splitName) - streamingBinding?.push(message: - StreamingIntegrationHelper.splitUpdateMessage(timestamp: numbers[splitsChangesHits], - changeNumber: numbers[splitsChangesHits])) + streamingBinding?.push(message: StreamingIntegrationHelper.splitUpdateMessage(timestamp: numbers[splitsChangesHits], + changeNumber: numbers[splitsChangesHits])) wait(for: [exp3], timeout: expTimeout) waitForUpdate(secs: 1) let treatmentNoKill = client.getTreatment(splitName) - streamingBinding?.push(message: - StreamingIntegrationHelper.splitKillMessagge(splitName: splitName, defaultTreatment: "conta", - timestamp: numbers[0], - changeNumber: numbers[0])) + streamingBinding?.push(message: StreamingIntegrationHelper.splitKillMessagge(splitName: splitName, defaultTreatment: "conta", + timestamp: numbers[0], + changeNumber: numbers[0])) ThreadUtils.delay(seconds: 2.0) // The server should not be hit here let treatmentOldKill = client.getTreatment(splitName) @@ -118,6 +116,60 @@ class StreamingSplitKillTest: XCTestCase { semaphore.wait() } + func testSplitKillWithMetadata() throws { + + // Setup + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.featuresRefreshRate = 9999 + splitConfig.segmentsRefreshRate = 9999 + splitConfig.impressionRefreshRate = 999999 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.eventsPushRate = 999999 + + let key: Key = Key(matchingKey: userKey) + let builder = DefaultSplitFactoryBuilder() + _ = builder.setHttpClient(httpClient) + _ = builder.setReachabilityChecker(ReachabilityMock()) + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "test")) + let factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build()! + let client = factory.client + let expTimeout: TimeInterval = 5 + + let sdkReadyExpectation = XCTestExpectation(description: "SDK READY Expectation") + exp1 = XCTestExpectation(description: "Streaming notification") + exp2 = XCTestExpectation(description: "Push notification") + exp5 = XCTestExpectation(description: "Wait for killed metadata event") + + client.on(event: .sdkReady) { sdkReadyExpectation.fulfill() } + + // Set listener + client.on(event: .sdkUpdated) { [weak self] metadata in + if metadata?.type == .FLAGS_KILLED { + XCTAssertEqual(metadata?.data, ["workm"]) + self?.exp5.fulfill() + } + } + + // Simulate Kill + wait(for: [sdkReadyExpectation, sseConnExp], timeout: expTimeout) + streamingBinding?.push(message: ":keepalive") // send keep alive to confirm streaming connection ok + wait(for: [exp1], timeout: expTimeout) + waitForUpdate(secs: 1) + streamingBinding?.push(message: StreamingIntegrationHelper.splitKillMessagge(splitName: "workm", defaultTreatment: "conta", + timestamp: numbers[splitsChangesHits], + changeNumber: numbers[splitsChangesHits])) + wait(for: [exp5, exp2], timeout: expTimeout) + waitForUpdate(secs: 1) + + // Cleanup + let semaphore = DispatchSemaphore(value: 0) + client.destroy(completion: { + _ = semaphore.signal() + }) + semaphore.wait() + } + + //MARK: Testing Helpers private func getChanges(for hitNumber: Int) -> Data { if hitNumber < 4 { return Data(self.changes[hitNumber].utf8) diff --git a/SplitTests/Service/MySegments/SegmentsSyncHelperTests.swift b/SplitTests/Service/MySegments/SegmentsSyncHelperTests.swift index 395e88c25..ab24d7fdf 100644 --- a/SplitTests/Service/MySegments/SegmentsSyncHelperTests.swift +++ b/SplitTests/Service/MySegments/SegmentsSyncHelperTests.swift @@ -40,18 +40,19 @@ class SegmentsSyncHelperTests: XCTestCase { } func testCdnByPassNoTillNoChange() throws { - try cdnByPassNoTill(segmentsChanged: false) + try cdnByPassNoTill(segmentsChanged: []) } func testCdnByPassNoTillChange() throws { - try cdnByPassNoTill(segmentsChanged: true) + try cdnByPassNoTill(segmentsChanged: ["Segment1"]) } - func cdnByPassNoTill(segmentsChanged: Bool) throws { + func cdnByPassNoTill(segmentsChanged: [String]) throws { let goalCn: Int64 = 300 mySegmentsStorage.changeNumber = 200 myLargeSegmentsStorage.changeNumber = 200 - changeChecker.haveChanged = segmentsChanged + changeChecker.haveChanged = !segmentsChanged.isEmpty + changeChecker.diffSegments = segmentsChanged let exp = XCTestExpectation() mySegmentsFetcher.countExp = exp diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 11d8603ae..ad666b4d1 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -262,6 +262,25 @@ class SplitEventsManagerTest: XCTestCase { eventManager.stop() } + + func testEventWithMetadata() { + + let taskExp = XCTestExpectation() + + // Build Task + let metadata = EventMetadata(type: .FLAGS_KILLED, data: ["TEST_FLAG"]) + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(metadata.type, handlerMetadata?.type) + XCTAssertEqual(metadata.data, ["TEST_FLAG"]) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 3.0) + } func testSdkReadyFromCacheWithoutFlagCachedValues() { @@ -296,6 +315,26 @@ class SplitEventsManagerTest: XCTestCase { eventManager.stop() } + func testSplitEventActionTaskMetadata() { + + // Dummy event with metadata + let metadataTypeToCheck: EventMetadataType = .FLAGS_KILLED + let metadataDataToCheck: [String] = ["Test-flag-42"] + let dummyMetadata = EventMetadata(type: metadataTypeToCheck, data: metadataDataToCheck) + + // This will be the task's "run()" + let action: SplitActionWithMetadata = { metadata in + XCTAssertEqual(metadataTypeToCheck, metadata!.type) + XCTAssertEqual(metadataDataToCheck,metadata!.data) + } + + // SUT + let SUT = TestTask(exp: nil, action: action, metadata: dummyMetadata) + + SUT.run(dummyMetadata) + XCTAssertTrue(SUT.taskTriggered) + } + // MARK: Helpers func currentTimestamp() -> Int { return Int(Date().unixTimestamp()) @@ -306,29 +345,21 @@ class SplitEventsManagerTest: XCTestCase { } } -class TestTask: SplitEventTask { - - var event: SplitEvent = .sdkReady - - var runInBackground: Bool = false - - var queue: DispatchQueue? +class TestTask: SplitEventActionTask { var taskTriggered = false let label: String var exp: XCTestExpectation? - init(exp: XCTestExpectation?, label: String = "") { + init(exp: XCTestExpectation?, label: String = "", action: SplitActionWithMetadata? = nil, metadata: EventMetadata? = nil) { self.exp = exp self.label = label + super.init(action: action ?? { _ in }, event: .sdkReady, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey)) } - - func takeQueue() -> DispatchQueue? { - return nil - } - - func run() { + + override func run(_ metadata: EventMetadata?) { print("run: \(self.label)") taskTriggered = true + super.run(metadata) if let exp = self.exp { exp.fulfill() } diff --git a/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift b/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift index efcfece3a..142f0d890 100644 --- a/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift +++ b/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift @@ -258,6 +258,13 @@ class FeatureFlagsSynchronizerTest: XCTestCase { XCTAssertTrue(persistentSplitsStorage.clearCalled) XCTAssertEqual(1, broadcasterChannel.pushedEvents.filter { $0 == .splitLoadedFromCache }.count) } + + func testMetadataOnFeatureFlagsSync() { + synchronizer.notifyUpdated(flags: ["Pepe2"]) + + XCTAssertEqual(eventsManager.metadata?.type, .FLAGS_UPDATED) + XCTAssertEqual(eventsManager.metadata?.data, ["Pepe2"]) + } func testSynchronizeSplitsWithChangeNumber() { diff --git a/SplitTests/Streaming/SynchronizerTest.swift b/SplitTests/Streaming/SynchronizerTest.swift index 63f65ca3b..9b1b4253a 100644 --- a/SplitTests/Streaming/SynchronizerTest.swift +++ b/SplitTests/Streaming/SynchronizerTest.swift @@ -223,6 +223,20 @@ class SynchronizerTest: XCTestCase { XCTAssertTrue(byKeyApiFacade.startSyncForKeyCalled[key] ?? false) } + + func testMetadataOnSegmentsByKeySync() { + let metadata = EventMetadata(type: .FLAGS_UPDATED, data: ["Carlos", "Juancho"]) + synchronizer.notifySegmentsUpdated(forKey: "4", metadata: metadata) + + XCTAssertEqual(byKeyApiFacade.updatedSegmentsMetadataForKey["4"], metadata) + } + + func testMetadataOnLargeSegmentsByKeySync() { + let metadata = EventMetadata(type: .FLAGS_UPDATED, data: ["Lagarto", "Juancho"]) + synchronizer.notifySegmentsUpdated(forKey: "5", metadata: metadata) + + XCTAssertEqual(byKeyApiFacade.updatedSegmentsMetadataForKey["5"], metadata) + } func testFlush() {