diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..0816760d3 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 @@ -57,26 +57,43 @@ public final class DefaultSplitClient: NSObject, SplitClient, TelemetrySplitClie // MARK: Events extension DefaultSplitClient { - - public func on(event: SplitEvent, execute action: @escaping SplitAction) { + + public func on(event: SplitEvent, perform: SplitAction?) { + guard let perform = perform else { return } + on(event: SplitEventWithMetadata(type: event, metadata: nil), execute: perform) + } + + public func on(event: SplitEventWithMetadata, 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: SplitEventWithMetadata, 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: SplitEventWithMetadata, queue: DispatchQueue, execute action: @escaping SplitAction) { on(event: event, runInBackground: true, queue: queue, execute: action) } + + private func on(event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + guard let factory = clientManager?.splitFactory else { + return + } - private func on(event: SplitEvent, - runInBackground: Bool, - queue: DispatchQueue?, - execute action: @escaping SplitAction) { - + let task = SplitEventActionTask(action: action, event: event, + runInBackground: runInBackground, + factory: factory, + queue: queue) + task.event = event + on(event: event, executeTask: task) + } + + public func on(event: SplitEvent, performWithMetadata action: SplitActionWithMetadata?) { + guard let action = action else { return } + onWithMetadata(event:SplitEventWithMetadata(type: event, metadata: nil), runInBackground: true, queue: nil, execute: action) + } + + private func onWithMetadata(event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitActionWithMetadata) { guard let factory = clientManager?.splitFactory else { return } @@ -88,16 +105,20 @@ extension DefaultSplitClient { 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, " + + private func on(event: SplitEventWithMetadata, executeTask task: SplitEventActionTask) { + if event.type != .sdkReadyFromCache, + eventsManager.eventAlreadyTriggered(event: event.type) { + Logger.w("A handler was added for \(event.type.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..50277542e 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -13,6 +13,9 @@ import Foundation /// class FailedClient: SplitClient { + func on(event: SplitEvent, performWithMetadata: SplitActionWithMetadata?) { + + } func getTreatment(_ split: String) -> String { return SplitConstants.control @@ -53,15 +56,21 @@ 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: SplitEventWithMetadata, execute action: @escaping SplitAction) { } - func on(event: SplitEvent, runInBackground: Bool, + func on(event: SplitEventWithMetadata, runInBackground: Bool, execute action: @escaping SplitAction) { } - func on(event: SplitEvent, + func on(event: SplitEventWithMetadata, queue: DispatchQueue, execute action: @escaping SplitAction) { } diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..2f9cf44dc 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 @@ -120,21 +117,35 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } return results } + + public func on(event: SplitEvent, perform: SplitAction?) { + guard let perform = perform else { return } + on(event: SplitEventWithMetadata(type: event, metadata: nil), execute: perform) + } + + public func on(event: SplitEvent, performWithMetadata: SplitActionWithMetadata?) { + guard let performWithMetadata = performWithMetadata else { return } + on(eventWithMetadata: SplitEventWithMetadata(type: event, metadata: nil), runInBackground: false, queue: nil, execute: performWithMetadata) + } - public func on(event: SplitEvent, runInBackground: Bool, + public func on(event: SplitEventWithMetadata, 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: SplitEventWithMetadata, queue: DispatchQueue, execute action: @escaping SplitAction) { on(event: event, runInBackground: true, queue: queue, execute: action) } - public func on(event: SplitEvent, execute action: @escaping SplitAction) { + public func on(event: SplitEventWithMetadata, execute action: @escaping SplitAction) { on(event: event, runInBackground: false, queue: nil, execute: action) } - private func on(event: SplitEvent, runInBackground: Bool, + private func on(event: SplitEventWithMetadata, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { + on(eventWithMetadata: event, 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 } @@ -146,6 +157,19 @@ public final class LocalhostSplitClient: NSObject, SplitClient { 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, + runInBackground: runInBackground, + factory: factory, + queue: queue) + eventsManager.register(event: event, task: task) + } + } public func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index ff4df2005..9d0c9a1b4 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -9,6 +9,7 @@ import Foundation public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (_ data: Any?) -> Void @objc public protocol SplitClient { @@ -32,11 +33,13 @@ public typealias SplitAction = () -> Void 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) - + + func on(event: SplitEvent, perform: SplitAction?) -> Void + func on(event: SplitEvent, performWithMetadata: SplitActionWithMetadata?) -> Void + func on(event: SplitEventWithMetadata, execute action: @escaping SplitAction) + func on(event: SplitEventWithMetadata, runInBackground: Bool, execute action: @escaping SplitAction) + func on(event: SplitEventWithMetadata, queue: DispatchQueue, execute action: @escaping SplitAction) + // MARK: Track feature func track(trafficType: String, eventType: String) -> Bool func track(trafficType: String, eventType: String, value: Double) -> Bool diff --git a/Split/Api/SplitClientManager.swift b/Split/Api/SplitClientManager.swift index 6cdbf743a..43cbbff1b 100644 --- a/Split/Api/SplitClientManager.swift +++ b/Split/Api/SplitClientManager.swift @@ -80,7 +80,7 @@ class DefaultClientManager: SplitClientManager { eventsManagerCoordinator.start() if let producer = telemetryProducer { - defaultClient?.on(event: .sdkReadyFromCache) { + defaultClient?.on(event: SplitEventWithMetadata(type: .sdkReadyFromCache, metadata: nil)) { DispatchQueue.general.async { [weak self] in if let self = self { producer.recordTimeUntilReadyFromCache(self.telemetryStopwatch?.interval() ?? 0) @@ -88,7 +88,7 @@ class DefaultClientManager: SplitClientManager { } } - defaultClient?.on(event: .sdkReady) { + defaultClient?.on(event: SplitEventWithMetadata(type: .sdkReadyFromCache, metadata: nil)) { DispatchQueue.general.async { [weak self] in if let self = self { producer.recordTimeUntilReady(self.telemetryStopwatch?.interval() ?? 0) diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..dec9f3af1 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -14,17 +14,18 @@ protocol SplitEventsManagerCoordinator: SplitEventsManager { } class MainSplitEventsManager: SplitEventsManagerCoordinator { + private var defaultManager: SplitEventsManager? private var managers = [Key: SplitEventsManager]() - private var triggered = Set() + private var triggered = Set() private let queue = DispatchQueue(label: "split-event-manager-coordinator") - private let eventsToHandle: Set = Set( + private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, .splitsUpdated, .splitKilledNotification] ) - func notifyInternalEvent(_ event: SplitInternalEvent) { + func notifyInternalEvent(_ event: SplitEventCase, _ metadata: [String : Any]? = nil) { if !eventsToHandle.contains(event) { return } @@ -33,10 +34,14 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { self.triggered.insert(event) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event) + manager.notifyInternalEvent(event, metadata) } } } + + func notifyInternalEventWithMetadata(_ event: SplitInternalEvent) { + notifyInternalEvent(event.type, event.metadata) + } func start() {} @@ -76,5 +81,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..0264c8ead 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -26,3 +26,24 @@ import Foundation } } } + +@objcMembers +public class SplitEventWithMetadata: NSObject { + let type: SplitEvent + let metadata: [String: Any]? + + @objc public init(type: SplitEvent, metadata: [String : Any]? = 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 + } + + public override var hash: Int { + return type.hashValue + } +} + diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index be1368d04..a36fe0253 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -8,19 +8,23 @@ import Foundation class SplitEventActionTask: SplitEventTask { - + private var eventHandler: SplitAction? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? - var event: SplitEvent + var event: SplitEventWithMetadata 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: SplitEventWithMetadata, 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: SplitEventWithMetadata, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { self.eventHandler = action self.event = event self.runInBackground = runInBackground @@ -33,7 +37,8 @@ class SplitEventActionTask: SplitEventTask { return queue } - func run() { + func run(_ data: Any?) { eventHandler?() + eventHandlerWithMetadata?(data) } } diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitEventCase.swift similarity index 50% rename from Split/Events/SplitInternalEvent.swift rename to Split/Events/SplitEventCase.swift index 4c9521204..a2c3640e8 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitEventCase.swift @@ -7,7 +7,7 @@ import Foundation -enum SplitInternalEvent { +enum SplitEventCase { case mySegmentsUpdated case myLargeSegmentsUpdated case splitsUpdated @@ -18,3 +18,17 @@ enum SplitInternalEvent { case sdkReadyTimeoutReached case splitKilledNotification } + +struct SplitInternalEvent: Equatable { + let type: SplitEventCase + let metadata: [String: Any]? + + init(type: SplitEventCase, metadata: [String : Any]? = nil) { + self.type = type + self.metadata = metadata + } + + static func == (lhs: SplitInternalEvent, rhs: SplitInternalEvent) -> Bool { + return lhs.type == rhs.type + } +} diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..f07cd5615 100644 --- a/Split/Events/SplitEventTask.swift +++ b/Split/Events/SplitEventTask.swift @@ -8,8 +8,8 @@ import Foundation protocol SplitEventTask { - var event: SplitEvent { get } + var event: SplitEventWithMetadata { get } var runInBackground: Bool { get } func takeQueue() -> DispatchQueue? - func run() + func run(_ data: Any?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 56774453b..e0c31f857 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -9,14 +9,25 @@ import Foundation protocol SplitEventsManager: AnyObject { - func register(event: SplitEvent, task: SplitEventTask) - func notifyInternalEvent(_ event: SplitInternalEvent) + func register(event: SplitEventWithMetadata, task: SplitEventActionTask) + func notifyInternalEvent(_ event: SplitEventCase, _ metadata: [String: Any]?) + func notifyInternalEventWithMetadata(_ event: SplitInternalEvent) func start() func stop() func eventAlreadyTriggered(event: SplitEvent) -> Bool } +/* This overload is intentionally 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. */ +extension SplitEventsManager { + func notifyInternalEvent(_ event: SplitEventCase) { + notifyInternalEvent(event, nil) + } +} + class DefaultSplitEventsManager: SplitEventsManager { + private let readingRefreshTime: Int private var sdkReadyTimeStart: Int64 @@ -28,6 +39,8 @@ class DefaultSplitEventsManager: SplitEventsManager { 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) @@ -44,44 +57,46 @@ 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(SplitEventCase.sdkReadyTimeoutReached) } } } - - func notifyInternalEvent(_ event: SplitInternalEvent) { + + func notifyInternalEvent(_ event: SplitEventCase, _ metadata: [String: Any]? = nil) { + notifyInternalEventWithMetadata(SplitInternalEvent(type: event, metadata: metadata)) + } + + func notifyInternalEventWithMetadata(_ event: SplitInternalEvent) { processQueue.async { [weak self] in if let self = self { - Logger.v("Event \(event) notified") + Logger.v("Event \(event.type) notified - Metadata: \(event.metadata ?? [:])") self.eventsQueue.add(event) } } } - func register(event: SplitEvent, task: SplitEventTask) { - let eventName = event.toString() + 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() } } @@ -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) + 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) + } } - 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,16 +230,16 @@ 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() + let eventName = task.event.type.toString() if task.runInBackground { TimeChecker.logInterval("Previous to run \(eventName) in Background") @@ -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 { return triggered.filter { $0 == event }.count > 0 } + + private func isTriggered(internal event: SplitEventCase) -> Bool { + return isTriggered(internal: SplitInternalEvent(type: event, metadata: nil)) + } // MARK: Safe Data Access func executionTimes(for eventName: String) -> Int? { diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index 66065d934..10d245bb2 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -132,7 +132,7 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { func notifyUpdate(_ events: [SplitInternalEvent]) { events.forEach { - eventsManager.notifyInternalEvent($0) + eventsManager.notifyInternalEventWithMetadata($0) } } } @@ -171,8 +171,15 @@ 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) + } + + notifyUpdate([SplitInternalEvent(type: .splitsUpdated, metadata: ["Updated Flags": updatedFlags])]) + } } } @@ -211,9 +218,8 @@ 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 { + notifyUpdate([SplitInternalEvent(type: .mySegmentsUpdated, metadata: ["Updated segments": result.msUpdated + result.mlsUpdated ])]) } } } catch { diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 36973b66b..67e1e9e59 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -46,11 +46,10 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { if result.success { 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([SplitInternalEvent(type: .mySegmentsUpdated, metadata: ["Segments updated" : result.msUpdated ])]) + notifyUpdate([SplitInternalEvent(type: .myLargeSegmentsUpdated, metadata: ["Large segments updated" : result.mlsUpdated ])]) + } else if !result.msUpdated.isEmpty || !result.mlsUpdated.isEmpty { + notifyUpdate([SplitInternalEvent(type: .mySegmentsUpdated, metadata: ["Segments updated" : result.msUpdated + result.mlsUpdated ])]) } return true } @@ -71,8 +70,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 +85,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 +164,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 +174,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 +195,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 +204,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..d8ccbd39e 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -81,7 +81,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { func notifyUpdate(_ events: [SplitInternalEvent]) { events.forEach { - eventsManager.notifyInternalEvent($0) + eventsManager.notifyInternalEvent($0.type, $0.metadata) } } @@ -162,9 +162,8 @@ 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 { + notifyUpdate([SplitInternalEvent(type: .splitsUpdated, metadata: ["Flags": result.featureFlagsUpdated ])]) } resetBackoffCounter() return true @@ -220,8 +219,8 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) if result.success { - if result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + if result.featureFlagsUpdated.count > 0 { + notifyUpdate([SplitInternalEvent(type: .splitsUpdated, metadata: ["Metadata" : result.featureFlagsUpdated.description]) ]) } 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..bcc70ee2d 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.notifyInternalEventWithMetadata(SplitInternalEvent(type: .mySegmentsUpdated, metadata: ["Metadata forKey segments": ""])) + newEventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .myLargeSegmentsUpdated, metadata: ["Metadata forKey largeSegments": ""])) return newClient } diff --git a/Split/Localhost/LocalhostSynchronizer.swift b/Split/Localhost/LocalhostSynchronizer.swift index 7bc59ea28..3785ca971 100644 --- a/Split/Localhost/LocalhostSynchronizer.swift +++ b/Split/Localhost/LocalhostSynchronizer.swift @@ -44,7 +44,7 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { func notifyKilled() { } - func notifyUpdated() { + func notifyUpdated(flagList: [String]) { } func pause() { @@ -74,7 +74,7 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { // Update will remove all records before insert new ones _ = self.featureFlagsStorage.update(splitChange: change) - self.eventsManager.notifyInternalEvent(.splitsUpdated) + self.eventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .splitsUpdated, metadata: ["Splits updated":"\(featureFlags.values.description)"])) } } } 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..7642dad39 100644 --- a/Split/Network/Sync/ByKeyFacade.swift +++ b/Split/Network/Sync/ByKeyFacade.swift @@ -77,7 +77,7 @@ class DefaultByKeyFacade: ByKeyFacade { func loadAttributesFromCache(forKey key: String) { doInAll(forMatchingKey: key) { group in group.attributesStorage.loadLocal() - group.eventsManager.notifyInternalEvent(.attributesLoadedFromCache) + group.eventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .attributesLoadedFromCache, metadata: ["Metadata":"Attributes from cache ready"])) } TimeChecker.logInterval("Time until attributes loaded from cache") } @@ -122,13 +122,13 @@ class DefaultByKeyFacade: ByKeyFacade { func notifyMySegmentsUpdated(forKey key: String) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.mySegmentsUpdated) + group.eventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .mySegmentsUpdated, metadata: ["Metadata":"Segments updated for key \(key)"])) } } func notifyMyLargeSegmentsUpdated(forKey key: String) { doInAll(forMatchingKey: key) { group in - group.eventsManager.notifyInternalEvent(.myLargeSegmentsUpdated) + group.eventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .myLargeSegmentsUpdated, metadata: ["Metadata":"Large segments updated for key \(key)"])) } } diff --git a/Split/Network/Sync/FeatureFlagsSynchronizer.swift b/Split/Network/Sync/FeatureFlagsSynchronizer.swift index b733627c2..5d3d466fe 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,7 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { splitsStorage.loadLocal() if splitsStorage.getAll().count > 0 { self.splitEventsManager.notifyInternalEvent(.splitsLoadedFromCache) + self.splitEventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .attributesLoadedFromCache, metadata: ["Metadata":"Splits from cache ready"])) } self.broadcasterChannel.push(event: .splitLoadedFromCache) Logger.v("Notifying Splits loaded from cache") @@ -142,8 +143,8 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { splitEventsManager.notifyInternalEvent(.splitKilledNotification) } - func notifyUpdated() { - splitEventsManager.notifyInternalEvent(.splitsUpdated) + func notifyUpdated(flagList: [String]) { + splitEventsManager.notifyInternalEventWithMetadata(SplitInternalEvent(type: .splitsUpdated, metadata: ["Updated flags:":flagList] )) } 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) {