diff --git a/CHANGES.txt b/CHANGES.txt index 753ae5eee..afa7531da 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -306,7 +306,7 @@ - Added client dependency to event background task 0.2.0: (Apr 25, 2018) - - Added events listener. Available events: SplitEvent.sdkReady and SplitEvent.sdkReadyTimedOut + - Added events listener. Available events: SplitEventCase.sdkReady and SplitEventCase.sdkReadyTimedOut 0.1.5: (Mar 19, 2018) - Added first load from cache diff --git a/README.md b/README.md index c1a533bd4..e7b006628 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ let factory = factoryBuilder.build() let client = factory?.client // Subscribe to SDK READY event and evaluate your feature flag -client?.on(event: SplitEvent.sdkReady) { +client?.on(event: SplitEventCase.sdkReady) { if let client = client { let treatment = client.getTreatment("my_first_feature_flag") if treatment == "on" { diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..2cc5d4f71 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -12,7 +12,7 @@ import Foundation typealias DestroyHandler = () -> Void public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClient { - + private var storageContainer: SplitStorageContainer private var key: Key private let config: SplitClientConfig @@ -55,52 +55,48 @@ 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) + } + + 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) + } 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 - 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 +// MARK: Treatments extension DefaultSplitClient { public func getTreatmentWithConfig(_ split: String) -> SplitResult { return treatmentManager.getTreatmentWithConfig(split, attributes: nil, evaluationOptions: nil) @@ -127,7 +123,7 @@ extension DefaultSplitClient { } } -// MARK: Treatment / Evaluation with Properties +// MARK: With Properties extension DefaultSplitClient { public func getTreatment(_ split: String, attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> String { return treatmentManager.getTreatment(split, attributes: attributes, evaluationOptions: evaluationOptions) @@ -164,7 +160,7 @@ extension DefaultSplitClient { } } -// MARK: Track Events +// MARK: Tracking extension DefaultSplitClient { public func track(trafficType: String, eventType: String) -> Bool { @@ -214,7 +210,7 @@ extension DefaultSplitClient { } } -// MARK: Persistent attributes feature +// MARK: Persistence extension DefaultSplitClient { public func setAttribute(name: String, value: Any) -> Bool { @@ -270,7 +266,7 @@ extension DefaultSplitClient { } } -// 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) @@ -290,7 +286,7 @@ extension DefaultSplitClient { } } -// MARK: Flush / Destroy +// MARK: Lifecycle extension DefaultSplitClient { private func syncFlush() { diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 6f90ba7b7..e1ec94d56 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -10,9 +10,9 @@ import Foundation /// To avoid crashing host app this dummy components will be returned /// on Failed init -/// class FailedClient: SplitClient { + func on(event: SplitEvent, executeWithMetadata: SplitActionWithMetadata) {} func getTreatment(_ split: String) -> String { return SplitConstants.control @@ -53,9 +53,12 @@ class FailedClient: SplitClient { func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] { return [:] } + + public func on(event: SplitEvent, perform: SplitAction?) {} + + func on(event: SplitEvent, perform: @escaping ([String : Any]?) -> Void) {} - 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) { @@ -137,14 +140,11 @@ 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?() diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..0e04ffc22 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -51,10 +51,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { private let key: Key weak var clientManger: SplitClientManager? - init(key: Key, splitsStorage: SplitsStorage, - clientManager: SplitClientManager?, - eventsManager: SplitEventsManager? = nil, - evaluator: Evaluator) { + init(key: Key, splitsStorage: SplitsStorage, clientManager: SplitClientManager?, eventsManager: SplitEventsManager? = nil, evaluator: Evaluator) { self.eventsManager = eventsManager self.key = key self.splitsStorage = splitsStorage @@ -64,6 +61,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { super.init() } + // MARK: Treatments public func getTreatment(_ split: String, attributes: [String: Any]?) -> String { return getTreatmentWithConfig(split).treatment } @@ -120,6 +118,16 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } return results } + + // MARK: Events Listeners + public func on(event: SplitEvent, perform: SplitAction?) { + guard let perform = perform else { return } + on(event: event, execute: perform) + } + + 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) { @@ -134,12 +142,29 @@ public final class LocalhostSplitClient: NSObject, SplitClient { on(event: event, runInBackground: false, queue: nil, execute: action) } - private func on(event: SplitEvent, runInBackground: Bool, + 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(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, + 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.type, runInBackground: runInBackground, factory: factory, queue: queue) @@ -147,6 +172,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } } + // MARK: Tracking public func track(trafficType: String, eventType: String) -> Bool { return true } @@ -179,11 +205,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 +220,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 +248,7 @@ extension LocalhostSplitClient { } } -// MARK: TreatmentBySets Feature +// MARK: By Flagset extension LocalhostSplitClient { public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] { return [String: String]() 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/Api/SplitClientManager.swift b/Split/Api/SplitClientManager.swift index 6cdbf743a..9777010ed 100644 --- a/Split/Api/SplitClientManager.swift +++ b/Split/Api/SplitClientManager.swift @@ -88,7 +88,7 @@ class DefaultClientManager: SplitClientManager { } } - defaultClient?.on(event: .sdkReady) { + defaultClient?.on(event: .sdkReadyFromCache) { DispatchQueue.general.async { [weak self] in if let self = self { producer.recordTimeUntilReady(self.telemetryStopwatch?.interval() ?? 0) diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index 8fd45f67f..df5bfbb6f 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -73,18 +73,18 @@ 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() - func add(_ item: SplitInternalEvent) { + let blockingQueue = GenericBlockingQueue() + 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/Engine/DefaultTreatmentManager.swift b/Split/Engine/DefaultTreatmentManager.swift index 73594b32e..d9b86bf53 100644 --- a/Split/Engine/DefaultTreatmentManager.swift +++ b/Split/Engine/DefaultTreatmentManager.swift @@ -338,7 +338,7 @@ extension DefaultTreatmentManager { private func isSdkReady() -> Bool { return eventsManager.eventAlreadyTriggered(event: SplitEvent.sdkReadyFromCache) || - eventsManager.eventAlreadyTriggered(event: SplitEvent.sdkReady) + eventsManager.eventAlreadyTriggered(event: .sdkReady) } private func checkAndLogIfDestroyed(logTag: String) -> Bool { diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..e84dc33f5 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() @@ -23,8 +24,17 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { .splitsUpdated, .splitKilledNotification] ) - + + // MARK: Notifications func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(event, metadata: SplitMetadata(type: .NETWORK_ERROR.toStrin(), value: "")) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + notifyInternalEvent(event.type, metadata: event.metadata ?? SplitMetadata(type: "", value: "")) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata) { if !eventsToHandle.contains(event) { return } @@ -33,7 +43,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { self.triggered.insert(event) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event) + manager.notifyInternalEvent(event, metadata: metadata) } } } @@ -76,5 +86,5 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { } } - func register(event: SplitEvent, task: SplitEventTask) {} + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) {} } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index d2561e2d9..279e1da1b 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -1,5 +1,5 @@ // -// SplitEvent.swift +// SplitEventCase.swift // Split // // Created by Sebastian Arrubia on 4/17/18. @@ -7,6 +7,21 @@ import Foundation +@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 @@ -15,14 +30,32 @@ import Foundation 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" } } } + +// Just a key-value wrapper for extensibility. +// (Also used by SplitInternalEvent) +@objc public class SplitMetadata: NSObject { + var type: String + var data: String = "" + + init(type: String, data: String) { + self.type = type + self.data = data + } +} + +enum SplitError: Int { + case NETWORK_ERROR + case STORAGE_ERROR + case SPLIT_UPDATE +} diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index be1368d04..4e1b9236f 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -7,20 +7,27 @@ import Foundation -class SplitEventActionTask: SplitEventTask { +public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (_ metadata: SplitMetadata?) -> Void +class SplitEventActionTask: SplitEventTask { + private var eventHandler: SplitAction? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? var event: SplitEvent var runInBackground: Bool = false var factory: SplitFactory - init(action: @escaping SplitAction, - event: SplitEvent, - runInBackground: Bool = false, - factory: SplitFactory, - queue: DispatchQueue? = nil) { - + 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) { self.eventHandler = action self.event = event self.runInBackground = runInBackground @@ -33,7 +40,11 @@ class SplitEventActionTask: SplitEventTask { return queue } - func run() { + func run(_ metadata: SplitMetadata?) { + // filter + eventHandler?() + + eventHandlerWithMetadata?(metadata) } } diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..79bf23d2d 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: SplitMetadata?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 56774453b..0d2375fce 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -9,25 +9,30 @@ import Foundation protocol SplitEventsManager: AnyObject { - func register(event: SplitEvent, task: SplitEventTask) + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) func notifyInternalEvent(_ event: SplitInternalEvent) + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata) + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) 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 private var eventsQueue: InternalEventBlockingQueue + + private let lock = NSLock() init(config: SplitClientConfig) { self.processQueue = DispatchQueue(label: "split-evt-mngr-process", attributes: .concurrent) @@ -35,7 +40,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() @@ -44,44 +49,54 @@ class DefaultSplitEventsManager: SplitEventsManager { let readyTimedoutQueue = DispatchQueue(label: "split-event-timedout") readyTimedoutQueue.asyncAfter(deadline: .now() + .milliseconds(config.sdkReadyTimeOut)) { [weak self] in guard let self = self else { return } - self.notifyInternalEvent(SplitInternalEvent.sdkReadyTimeoutReached) + self.notifyInternalEvent(.sdkReadyTimeoutReached) } } } - + + // MARK: Notifications + /* Overload kept for backwards compatibility. + It allows calling `notifyInternalEvent(.event)` without needing to pass `nil` as metadata. + Do not remove unless all usages have migrated to the new signature. */ func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEvent, metadata: SplitMetadata) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: metadata)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { 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: Register + 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 if let times = self.executionTimes(for: eventName), times == 0 { - self.executeTask(event: event, task: task) + self.executeTask(eventWithMetadata: event, task: task) return } - self.subscribe(task: task, to: event) + self.subscribe(task: task, to: event.type) } } func start() { - dataAccessQueue.sync { - if self.isStarted { - return + lock.lock() + if !isStarted { + isStarted = true } - self.isStarted = true - } + lock.unlock() + processQueue.async { [weak self] in - if let self = self { - self.processEvents() - } + self?.processEvents() } } @@ -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 { @@ -145,33 +160,32 @@ class DefaultSplitEventsManager: SplitEventsManager { 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) + switch event.type { + case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: + if isTriggered(external: .sdkReady) { + trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) + continue + } + self.triggerSdkReadyIfNeeded() + + case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, .splitsLoadedFromCache, .attributesLoadedFromCache: + Logger.v("Event \(event.type) 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) + } } - } } } @@ -193,12 +207,16 @@ class DefaultSplitEventsManager: SplitEventsManager { isTriggered(internal: .splitsUpdated), isTriggered(internal: .myLargeSegmentsUpdated), !isTriggered(external: .sdkReady) { - self.trigger(event: SplitEvent.sdkReady) + self.trigger(event: .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 +230,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) + executeTask(eventWithMetadata: event, task: task) } } } - private func executeTask(event: SplitEvent, task: SplitEventTask) { + private func executeTask(eventWithMetadata event: SplitEventWithMetadata, task: SplitEventTask) { let eventName = task.event.toString() @@ -229,21 +247,24 @@ 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) + } + } else { + DispatchQueue.main.async { + TimeChecker.logInterval("Running event on main: \(eventName)") + // UI Updates + task.run(event.metadata) } - return - } - - DispatchQueue.main.async { - TimeChecker.logInterval("Running event on main: \(eventName)") - // UI Updates - task.run() } } - 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..8e3c31cdc 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -3,10 +3,23 @@ // Split // // Created by Sebastian Arrubia on 4/16/18. -// 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/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index 66065d934..b6c0807da 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -130,7 +130,7 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { Logger.i("Fetch from remote not implemented") } - func notifyUpdate(_ events: [SplitInternalEvent]) { + func notifyUpdate(_ events: [SplitInternalEventWithMetadata]) { events.forEach { eventsManager.notifyInternalEvent($0) } @@ -171,8 +171,16 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { guard let result = try? syncHelper.sync(since: splitsStorage.changeNumber) else { return } - if result.success, result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if result.success, result.featureFlagsUpdated.count > 0 { + + var updatedFlags = result.featureFlagsUpdated + for flag in updatedFlags { + updatedFlags.append(flag) + } + + let metadata = SplitMetadata(type: "Updated Flags", value: updatedFlags.description) + notifyUpdate([SplitInternalEventWithMetadata(.splitsUpdated, metadata: metadata)]) + } } } @@ -211,9 +219,10 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { mlsTill: myLargeSegmentsStorage.changeNumber, headers: nil) if result.success { - if result.msUpdated || result.mlsUpdated { - // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + if !result.msUpdated.isEmpty || !result.mlsUpdated.isEmpty { + + let metadata = SplitMetadata(type: "Updated segments", value: (result.msUpdated + result.mlsUpdated).description) + notifyUpdate([SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: metadata)]) } } } catch { diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 36973b66b..14504b946 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -44,13 +44,16 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { mlsTill: changeNumbers.mlsChangeNumber, headers: getHeaders()) if result.success { + let msMetadata = SplitMetadata(type: "Segments updated", value: result.msUpdated.description) + if !isSdkReadyTriggered() { // Notifying both to trigger SDK Ready - notifyUpdate([.mySegmentsUpdated]) - notifyUpdate([.myLargeSegmentsUpdated]) - } else if result.msUpdated || result.mlsUpdated { - // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + notifyUpdate([SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: msMetadata)]) + let mlsMetadata = SplitMetadata(type: "Large segments updated", value: result.mlsUpdated.description) + notifyUpdate([SplitInternalEventWithMetadata(.myLargeSegmentsUpdated, metadata: mlsMetadata)]) + } else if !result.msUpdated.isEmpty || !result.mlsUpdated.isEmpty { + + notifyUpdate([SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: msMetadata)]) } return true } @@ -71,8 +74,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 +89,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 +168,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,25 +178,20 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { return SegmentsSyncResult(success: false, msChangeNumber: -1, mlsChangeNumber: -1, - msUpdated: false, - mlsUpdated: false) + msUpdated: [], + mlsUpdated: []) } - private func fetchUntil(till: Int64?, - headers: HttpHeaders? = nil) throws -> FetchResult { + private func fetchUntil(till: Int64?, headers: HttpHeaders? = nil) throws -> FetchResult { - let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), - changeNumber: mySegmentsStorage.changeNumber) + let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), changeNumber: mySegmentsStorage.changeNumber) - let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), - changeNumber: myLargeSegmentsStorage.changeNumber) + let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), changeNumber: myLargeSegmentsStorage.changeNumber) - var prevChange = AllSegmentsChange(mySegmentsChange: oldChange, - myLargeSegmentsChange: oldLargeChange) + var prevChange = AllSegmentsChange(mySegmentsChange: oldChange, myLargeSegmentsChange: oldLargeChange) + while true { - guard let change = try segmentsFetcher.execute(userKey: userKey, - till: till, - headers: headers) else { + guard let change = try segmentsFetcher.execute(userKey: userKey, till: till, headers: headers) else { throw HttpError.unknown(code: -1, message: "Segment result is null") } @@ -201,10 +199,8 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { let myLargeSegmentsChange = change.myLargeSegmentsChange if !isOutdated(change, prevChange) { - let msChanged = changeChecker.mySegmentsHaveChanged(old: oldChange, - new: mySegmentsChange) - let mlsChanged = changeChecker.mySegmentsHaveChanged(old: oldLargeChange, - new: myLargeSegmentsChange) + let msChanged = changeChecker.mySegmentsHaveChanged(old: oldChange, new: mySegmentsChange) + let mlsChanged = changeChecker.mySegmentsHaveChanged(old: oldLargeChange, new: myLargeSegmentsChange) Logger.d("Checking my segments update") checkAndUpdate(isChanged: msChanged, change: mySegmentsChange, storage: mySegmentsStorage) Logger.d("Checking my large segments update") @@ -212,8 +208,8 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { return FetchResult(msTill: mySegmentsChange.unwrappedChangeNumber, mlsTill: myLargeSegmentsChange.unwrappedChangeNumber, - msUpdated: msChanged, - mlsUdated: mlsChanged) + msUpdated: mySegmentsChange.segments.compactMap {$0.name}, + mlsUpdated: myLargeSegmentsChange.segments.compactMap {$0.name}) } prevChange = change } diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 17d91c78b..61869a284 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -79,9 +79,9 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { } } - func notifyUpdate(_ events: [SplitInternalEvent]) { + func notifyUpdate(_ events: [SplitInternalEventWithMetadata]) { events.forEach { - eventsManager.notifyInternalEvent($0) + eventsManager.notifyInternalEvent($0.type, metadata: $0.metadata ?? SplitMetadata(type: "", value: "")) } } @@ -162,9 +162,9 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { do { let result = try syncHelper.sync(since: changeNumber, clearBeforeUpdate: clearCache) if result.success { - if !isSdkReadyTriggered() || - result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if !isSdkReadyTriggered() || result.featureFlagsUpdated.count > 0 { + let metadata = SplitMetadata(type: "Flags", value: result.featureFlagsUpdated.description) + notifyUpdate([SplitInternalEventWithMetadata(.splitsUpdated, metadata: metadata)]) } resetBackoffCounter() return true @@ -220,8 +220,9 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) if result.success { - if result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if result.featureFlagsUpdated.count > 0 { + let metadata = SplitMetadata(type: "Data", value: result.featureFlagsUpdated.description) + notifyUpdate([SplitInternalEventWithMetadata(.splitsUpdated, metadata: metadata)]) } resetBackoffCounter() return true diff --git a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift index 83bcfd4c0..d5c992731 100644 --- a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift +++ b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift @@ -11,14 +11,14 @@ import Foundation struct SyncResult { let success: Bool let changeNumber: Int64 - let featureFlagsUpdated: Bool + let featureFlagsUpdated: [String] } class SplitsSyncHelper { struct FetchResult { let till: Int64 - let featureFlagsUpdated: Bool + let splitNames: [String] } private let splitFetcher: HttpSplitFetcher @@ -85,8 +85,9 @@ class SplitsSyncHelper { var nextSince = since var attemptCount = 0 let goalTill = till ?? -10 + var result = FetchResult(till: 0, splitNames: []) while attemptCount < maxAttempts { - let result = try fetchUntil(since: nextSince, + result = try fetchUntil(since: nextSince, till: useTillParam ? till : nil, clearBeforeUpdate: clearBeforeUpdate, headers: headers) @@ -95,13 +96,13 @@ class SplitsSyncHelper { if nextSince >= goalTill { return SyncResult(success: true, changeNumber: nextSince, - featureFlagsUpdated: result.featureFlagsUpdated) + featureFlagsUpdated: result.splitNames) } Thread.sleep(forTimeInterval: backoffCounter.getNextRetryTime()) attemptCount+=1 } - return SyncResult(success: false, changeNumber: nextSince, featureFlagsUpdated: false) + return SyncResult(success: false, changeNumber: nextSince, featureFlagsUpdated: result.splitNames) } func fetchUntil(since: Int64, @@ -112,7 +113,8 @@ class SplitsSyncHelper { var clearCache = clearBeforeUpdate var firstFetch = true var nextSince = since - var featureFlagsUpdated = false + var splitNames: [String] = [] + while true { clearCache = clearCache && firstFetch let splitChange = try self.splitFetcher.execute(since: nextSince, @@ -124,14 +126,15 @@ class SplitsSyncHelper { splitsStorage.clear() } firstFetch = false - if splitsStorage.update(splitChange: splitChangeProcessor.process(splitChange)) { - featureFlagsUpdated = true + let processedSplits = splitChangeProcessor.process(splitChange) + if splitsStorage.update(splitChange: processedSplits) { + splitNames += processedSplits.archivedSplits.compactMap(\.name) + splitNames += processedSplits.activeSplits.compactMap(\.name) } Logger.i("Feature flag definitions have been updated") // Line below commented temporary for debug purposes - // Logger.v(splitChange.description) if newSince == newTill, newTill >= since { - return FetchResult(till: newTill, featureFlagsUpdated: featureFlagsUpdated) + return FetchResult(till: newTill, splitNames: splitNames) } nextSince = newTill } diff --git a/Split/Localhost/LocalhostClientManager.swift b/Split/Localhost/LocalhostClientManager.swift index 6679e6859..cf2b28917 100644 --- a/Split/Localhost/LocalhostClientManager.swift +++ b/Split/Localhost/LocalhostClientManager.swift @@ -92,8 +92,8 @@ class LocalhostClientManager: SplitClientManager { let newGroup = LocalhostComponentsGroup(client: newClient, eventsManager: newEventsManager) clients.setValue(newGroup, forKey: key.matchingKey) eventsManagerCoordinator.add(newEventsManager, forKey: key) - newEventsManager.notifyInternalEvent(.mySegmentsUpdated) - newEventsManager.notifyInternalEvent(.myLargeSegmentsUpdated) + newEventsManager.notifyInternalEvent(.mySegmentsUpdated, metadata: SplitMetadata(type: "Metadata forKey segments", value: "")) + newEventsManager.notifyInternalEvent(.myLargeSegmentsUpdated, metadata: SplitMetadata(type: "Metadata forKey largeSegments", value: "")) return newClient } diff --git a/Split/Localhost/LocalhostSynchronizer.swift b/Split/Localhost/LocalhostSynchronizer.swift index 7bc59ea28..489fa6b82 100644 --- a/Split/Localhost/LocalhostSynchronizer.swift +++ b/Split/Localhost/LocalhostSynchronizer.swift @@ -24,34 +24,27 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { setup() } - func load() { - } + func load() {} func synchronize() { featureFlagsDataSource.start() } - func synchronize(changeNumber: Int64) { - } + func synchronize(changeNumber: Int64) {} func startPeriodicSync() { featureFlagsDataSource.start() } - func stopPeriodicSync() { - } + func stopPeriodicSync() {} - func notifyKilled() { - } + func notifyKilled() {} - func notifyUpdated() { - } + func notifyUpdated(flagList: [String]) {} - func pause() { - } + func pause() {} - func resume() { - } + func resume() {} func destroy() { featureFlagsDataSource.stop() @@ -74,7 +67,8 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { // Update will remove all records before insert new ones _ = self.featureFlagsStorage.update(splitChange: change) - self.eventsManager.notifyInternalEvent(.splitsUpdated) + let metadata = SplitMetadata(type: "Splits Updated", value: featureFlags.values.description) + self.eventsManager.notifyInternalEvent(SplitInternalEventWithMetadata(.splitsUpdated, metadata: metadata)) } } } diff --git a/Split/Network/Streaming/PushNotificationManager.swift b/Split/Network/Streaming/PushNotificationManager.swift index 38aa4f28f..59e46de8e 100644 --- a/Split/Network/Streaming/PushNotificationManager.swift +++ b/Split/Network/Streaming/PushNotificationManager.swift @@ -134,7 +134,14 @@ class DefaultPushNotificationManager: PushNotificationManager { } Logger.d("Streaming authentication success") - let connectionDelay = result.sseConnectionDelay + var connectionDelay: Int64 = 0 + + #if DEBUG + connectionDelay = 1 + #else + connectionDelay = result.sseConnectionDelay + #endif + self.broadcasterChannel.push(event: .pushDelayReceived(delaySeconds: connectionDelay)) let lastId = lastConnId.value if connectionDelay > 0 { diff --git a/Split/Network/Streaming/SyncUpdateWorker.swift b/Split/Network/Streaming/SyncUpdateWorker.swift index 0a87fbd68..80cd6a02e 100644 --- a/Split/Network/Streaming/SyncUpdateWorker.swift +++ b/Split/Network/Streaming/SyncUpdateWorker.swift @@ -66,8 +66,14 @@ class SplitsUpdateWorker: UpdateWorker { since: previousChangeNumber, till: notification.changeNumber) Logger.v("Split update received: \(change)") - if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { - self.synchronizer.notifyFeatureFlagsUpdated() + + let processedFlags = self.splitChangeProcessor.process(change) + + if self.splitsStorage.update(splitChange: processedFlags) { + var updatedFlags: [String] = processedFlags.activeSplits.compactMap(\.name) + updatedFlags += processedFlags.archivedSplits.compactMap(\.name) + self.synchronizer.notifyFeatureFlagsUpdated(flagList: updatedFlags) + } self.telemetryProducer?.recordUpdatesFromSse(type: .splits) return diff --git a/Split/Network/Sync/ByKeyFacade.swift b/Split/Network/Sync/ByKeyFacade.swift index 5f7212bdf..777382fa1 100644 --- a/Split/Network/Sync/ByKeyFacade.swift +++ b/Split/Network/Sync/ByKeyFacade.swift @@ -77,7 +77,8 @@ class DefaultByKeyFacade: ByKeyFacade { func loadAttributesFromCache(forKey key: String) { doInAll(forMatchingKey: key) { group in group.attributesStorage.loadLocal() - group.eventsManager.notifyInternalEvent(.attributesLoadedFromCache) + let metadata = SplitMetadata(type: "Metadata", value: "Attributes from cache ready") + group.eventsManager.notifyInternalEvent(.attributesLoadedFromCache, metadata: metadata) } TimeChecker.logInterval("Time until attributes loaded from cache") } @@ -122,13 +123,15 @@ class DefaultByKeyFacade: ByKeyFacade { func notifyMySegmentsUpdated(forKey key: String) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.mySegmentsUpdated) + let metadata = SplitMetadata(type: "Metadata", value: "Segments updated for key \(key)") + group.eventsManager.notifyInternalEvent(.mySegmentsUpdated, metadata: metadata) } } func notifyMyLargeSegmentsUpdated(forKey key: String) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.myLargeSegmentsUpdated) + let metadata = SplitMetadata(type: "Metadata", value: "Large segments updated for key \(key)") + group.eventsManager.notifyInternalEvent(.myLargeSegmentsUpdated, metadata: metadata) } } diff --git a/Split/Network/Sync/FeatureFlagsSynchronizer.swift b/Split/Network/Sync/FeatureFlagsSynchronizer.swift index b733627c2..88b8c49ff 100644 --- a/Split/Network/Sync/FeatureFlagsSynchronizer.swift +++ b/Split/Network/Sync/FeatureFlagsSynchronizer.swift @@ -15,7 +15,7 @@ protocol FeatureFlagsSynchronizer { func startPeriodicSync() func stopPeriodicSync() func notifyKilled() - func notifyUpdated() + func notifyUpdated(flagList: [String]) func pause() func resume() func destroy() @@ -40,7 +40,7 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { syncWorkerFactory: SyncWorkerFactory, broadcasterChannel: SyncEventBroadcaster, syncTaskByChangeNumberCatalog: ConcurrentDictionary - = ConcurrentDictionary(), += ConcurrentDictionary(), splitsFilterQueryString: String, flagsSpec: String, splitEventsManager: SplitEventsManager) { @@ -85,6 +85,11 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { splitsStorage.loadLocal() if splitsStorage.getAll().count > 0 { self.splitEventsManager.notifyInternalEvent(.splitsLoadedFromCache) + + // Trigger event + let metadata = SplitMetadata(type: "Metadata", value: "Splits from cache ready") + let event = SplitInternalEventWithMetadata(.attributesLoadedFromCache, metadata: metadata) + self.splitEventsManager.notifyInternalEvent(event) } self.broadcasterChannel.push(event: .splitLoadedFromCache) Logger.v("Notifying Splits loaded from cache") @@ -142,8 +147,9 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { splitEventsManager.notifyInternalEvent(.splitKilledNotification) } - func notifyUpdated() { - splitEventsManager.notifyInternalEvent(.splitsUpdated) + func notifyUpdated(flagList: [String]) { + let metadata = SplitMetadata(type: "Updated Flags", value: flagList.description) + splitEventsManager.notifyInternalEvent(.splitsUpdated, metadata: metadata) } func pause() { diff --git a/Split/Network/Sync/Synchronizer.swift b/Split/Network/Sync/Synchronizer.swift index 4a8320fc7..9e3e796eb 100644 --- a/Split/Network/Sync/Synchronizer.swift +++ b/Split/Network/Sync/Synchronizer.swift @@ -29,7 +29,7 @@ protocol Synchronizer: ImpressionLogger { func startRecordingTelemetry() func stopRecordingTelemetry() func pushEvent(event: EventDTO) - func notifyFeatureFlagsUpdated() + func notifyFeatureFlagsUpdated(flagList: [String]) func notifySegmentsUpdated(forKey key: String) func notifyLargeSegmentsUpdated(forKey key: String) func notifySplitKilled() @@ -200,8 +200,8 @@ class DefaultSynchronizer: Synchronizer { } } - func notifyFeatureFlagsUpdated() { - featureFlagsSynchronizer.notifyUpdated() + func notifyFeatureFlagsUpdated(flagList: [String]) { + featureFlagsSynchronizer.notifyUpdated(flagList: flagList) } func notifySegmentsUpdated(forKey key: String) { diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 1d7685dc3..74105efcc 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -19,7 +19,7 @@ class SplitEventsManagerStub: SplitEventsManager { var startCalled = false var stopCalled = false - func notifyInternalEvent(_ event: SplitInternalEvent) { + func notifyInternalEvent(_ event: SplitInternalEventCase) { switch event { case .mySegmentsLoadedFromCache: mySegmentsLoadedEventFiredCount+=1 @@ -40,7 +40,7 @@ class SplitEventsManagerStub: SplitEventsManager { } var registeredEvents = [SplitEvent: SplitEventTask]() - func register(event: SplitEvent, task: SplitEventTask) { + func register(event: SplitEventCase, task: SplitEventTask) { registeredEvents[event] = task } @@ -52,7 +52,7 @@ class SplitEventsManagerStub: SplitEventsManager { stopCalled = true } - func eventAlreadyTriggered(event: SplitEvent) -> Bool { + func eventAlreadyTriggered(event: SplitEventCase) -> Bool { return false } } diff --git a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift index 6695c3cfd..d157ce347 100644 --- a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift +++ b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift @@ -40,7 +40,7 @@ class FeatureFlagsSynchronizerStub: FeatureFlagsSynchronizer { } var notifyUpdatedCalled = false - func notifyUpdated() { + func notifyUpdated(flagList: [String]) { notifyUpdatedCalled = true } diff --git a/SplitTests/Fake/Streaming/SynchronizerSpy.swift b/SplitTests/Fake/Streaming/SynchronizerSpy.swift index da1b1011b..16361a8e0 100644 --- a/SplitTests/Fake/Streaming/SynchronizerSpy.swift +++ b/SplitTests/Fake/Streaming/SynchronizerSpy.swift @@ -196,7 +196,7 @@ class SynchronizerSpy: Synchronizer { } var notifyFeatureFlagsUpdatedCalled = false - func notifyFeatureFlagsUpdated() { + func notifyFeatureFlagsUpdated(flagList: [String]) { notifyFeatureFlagsUpdatedCalled = true } diff --git a/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift b/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift index 4b5561382..bbbac9e1e 100644 --- a/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift +++ b/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift @@ -799,11 +799,11 @@ class FlagSetsIntegrationTests: XCTestCase { let client = factory?.client let sdkReadyExpectation = XCTestExpectation(description: "SDK READY Expectation") - client?.on(event: SplitEvent.sdkReady) { + client?.on(event: SplitEventCase.sdkReady) { sdkReadyExpectation.fulfill() } - client?.on(event: SplitEvent.sdkReadyTimedOut) { + client?.on(event: SplitEventCase.sdkReadyTimedOut) { sdkReadyExpectation.fulfill() } diff --git a/SplitTests/Integration/Recorder/TrackTest.swift b/SplitTests/Integration/Recorder/TrackTest.swift index d5b4e7fe6..9dd55a988 100644 --- a/SplitTests/Integration/Recorder/TrackTest.swift +++ b/SplitTests/Integration/Recorder/TrackTest.swift @@ -128,7 +128,7 @@ class TrackTest: XCTestCase { var sdkReadyFired = false - client.on(event: SplitEvent.sdkReady) { + client.on(event: SplitEventCase.sdkReady) { sdkReadyFired = true sdkReady.fulfill() } diff --git a/SplitTests/Integration/streaming/StreamingOccupancyTest.swift b/SplitTests/Integration/streaming/StreamingOccupancyTest.swift index b971a6c4d..7e7e8d428 100644 --- a/SplitTests/Integration/streaming/StreamingOccupancyTest.swift +++ b/SplitTests/Integration/streaming/StreamingOccupancyTest.swift @@ -45,11 +45,11 @@ class StreamingOccupancyTest: XCTestCase { let sdkReadyExpectation = XCTestExpectation(description: "SDK READY Expectation") - client.on(event: SplitEvent.sdkReady) { + client.on(event: SplitEventCase.sdkReady) { sdkReadyExpectation.fulfill() } - client.on(event: SplitEvent.sdkReadyTimedOut) { + client.on(event: SplitEventCase.sdkReadyTimedOut) { sdkReadyExpectation.fulfill() }