diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 2e0582f60..4455e26ba 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -353,6 +353,8 @@ 59FB7C35220329B900ECC96A /* SplitFactoryBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */; }; 59FB7C3C2203795F00ECC96A /* LocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */; }; 59FB7C3E22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */; }; + 5B91B8362DD84276000510F0 /* SplitMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B91B8352DD84272000510F0 /* SplitMetadata.swift */; }; + 5B91B8372DD84276000510F0 /* SplitMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B91B8352DD84272000510F0 /* SplitMetadata.swift */; }; 9500D9922C56F9BA00383593 /* HostDomainFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9500D9912C56F9BA00383593 /* HostDomainFilterTests.swift */; }; 9500D9A92C59297400383593 /* HostDomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9500D9A82C59297400383593 /* HostDomainFilter.swift */; }; 9500D9AA2C59382000383593 /* HostDomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9500D9A82C59297400383593 /* HostDomainFilter.swift */; }; @@ -1494,6 +1496,7 @@ 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFactoryBuilderTests.swift; sourceTree = "<group>"; }; 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalhostSplitsParser.swift; sourceTree = "<group>"; }; 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceDelimitedLocalhostSplitsParser.swift; sourceTree = "<group>"; }; + 5B91B8352DD84272000510F0 /* SplitMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitMetadata.swift; sourceTree = "<group>"; }; 9500D9912C56F9BA00383593 /* HostDomainFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostDomainFilterTests.swift; sourceTree = "<group>"; }; 9500D9A82C59297400383593 /* HostDomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostDomainFilter.swift; sourceTree = "<group>"; }; 9500D9AC2C5A918300383593 /* split_cache_v5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = split_cache_v5.xcdatamodel; sourceTree = "<group>"; }; @@ -2296,6 +2299,7 @@ isa = PBXGroup; children = ( 3B6DEEC420EA6AE20067435E /* SplitEvent.swift */, + 5B91B8352DD84272000510F0 /* SplitMetadata.swift */, 3B6DEEC520EA6AE20067435E /* SplitEventsManager.swift */, 9530FD7927F24306005027AA /* EventsManagerCoordinator.swift */, 3B6DEEC620EA6AE20067435E /* SplitEventTask.swift */, @@ -3954,6 +3958,7 @@ 957E674C25E48753006F5B19 /* Bundle+Finder.swift in Sources */, 95F3F09525AE1F3800084AF8 /* ServiceConstants.swift in Sources */, 95CED0362B459A10005E3C34 /* LocalhostSynchronizer.swift in Sources */, + 5B91B8372DD84276000510F0 /* SplitMetadata.swift in Sources */, 9505682426836B20001D7B10 /* ImpressionsCountRecorder.swift in Sources */, 3B6DEF3020EA6AE50067435E /* Evaluator.swift in Sources */, 598EDE78224BB3E9005D4762 /* InternalSplitClient.swift in Sources */, @@ -4875,6 +4880,7 @@ 95B02D9828D0BDC30030EC8B /* RestClient+UniqueKeys.swift in Sources */, 95B02D9928D0BDC30030EC8B /* RestClient+ImpressionsCount.swift in Sources */, 95B02D9A28D0BDC30030EC8B /* RestClient+SplitChanges.swift in Sources */, + 5B91B8362DD84276000510F0 /* SplitMetadata.swift in Sources */, 95B02D9B28D0BDC30030EC8B /* DataResult.swift in Sources */, 958AD2132CA457C100E3DD43 /* RetryableSegmentsSyncWorker.swift in Sources */, 9566744729F03662001B4FA5 /* DbCipher.swift in Sources */, diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..95526ae67 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -55,49 +55,45 @@ public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClie } } -// MARK: Events +// MARK: Events Listeners extension DefaultSplitClient { + + 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: SplitEventWithMetadata(type: event, metadata: nil), task: task) + } public func on(event: SplitEvent, execute action: @escaping SplitAction) { on(event: event, runInBackground: false, queue: nil, execute: action) } - public func on(event: SplitEvent, runInBackground: Bool, - execute action: @escaping SplitAction) { + public func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { on(event: event, runInBackground: runInBackground, queue: nil, execute: action) } - public func on(event: SplitEvent, - queue: DispatchQueue, execute action: @escaping SplitAction) { + public func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { on(event: event, runInBackground: true, queue: queue, execute: action) } - private func on(event: SplitEvent, - runInBackground: Bool, - queue: DispatchQueue?, - execute action: @escaping SplitAction) { - - guard let factory = clientManager?.splitFactory else { - return - } - - let task = SplitEventActionTask(action: action, event: event, - runInBackground: runInBackground, - factory: factory, - queue: queue) - task.event = event + private func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + guard let factory = clientManager?.splitFactory else { return } + let task = SplitEventActionTask(action: action, event: event, runInBackground: runInBackground, factory: factory, queue: queue) 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.") - return - } - eventsManager.register(event: event, task: task) - } } // MARK: Treatment / Evaluation diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 6f90ba7b7..b3e5496f8 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -10,10 +10,10 @@ import Foundation /// To avoid crashing host app this dummy components will be returned /// on Failed init -/// class FailedClient: SplitClient { + // MARK: Treatments func getTreatment(_ split: String) -> String { return SplitConstants.control } @@ -54,17 +54,16 @@ class FailedClient: SplitClient { return [:] } - func on(event: SplitEvent, execute action: @escaping SplitAction) { - } + // MARK: Events Listeners + 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: SplitActionWithMetadata) {} + // MARK: Tracking func track(trafficType: String, eventType: String) -> Bool { return false } @@ -105,6 +104,7 @@ class FailedClient: SplitClient { return false } + // MARK: By FlagSets func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { return [:] } @@ -137,19 +137,18 @@ class FailedClient: SplitClient { return [:] } - func setUserConsent(enabled: Bool) { - } + func setUserConsent(enabled: Bool) {} - func flush() { - } + // MARK: Lifeycle + func flush() {} - func destroy() { - } + func destroy() {} func destroy(completion: (() -> Void)?) { completion?() } + // MARK: Tracking func track(trafficType: String, eventType: String, properties: [String: Any]?) -> Bool { return false } diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..71144b854 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -64,6 +64,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { super.init() } + // MARK: Treatments public func getTreatment(_ split: String, attributes: [String: Any]?) -> String { return getTreatmentWithConfig(split).treatment } @@ -121,25 +122,44 @@ public final class LocalhostSplitClient: NSObject, SplitClient { return results } - public func on(event: SplitEvent, runInBackground: Bool, - execute action: @escaping SplitAction) { + // MARK: Events Listeners + 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) } 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(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + on(eventWithMetadata: SplitEventWithMetadata(type: event, metadata: nil), runInBackground: runInBackground, queue: queue, execute: action) } - 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 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(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, + let task = SplitEventActionTask(action: action, event: event.type, runInBackground: runInBackground, factory: factory, queue: queue) @@ -147,6 +167,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } } + // MARK: Tracking public func track(trafficType: String, eventType: String) -> Bool { return true } @@ -179,11 +200,10 @@ public final class LocalhostSplitClient: NSObject, SplitClient { return true } - public func setUserConsent(enabled: Bool) { - } + public func setUserConsent(enabled: Bool) {} - public func flush() { - } + // MARK: Lifecycle + public func flush() {} public func destroy() { splitsStorage.destroy() @@ -195,7 +215,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } } -// MARK: Persistent attributes feature +// MARK: Persistence extension LocalhostSplitClient { public func setAttribute(name: String, value: Any) -> Bool { @@ -223,7 +243,7 @@ extension LocalhostSplitClient { } } -// MARK: TreatmentBySets Feature +// MARK: By FlagSet extension LocalhostSplitClient { public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { return [String: String]() @@ -249,13 +269,11 @@ extension LocalhostSplitClient { return [String: String]() } - public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, - attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { return [String: SplitResult]() } - public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], - attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { + public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { return [String: SplitResult]() } } diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index ff4df2005..86a7fdae5 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -8,91 +8,78 @@ import Foundation -public typealias SplitAction = () -> Void - @objc public protocol SplitClient { + + // MARK: Listeners for customer + 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: Evaluation feature + // MARK: Treatments 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] + + // With config func getTreatmentWithConfig(_ split: String) -> SplitResult func getTreatmentWithConfig(_ split: String, attributes: [String: Any]?) -> SplitResult - @objc(getTreatmentsWithConfigForSplits:attributes:) func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?) -> [String: SplitResult] - // MARK: Evaluation with Properties + // 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] - - func on(event: SplitEvent, execute action: @escaping SplitAction) - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) - - // MARK: Track feature + + // By Flagset + 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] + 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] + func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] + + + // 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 + @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(trackWithEventType:properties:) + func track(eventType: String, properties: [String: Any]?) -> Bool + @objc(trackWithEventType:value:properties:) + func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool - // MARK: Persistent attributes feature - - /// Creates or updates the value for the given attribute + + // MARK: Persistence + /// Creates or updates the value for the given attribute/ func setAttribute(name: String, value: Any) -> Bool - - /// Retrieves the value of a given attribute so it can be checked by the customer if needed + /// Retrieves the value of a given attribute so it can be checked by the customer if needed/ func getAttribute(name: String) -> Any? - - /// It will create or update all the given attributes + /// It will create or update all the given attributes/ func setAttributes(_ values: [String: Any]) -> Bool - - /// Retrieve the full attributes map + /// Retrieve the full attributes map/ func getAttributes() -> [String: Any]? - - /// Removes a given attribute from the map + /// Removes a given attribute from the map/ func removeAttribute(name: String) -> Bool - - /// Clears all attributes stored in the SDK. + /// 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 - - @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:value:properties:) func track(eventType: String, - value: Double, - properties: [String: Any]?) -> Bool - - // MARK: Evaluation 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 - 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] - func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] } diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index 8fd45f67f..428e8532b 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -73,18 +73,23 @@ class GenericBlockingQueue<Item> { // 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<SplitInternalEvent>() + let blockingQueue = GenericBlockingQueue<SplitInternalEventWithMetadata>() + func add(_ item: SplitInternalEvent) { + blockingQueue.add(SplitInternalEventWithMetadata(item)) + } + + func add(_ item: SplitInternalEventWithMetadata) { blockingQueue.add(item) } - func take() throws -> SplitInternalEvent { + func take() throws -> SplitInternalEventWithMetadata { let value = try blockingQueue.take() return value } diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..0978581f3 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -14,6 +14,7 @@ protocol SplitEventsManagerCoordinator: SplitEventsManager { } class MainSplitEventsManager: SplitEventsManagerCoordinator { + private var defaultManager: SplitEventsManager? private var managers = [Key: SplitEventsManager]() private var triggered = Set<SplitInternalEvent>() @@ -23,20 +24,30 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { .splitsUpdated, .splitKilledNotification] ) - + + // MARK: Notifications func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata? = nil) { if !eventsToHandle.contains(event) { return } + queue.async { [weak self] in guard let self = self else { return } self.triggered.insert(event) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event) + manager.notifyInternalEvent(event, metadata: metadata) } } } + + // MARK: Registers + func register(event: SplitEvent, task: SplitEventActionTask) {} + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) {} func start() {} @@ -75,6 +86,4 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { } } } - - func register(event: SplitEvent, task: SplitEventTask) {} } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index d2561e2d9..6c23ea792 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -1,28 +1,44 @@ -// -// 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: SplitMetadata? + + @objc public init(type: SplitEvent, metadata: SplitMetadata? = nil) { + self.type = type + self.metadata = metadata + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? SplitEventWithMetadata else { return false } + return self.type == other.type + } +} + @objc public enum SplitEvent: Int { case sdkReady case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated + case sdkError public func toString() -> String { switch self { - case .sdkReady: - return "SDK_READY" - case .sdkUpdated: - return "SDK_UPDATE" - case .sdkReadyTimedOut: - return "SDK_READY_TIMED_OUT" - case .sdkReadyFromCache: - return "SDK_READY_FROM_CACHE" + case .sdkReady: + return "SDK_READY" + case .sdkUpdated: + return "SDK_UPDATE" + case .sdkReadyTimedOut: + 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..a70a1f4d1 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -7,20 +7,30 @@ import Foundation -class SplitEventActionTask: SplitEventTask { +public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (_ metadata: SplitMetadata?) -> Void +class SplitEventActionTask: SplitEventTask { + + // Private private var eventHandler: SplitAction? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? + + // Public var event: SplitEvent var runInBackground: Bool = false var factory: SplitFactory + + 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 + } - init(action: @escaping SplitAction, - event: SplitEvent, - runInBackground: Bool = false, - factory: SplitFactory, - queue: DispatchQueue? = nil) { - + init(action: @escaping SplitAction, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { self.eventHandler = action self.event = event self.runInBackground = runInBackground @@ -33,7 +43,11 @@ class SplitEventActionTask: SplitEventTask { return queue } - func run() { + func run(_ metadata: SplitMetadata?) { eventHandler?() + + if let metadata = metadata { + eventHandlerWithMetadata?(metadata) + } } } diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..63421932f 100644 --- a/Split/Events/SplitEventTask.swift +++ b/Split/Events/SplitEventTask.swift @@ -7,9 +7,10 @@ import Foundation +// This protocol exists just for a dummy SplitEventTask por testing protocol SplitEventTask { var event: SplitEvent { get } var runInBackground: Bool { get } func takeQueue() -> DispatchQueue? - func run() + func run(_ metadata: SplitMetadata?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 56774453b..994934ef0 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -9,21 +9,24 @@ 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: SplitMetadata?) func start() func stop() func eventAlreadyTriggered(event: SplitEvent) -> Bool } class DefaultSplitEventsManager: SplitEventsManager { + private let readingRefreshTime: Int private var sdkReadyTimeStart: Int64 private var subscriptions = [SplitEvent: [SplitEventTask]]() private var executionTimes: [String: Int] - private var triggered: [SplitInternalEvent] + private var triggered: [SplitInternalEventWithMetadata] private let processQueue: DispatchQueue private let dataAccessQueue: DispatchQueue private var isStarted: Bool @@ -35,7 +38,7 @@ class DefaultSplitEventsManager: SplitEventsManager { self.isStarted = false self.sdkReadyTimeStart = Date().unixTimestampInMiliseconds() self.readingRefreshTime = 300 - self.triggered = [SplitInternalEvent]() + self.triggered = [SplitInternalEventWithMetadata]() self.eventsQueue = DefaultInternalEventBlockingQueue() self.executionTimes = [String: Int]() registerMaxAllowedExecutionTimesPerEvent() @@ -48,18 +51,10 @@ class DefaultSplitEventsManager: SplitEventsManager { } } } - - 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() + + // 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 +62,33 @@ 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: Notifiers + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata? = nil) { + let event = SplitInternalEventWithMetadata(event, metadata: metadata) + + processQueue.async { [weak self] in + if let self = self { + self.eventsQueue.add(event) + } + } + } + + // Method kept for backwards compatibility. Allows notifying an event without metadata. + func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + // MARK: Cycle func start() { dataAccessQueue.sync { if self.isStarted { @@ -128,7 +146,7 @@ class DefaultSplitEventsManager: SplitEventsManager { return isRunning } - private func takeEvent() -> SplitInternalEvent? { + private func takeEvent() -> SplitInternalEventWithMetadata? { do { return try eventsQueue.take() } catch BlockingQueueError.hasBeenStopped { @@ -145,10 +163,10 @@ class DefaultSplitEventsManager: SplitEventsManager { return } self.triggered.append(event) - switch event { + switch event.type { case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) + trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) continue } self.triggerSdkReadyIfNeeded() @@ -160,7 +178,7 @@ class DefaultSplitEventsManager: SplitEventsManager { isTriggered(internal: .mySegmentsLoadedFromCache), isTriggered(internal: .myLargeSegmentsLoadedFromCache), isTriggered(internal: .attributesLoadedFromCache) { - trigger(event: SplitEvent.sdkReadyFromCache) + trigger(event: .sdkReadyFromCache) } case .splitKilledNotification: if isTriggered(external: .sdkReady) { @@ -169,13 +187,13 @@ class DefaultSplitEventsManager: SplitEventsManager { } case .sdkReadyTimeoutReached: if !isTriggered(external: .sdkReady) { - trigger(event: SplitEvent.sdkReadyTimedOut) + trigger(event: .sdkReadyTimedOut) } } } } - // MARK: Helper functions. + // MARK: Helper functions func isTriggered(external event: SplitEvent) -> Bool { var triggered = false dataAccessQueue.sync { @@ -196,9 +214,13 @@ class DefaultSplitEventsManager: SplitEventsManager { self.trigger(event: SplitEvent.sdkReady) } } - + private func trigger(event: SplitEvent) { - let eventName = event.toString() + trigger(event: SplitEventWithMetadata(type: event, metadata: nil)) + } + + private func trigger(event: SplitEventWithMetadata) { + let eventName = event.type.toString() // If executionTimes is zero, maximum executions has been reached if executionTimes(for: eventName) == 0 { @@ -212,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() @@ -229,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 } @@ -237,13 +259,17 @@ 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 { + private func isTriggered(internal event: SplitInternalEventWithMetadata) -> Bool { return triggered.filter { $0 == event }.count > 0 } + + private func isTriggered(internal event: SplitInternalEvent) -> Bool { + return isTriggered(internal: SplitInternalEventWithMetadata(event, metadata: nil)) + } // MARK: Safe Data Access func executionTimes(for eventName: String) -> Int? { diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 4c9521204..e4ad78b74 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -7,6 +7,20 @@ import Foundation +struct SplitInternalEventWithMetadata { + let type: SplitInternalEvent + let metadata: SplitMetadata? + + init(_ type: SplitInternalEvent, metadata: SplitMetadata? = nil) { + self.type = type + self.metadata = metadata + } + + static func == (lhs: SplitInternalEventWithMetadata, rhs: SplitInternalEventWithMetadata) -> Bool { + return lhs.type == rhs.type + } +} + enum SplitInternalEvent { case mySegmentsUpdated case myLargeSegmentsUpdated diff --git a/Split/Events/SplitMetadata.swift b/Split/Events/SplitMetadata.swift new file mode 100644 index 000000000..5d276e72c --- /dev/null +++ b/Split/Events/SplitMetadata.swift @@ -0,0 +1,29 @@ +// Created by Martin Cardozo on 17/05/2025 +// Copyright © 2025 Split. All rights reserved. + +import Foundation + +@objc public class SplitMetadata: NSObject { + var type: SplitMetadataType + var data: String = "" + + init(type: SplitMetadataType, data: String) { + self.type = type + self.data = data + } +} + +enum SplitMetadataType: Int { + case FEATURE_FLAGS_SYNC_ERROR + case SEGMENTS_SYNC_ERROR + + public func toString() -> String { + switch self { + case .FEATURE_FLAGS_SYNC_ERROR: + return "FEATURE_FLAGS_SYNC_ERROR" + case .SEGMENTS_SYNC_ERROR: + return "SEGMENTS_SYNC_ERROR" + + } + } +} diff --git a/Split/FetcherEngine/Recorder/TelemetryStatsRecorderWorker.swift b/Split/FetcherEngine/Recorder/TelemetryStatsRecorderWorker.swift index 0bee73e79..c0c0ebcd3 100644 --- a/Split/FetcherEngine/Recorder/TelemetryStatsRecorderWorker.swift +++ b/Split/FetcherEngine/Recorder/TelemetryStatsRecorderWorker.swift @@ -16,11 +16,7 @@ class TelemetryStatsRecorderWorker: RecorderWorker { private let mySegmentsStorage: MySegmentsStorage private let myLargeSegmentsStorage: MySegmentsStorage - init(telemetryStatsRecorder: HttpTelemetryStatsRecorder, - telemetryConsumer: TelemetryConsumer, - splitsStorage: SplitsStorage, - mySegmentsStorage: MySegmentsStorage, - myLargeSegmentsStorage: MySegmentsStorage) { + init(telemetryStatsRecorder: HttpTelemetryStatsRecorder, telemetryConsumer: TelemetryConsumer, splitsStorage: SplitsStorage, mySegmentsStorage: MySegmentsStorage, myLargeSegmentsStorage: MySegmentsStorage) { self.telemetryConsumer = telemetryConsumer self.statsRecorder = telemetryStatsRecorder @@ -30,7 +26,7 @@ class TelemetryStatsRecorderWorker: RecorderWorker { } func flush() { - if !statsRecorder.isEndpointAvailable() { + guard statsRecorder.isEndpointAvailable() else { Logger.d("Endpoint not reachable. Telemetry stats post will be delayed") return } diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index 04c4b5df3..24cfa4710 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() } @@ -34,19 +34,19 @@ class BlockingQueueTest: XCTestCase { } } globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.mySegmentsLoadedFromCache) + queue.add(.mySegmentsLoadedFromCache) globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.splitsLoadedFromCache) + queue.add(.splitsLoadedFromCache) } } - queue.add(SplitInternalEvent.splitsUpdated) - queue.add(SplitInternalEvent.mySegmentsUpdated) + queue.add(.splitsUpdated) + queue.add(.mySegmentsUpdated) wait(for: [endExp], timeout: 10) - XCTAssertEqual(SplitInternalEvent.splitsUpdated, local[0]) - XCTAssertEqual(SplitInternalEvent.mySegmentsUpdated, local[1]) - XCTAssertEqual(SplitInternalEvent.mySegmentsLoadedFromCache, local[2]) - XCTAssertEqual(SplitInternalEvent.splitsLoadedFromCache, local[3]) + XCTAssertEqual(.splitsUpdated, local[0]) + XCTAssertEqual(.mySegmentsUpdated, local[1]) + XCTAssertEqual(.mySegmentsLoadedFromCache, local[2]) + XCTAssertEqual(.splitsLoadedFromCache, local[3]) } func testInterrupt() { @@ -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 @@ -68,19 +68,19 @@ class BlockingQueueTest: XCTestCase { } } globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.mySegmentsLoadedFromCache) + queue.add(.mySegmentsLoadedFromCache) globalQ.asyncAfter(deadline: .now() + 1) { queue.stop() } } - queue.add(SplitInternalEvent.splitsUpdated) - queue.add(SplitInternalEvent.mySegmentsUpdated) + queue.add(.splitsUpdated) + queue.add(.mySegmentsUpdated) wait(for: [endExp], timeout: 10) - XCTAssertEqual(SplitInternalEvent.splitsUpdated, local[0]) - XCTAssertEqual(SplitInternalEvent.mySegmentsUpdated, local[1]) - XCTAssertEqual(SplitInternalEvent.mySegmentsLoadedFromCache, local[2]) + XCTAssertEqual(.splitsUpdated, local[0]) + XCTAssertEqual(.mySegmentsUpdated, local[1]) + XCTAssertEqual(.mySegmentsLoadedFromCache, local[2]) XCTAssertTrue(interrupted) } @@ -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 { } } @@ -151,7 +151,7 @@ class BlockingQueueTest: XCTestCase { qu1.async { for _ in 1..<100000 { - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(.splitsUpdated) print("qu1 add") Thread.sleep(forTimeInterval: 0.2) } @@ -160,7 +160,7 @@ class BlockingQueueTest: XCTestCase { qu2.async { for _ in 1..<10000 { print("qu2 add") - queue.add(SplitInternalEvent.sdkReadyTimeoutReached) + queue.add(.sdkReadyTimeoutReached) Thread.sleep(forTimeInterval: 0.5) } } @@ -168,7 +168,7 @@ class BlockingQueueTest: XCTestCase { qu3.async { for _ in 1..<10000 { print("qu3 add") - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(.splitsUpdated) Thread.sleep(forTimeInterval: 0.8) } } @@ -176,7 +176,7 @@ class BlockingQueueTest: XCTestCase { qu4.async { for _ in 1..<10000 { print("qu4 add") - queue.add(SplitInternalEvent.mySegmentsUpdated) + queue.add(.mySegmentsUpdated) sleep(1) } } diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index 5b792fb3e..17e6bf2d5 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -23,6 +23,7 @@ class InternalSplitClientStub: InternalSplitClient { self.myLargeSegmentsStorage = myLargeSegmentsStorage } + // MARK: Treatments func getTreatment(_ split: String, attributes: [String : Any]?) -> String { return SplitConstants.control } @@ -43,6 +44,7 @@ class InternalSplitClientStub: InternalSplitClient { return createControlTreatmentsDictionary(splits: splits) } + // With config func getTreatmentWithConfig(_ split: String) -> SplitResult { return SplitResult(treatment: SplitConstants.control) } @@ -63,6 +65,7 @@ class InternalSplitClientStub: InternalSplitClient { return createControlTreatmentsDictionary(splits: splits) } + // By FlagSets func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : String] { return ["": SplitConstants.control] } @@ -95,15 +98,14 @@ class InternalSplitClientStub: InternalSplitClient { return ["": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { - } - - func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) { - } - - func on(event: SplitEvent, execute action: @escaping SplitAction) { - } + // MARK: Events Listeners + func on(event: SplitEvent, queue: DispatchQueue, 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, executeWithMetadata: @escaping SplitActionWithMetadata) {} + func on(event: SplitEvent, executeTask: SplitEventTask) {} + // MARK: Tracking func track(trafficType: String, eventType: String) -> Bool { return true } @@ -160,18 +162,10 @@ class InternalSplitClientStub: InternalSplitClient { return true } - func flush() { - } - - func destroy() { - } - - func destroy(completion: (() -> Void)?) { - } - - func on(event: SplitEvent, executeTask: SplitEventTask) { - - } + // MARK: Lifecycle + func flush() {} + func destroy() {} + func destroy(completion: (() -> Void)?) {} private func createControlTreatmentsDictionary<T>(splits: [String]) -> [String: T] where T: Any { var result = [String: T]() diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 94cbb8991..7fd72f601 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -22,9 +22,11 @@ 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: SplitMetadata?) {} var notifiedEvents = Set<String>() func notifyInternalEvent(_ event: SplitInternalEvent) { diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index a19acdcb7..862009b90 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -10,7 +10,8 @@ import Foundation @testable import Split class SplitClientStub: SplitClient { - + + // MARK: Treatments func getTreatment(_ split: String, attributes: [String : Any]?) -> String { return SplitConstants.control } @@ -31,6 +32,7 @@ class SplitClientStub: SplitClient { return ["feature": SplitConstants.control] } + // With Config func getTreatmentWithConfig(_ split: String) -> SplitResult { return SplitResult(treatment: SplitConstants.control) } @@ -51,6 +53,7 @@ class SplitClientStub: SplitClient { return ["feature": SplitResult(treatment: SplitConstants.control)] } + // By FlagSets func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : String] { return ["feature": SplitConstants.control] } @@ -83,18 +86,14 @@ class SplitClientStub: SplitClient { return ["feature": SplitResult(treatment: SplitConstants.control)] } - func on(event: SplitEvent, queue: DispatchQueue, 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, queue: DispatchQueue?, execute action: @escaping SplitAction) { - } + // MARK: Events Listeners + func on(event: SplitEvent, queue: DispatchQueue, 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, queue: DispatchQueue?, execute action: @escaping SplitAction) {} + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) {} + // MARK: Tracking func track(trafficType: String, eventType: String) -> Bool { return true } @@ -127,6 +126,7 @@ class SplitClientStub: SplitClient { return true } + // MARK: Attributes func setAttribute(name: String, value: Any) -> Bool { return true } @@ -151,12 +151,8 @@ class SplitClientStub: SplitClient { return true } - func flush() { - } - - func destroy() { - } - - func destroy(completion: (() -> Void)?) { - } + // MARK: Lifecycle + func flush() {} + func destroy() {} + func destroy(completion: (() -> Void)?) {} } diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index 1132148df..d1bdd234e 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -21,48 +21,59 @@ class SplitEventsManagerMock: SplitEventsManager { var isSdkReadyFromCacheFired: Bool { return isSegmentsReadyFromCacheFired && isSplitsReadyFromCacheFired } + var isSegmentsReadyFired = false var isSplitsReadyFired = false var isSegmentsReadyFromCacheFired = false var isSplitsReadyFromCacheFired = false var isSdkTimeoutFired = false - + var isSdkErrorFired = false var isSplitUpdatedTriggered = false var isSdkUpdatedFired = false - var isSdkReadyChecked = false + + var metadata: SplitMetadata? - func notifyInternalEvent(_ event:SplitInternalEvent) { + // MARK: Notifiers + func notifyInternalEvent(_ event: SplitInternalEvent) { switch event { - case .mySegmentsUpdated: - isSegmentsReadyFired = true - case .splitsUpdated: - isSplitsReadyFired = true - isSplitUpdatedTriggered = true - if let exp = readyExp { - exp.fulfill() + case .mySegmentsUpdated: + isSegmentsReadyFired = true + case .splitsUpdated: + isSplitsReadyFired = true + isSplitUpdatedTriggered = true + if let exp = readyExp { + exp.fulfill() + } + case .sdkReadyTimeoutReached: + isSdkTimeoutFired = true + if let exp = timeoutExp { + exp.fulfill() + } + default: + print("\(event)") } - case .sdkReadyTimeoutReached: - isSdkTimeoutFired = true - if let exp = timeoutExp { - exp.fulfill() - } - default: - print("\(event)") - } } - - var registeredEvents = [SplitEvent: SplitEventTask]() - func register(event: SplitEvent, task: SplitEventTask) { - registeredEvents[event] = task + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata?) { + self.metadata = metadata + notifyInternalEvent(event) } - func start() { + // MARK: Registers + var registeredEvents = [SplitEvent: SplitEventActionTask]() + func register(event: SplitEvent, task: SplitEventActionTask) { + register(event: SplitEventWithMetadata(type: event), task: task) } - - func stop() { + + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) { + registeredEvents[event.type] = task } + func start() {} + + func stop() {} + func eventAlreadyTriggered(event: SplitEvent) -> Bool { switch event { case.sdkReady: @@ -74,6 +85,8 @@ class SplitEventsManagerMock: SplitEventsManager { return isSdkTimeoutFired case .sdkUpdated: return isSdkUpdatedFired + case .sdkError: + return isSdkErrorFired default: return true diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 1d7685dc3..15fcdf023 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -15,32 +15,49 @@ class SplitEventsManagerStub: SplitEventsManager { var splitsKilledEventFiredCount = 0 var splitsUpdatedEventFiredCount = 0 var mySegmentsLoadedEventFiredCount = 0 + + var metadata: SplitMetadata? + var mySegmentsLoadedEventExp: XCTestExpectation? var startCalled = false var stopCalled = false func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: nil) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata? = nil) { + + self.metadata = metadata + switch event { - case .mySegmentsLoadedFromCache: - mySegmentsLoadedEventFiredCount+=1 - if let exp = mySegmentsLoadedEventExp { - exp.fulfill() - } - case .splitsLoadedFromCache: - splitsLoadedEventFiredCount+=1 - - case .splitKilledNotification: - splitsKilledEventFiredCount+=1 - - case .splitsUpdated: - splitsUpdatedEventFiredCount+=1 - default: - print("internal event fired: \(event)") + case .mySegmentsLoadedFromCache: + mySegmentsLoadedEventFiredCount += 1 + if let exp = mySegmentsLoadedEventExp { + exp.fulfill() + } + case .splitsLoadedFromCache: + splitsLoadedEventFiredCount += 1 + + case .splitKilledNotification: + splitsKilledEventFiredCount += 1 + + case .splitsUpdated: + splitsUpdatedEventFiredCount += 1 + + case .sdkReadyTimeoutReached: + splitsUpdatedEventFiredCount += 1 + default: + print("internal event fired: \(event)") } } - 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/Integration/Recorder/TelemetryTest.swift b/SplitTests/Integration/Recorder/TelemetryTest.swift index a766a21f6..28ba9d8c8 100644 --- a/SplitTests/Integration/Recorder/TelemetryTest.swift +++ b/SplitTests/Integration/Recorder/TelemetryTest.swift @@ -167,6 +167,7 @@ class TelemetryTest: XCTestCase { XCTAssertEqual(0, sum(latenciesAfter.treatmentsWithConfig)) XCTAssertEqual(0, sum(latenciesAfter.track)) + let semaphore = DispatchSemaphore(value: 0) client.destroy(completion: { _ = semaphore.signal() diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 9f66d2936..6daa666b0 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -25,17 +25,17 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkUpdated, task: updatedTask) eventManager.start() - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) ThreadUtils.delay(seconds: 0.2) let expectation = XCTestExpectation(description: "SDK Readky triggered") queue.async { while !shouldStop { sleep(UInt32(self.intervalExecutionTime)) - if eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady) { + if eventManager.eventAlreadyTriggered(event: .sdkReady) { shouldStop = true; expectation.fulfill() } @@ -43,9 +43,9 @@ class SplitEventsManagerTest: XCTestCase { } wait(for: [expectation], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkUpdated), "SDK Update shouldn't be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkUpdated), "SDK Update shouldn't be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); eventManager.stop() } @@ -56,20 +56,20 @@ class SplitEventsManagerTest: XCTestCase { let eventManager: SplitEventsManager = DefaultSplitEventsManager(config: config) eventManager.start() - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.attributesLoadedFromCache) + eventManager.notifyInternalEvent(.mySegmentsLoadedFromCache) + eventManager.notifyInternalEvent(.myLargeSegmentsLoadedFromCache) + eventManager.notifyInternalEvent(.splitsLoadedFromCache) + eventManager.notifyInternalEvent(.attributesLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) var shouldStop = false let expectation = XCTestExpectation(description: "SDK Readky from cache triggered") queue.async { while !shouldStop { sleep(UInt32(self.intervalExecutionTime)) - if eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady) { + if eventManager.eventAlreadyTriggered(event: .sdkReady) { shouldStop = true; expectation.fulfill() } @@ -77,9 +77,9 @@ class SplitEventsManagerTest: XCTestCase { } wait(for: [expectation], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyFromCache), "SDK Ready should from cache be triggered"); - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReadyFromCache), "SDK Ready should from cache be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); eventManager.stop() } @@ -96,17 +96,17 @@ class SplitEventsManagerTest: XCTestCase { let timeoutExp = XCTestExpectation() eventManager.register(event: .sdkReadyTimedOut, task: TestTask(exp: timeoutExp)) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.attributesLoadedFromCache) - eventManager.notifyInternalEvent(SplitInternalEvent.sdkReadyTimeoutReached) + eventManager.notifyInternalEvent(.mySegmentsLoadedFromCache) + eventManager.notifyInternalEvent(.myLargeSegmentsLoadedFromCache) + eventManager.notifyInternalEvent(.splitsLoadedFromCache) + eventManager.notifyInternalEvent(.attributesLoadedFromCache) + eventManager.notifyInternalEvent(.sdkReadyTimeoutReached) wait(for: [cacheExp, timeoutExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyFromCache), "SDK Ready should from cache be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should not be triggered"); - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out should be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReadyFromCache), "SDK Ready should from cache be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should not be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out should be triggered"); eventManager.stop() } @@ -122,8 +122,8 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkReadyTimedOut, task: TestTask(exp: timeoutExp)) wait(for: [timeoutExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out should be triggered") - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready shouldn't be triggered") + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out should be triggered") + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready shouldn't be triggered") eventManager.stop() } @@ -141,18 +141,18 @@ class SplitEventsManagerTest: XCTestCase { wait(for: [expectationTimeout], timeout: expectationTimeOut) //At this line timeout has been reached - let timeoutTriggered = eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut) + let timeoutTriggered = eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut) let readyExp = XCTestExpectation(description: "SDK Readky triggered") eventManager.register(event: .sdkReady, task: TestTask(exp: readyExp)) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) wait(for: [readyExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); XCTAssertTrue(timeoutTriggered) eventManager.stop() @@ -170,17 +170,17 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkReady, task: TestTask(exp: readyExp)) eventManager.register(event: .sdkUpdated, task: updatedTask) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) ThreadUtils.delay(seconds: 0.5) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) wait(for: [readyExp, sdkUpdatedExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); XCTAssertTrue(updatedTask.taskTriggered, "SDK Update should be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); eventManager.stop() } @@ -197,15 +197,15 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkUpdated, task: updatedTask) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) ThreadUtils.delay(seconds: 0.5) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) wait(for: [readyExp, sdkUpdatedExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); XCTAssertTrue(updatedTask.taskTriggered, "SDK Update should be triggered"); XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); @@ -223,17 +223,17 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkReady, task: TestTask(exp: readyExp)) eventManager.register(event: .sdkUpdated, task: updatedTask) - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.myLargeSegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitsUpdated) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.myLargeSegmentsUpdated) + eventManager.notifyInternalEvent(.splitsUpdated) ThreadUtils.delay(seconds: 0.5) - eventManager.notifyInternalEvent(SplitInternalEvent.splitKilledNotification) + eventManager.notifyInternalEvent(.splitKilledNotification) wait(for: [readyExp, sdkUpdatedExp], timeout: expectationTimeOut) - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady), "SDK Ready should be triggered"); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReady), "SDK Ready should be triggered"); XCTAssertTrue(updatedTask.taskTriggered, "SDK Update should be triggered"); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut), "SDK Time out shouldn't be triggered"); eventManager.stop() } @@ -250,19 +250,39 @@ class SplitEventsManagerTest: XCTestCase { eventManager.register(event: .sdkReadyTimedOut, task: timeOutTask) eventManager.start() - eventManager.notifyInternalEvent(SplitInternalEvent.mySegmentsUpdated) - eventManager.notifyInternalEvent(SplitInternalEvent.splitKilledNotification) + eventManager.notifyInternalEvent(.mySegmentsUpdated) + eventManager.notifyInternalEvent(.splitKilledNotification) wait(for: [sdkTiemoutExp], timeout: timeout) XCTAssertFalse(updatedTask.taskTriggered); - XCTAssertFalse(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReady)); + XCTAssertFalse(eventManager.eventAlreadyTriggered(event: .sdkReady)); XCTAssertTrue(timeOutTask.taskTriggered); - XCTAssertTrue(eventManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyTimedOut)); + XCTAssertTrue(eventManager.eventAlreadyTriggered(event: .sdkReadyTimedOut)); eventManager.stop() } + func testSplitEventActionTaskMetadata() { + + // Dummy event with metadata + let metadataTypeToCheck: SplitMetadataType = .FEATURE_FLAGS_SYNC_ERROR + let metadataDataToCheck: String = "Error syncing" + let dummyMetadata = SplitMetadata(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()) @@ -273,29 +293,24 @@ 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: SplitMetadata? = 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: SplitMetadata?) { print("run: \(self.label)") taskTriggered = true + + super.run(metadata) + if let exp = self.exp { exp.fulfill() }