From 9fcc1b30e81fd2c0a2b451388e082ad5344eeb23 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Fri, 8 Aug 2025 14:10:29 -0300 Subject: [PATCH 01/35] Initial commit --- Split/Common/Structs/BlockingQueue.swift | 10 ++--- Split/Events/SplitEventsManager.swift | 2 +- Split/Events/SplitInternalEvent.swift | 41 +++++++++++++++++++ .../Refresh/PeriodicSyncWorker.swift | 2 +- 4 files changed, 48 insertions(+), 7 deletions(-) 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/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index ea64d0279..037643561 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -179,7 +179,7 @@ class DefaultSplitEventsManager: SplitEventsManager { func isTriggered(external event: SplitEvent) -> Bool { var triggered = false dataAccessQueue.sync { - if let times = executionTimes[event.toString()] { + if let times = executionTimes[event] { triggered = (times == 0) } else { triggered = false diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 4c9521204..424926a8f 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -7,6 +7,47 @@ import Foundation +// All events (internal & external) support metadata. +// Internal errors are propagated to the customer as events "(.sdkError)". The error info will travel as the event metadata. +struct SplitInternalEventWithMetadata { + let type: SplitInternalEvent + let metadata: EventMetadata? + + init(_ type: SplitInternalEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } + + static func == (lhs: SplitInternalEventWithMetadata, rhs: SplitInternalEventWithMetadata) -> Bool { + return lhs.type == rhs.type + } +} + +@objc public class EventMetadata: NSObject { + var type: EventMetadataType + var data: String = "" + + init(type: EventMetadataType, data: String) { + self.type = type + self.data = data + } +} + +enum EventMetadataType: Int { + case FEATURE_FLAGS_SYNC_ERROR + case SEGMENTS_SYNC_ERROR + + public func toString() -> String { + switch self { + case .FEATURE_FLAGS_SYNC_ERROR: + return "FEATURE_FLAGS_SYNC_ERROR" + case .SEGMENTS_SYNC_ERROR: + return "SEGMENTS_SYNC_ERROR" + + } + } +} + enum SplitInternalEvent { case mySegmentsUpdated case myLargeSegmentsUpdated diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index b6fd71c81..a41a03205 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -225,7 +225,7 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { if result.success { if result.msUpdated || result.mlsUpdated { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + notifyUpdate([.mySegmentsUpdated(nil)]) } } } catch { From d3424316a88e36fdf9a24c3587ffdf846dc8b19f Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Fri, 8 Aug 2025 15:32:38 -0300 Subject: [PATCH 02/35] New SplitEventWithMetadata --- Split/Api/SplitClient.swift | 1 + Split/Common/Structs/BlockingQueue.swift | 5 + Split/Events/EventsManagerCoordinator.swift | 15 ++- Split/Events/SplitEvent.swift | 18 +++ Split/Events/SplitEventActionTask.swift | 8 +- Split/Events/SplitEventTask.swift | 2 +- Split/Events/SplitEventsManager.swift | 111 +++++++++++------- Split/Events/SplitInternalEvent.swift | 1 + .../Refresh/PeriodicSyncWorker.swift | 2 +- .../Collections/BlockingQueueTest.swift | 28 ++--- 10 files changed, 127 insertions(+), 64 deletions(-) diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index ff4df2005..49726585c 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -9,6 +9,7 @@ import Foundation public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (EventMetadata) -> Void @objc public protocol SplitClient { diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index df5bfbb6f..739a29507 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -80,6 +80,11 @@ protocol InternalEventBlockingQueue { class DefaultInternalEventBlockingQueue: InternalEventBlockingQueue { let blockingQueue = GenericBlockingQueue() + + func add(_ item: SplitInternalEvent) { + blockingQueue.add(SplitInternalEventWithMetadata(item, metadata: nil)) + } + func add(_ item: SplitInternalEventWithMetadata) { blockingQueue.add(item) } diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..fc265513d 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -21,19 +21,24 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, .splitsUpdated, - .splitKilledNotification] + .splitKilledNotification, + .sdkError] ) - + func notifyInternalEvent(_ event: SplitInternalEvent) { - if !eventsToHandle.contains(event) { + notifyInternalEventWithMetadata(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEventWithMetadata(_ event: SplitInternalEventWithMetadata) { + if !eventsToHandle.contains(event.type) { return } queue.async { [weak self] in guard let self = self else { return } - self.triggered.insert(event) + self.triggered.insert(event.type) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event) + manager.notifyInternalEvent(event.type) } } } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index d2561e2d9..09e354c69 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -7,11 +7,27 @@ import Foundation +@objcMembers public class SplitEventWithMetadata: NSObject { + let type: SplitEvent + let metadata: EventMetadata? + + @objc public init(type: SplitEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? SplitEventWithMetadata else { return false } + return self.type == other.type + } +} + @objc public enum SplitEvent: Int { case sdkReady case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated + case sdkError public func toString() -> String { switch self { @@ -23,6 +39,8 @@ import Foundation return "SDK_READY_TIMED_OUT" case .sdkReadyFromCache: return "SDK_READY_FROM_CACHE" + case .sdkError: + return "SDK_ERROR" } } } diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index be1368d04..13d96f706 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -10,7 +10,9 @@ import Foundation 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 @@ -33,7 +35,9 @@ class SplitEventActionTask: SplitEventTask { return queue } - func run() { - eventHandler?() + func run(_ metadata: EventMetadata?) { + if let metadata = metadata { + eventHandlerWithMetadata?(metadata) + } } } diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..147bc8441 100644 --- a/Split/Events/SplitEventTask.swift +++ b/Split/Events/SplitEventTask.swift @@ -11,5 +11,5 @@ protocol SplitEventTask { var event: SplitEvent { get } var runInBackground: Bool { get } func takeQueue() -> DispatchQueue? - func run() + func run(_ metadata: EventMetadata?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 037643561..dd33d42d1 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -23,7 +23,7 @@ class DefaultSplitEventsManager: SplitEventsManager { private var subscriptions = [SplitEvent: [SplitEventTask]]() private var executionTimes: [String: Int] - private var triggered: [SplitInternalEvent] + private var triggered: [SplitInternalEventWithMetadata] private let processQueue: DispatchQueue private let dataAccessQueue: DispatchQueue private var isStarted: Bool @@ -35,7 +35,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() @@ -49,11 +49,20 @@ class DefaultSplitEventsManager: SplitEventsManager { } } + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + processQueue.async { [weak self] in + if let self = self { + Logger.v("Event \(event.type) notified") + self.eventsQueue.add(event) + } + } + } + func notifyInternalEvent(_ event: SplitInternalEvent) { processQueue.async { [weak self] in if let self = self { Logger.v("Event \(event) notified") - self.eventsQueue.add(event) + self.eventsQueue.add(SplitInternalEventWithMetadata(event, metadata: nil)) } } } @@ -70,6 +79,19 @@ class DefaultSplitEventsManager: SplitEventsManager { self.subscribe(task: task, to: event) } } + + func register(event: SplitEventWithMetadata, task: SplitEventTask) { + 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) + return + } + self.subscribe(task: task, to: event) + } + } func start() { dataAccessQueue.sync { @@ -128,7 +150,7 @@ class DefaultSplitEventsManager: SplitEventsManager { return isRunning } - private func takeEvent() -> SplitInternalEvent? { + private func takeEvent() -> SplitInternalEventWithMetadata? { do { return try eventsQueue.take() } catch BlockingQueueError.hasBeenStopped { @@ -144,42 +166,45 @@ class DefaultSplitEventsManager: SplitEventsManager { guard let event = takeEvent() else { return } - self.triggered.append(event) - switch event { - case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - self.triggerSdkReadyIfNeeded() - - case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, - .splitsLoadedFromCache, .attributesLoadedFromCache: - Logger.v("Event \(event) triggered") - if isTriggered(internal: .splitsLoadedFromCache), - isTriggered(internal: .mySegmentsLoadedFromCache), - isTriggered(internal: .myLargeSegmentsLoadedFromCache), - isTriggered(internal: .attributesLoadedFromCache) { - trigger(event: SplitEvent.sdkReadyFromCache) - } - case .splitKilledNotification: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - case .sdkReadyTimeoutReached: - if !isTriggered(external: .sdkReady) { - trigger(event: SplitEvent.sdkReadyTimedOut) - } + triggered.append(event.type) + switch event.type { + case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated) + continue + } + triggerSdkReadyIfNeeded() + + case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, + .splitsLoadedFromCache, .attributesLoadedFromCache: + Logger.v("Event \(event) triggered") + if isTriggered(internal: .splitsLoadedFromCache), + isTriggered(internal: .mySegmentsLoadedFromCache), + isTriggered(internal: .myLargeSegmentsLoadedFromCache), + isTriggered(internal: .attributesLoadedFromCache) { + trigger(event: .sdkReadyFromCache) + } + case .splitKilledNotification: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated) + } + case .sdkReadyTimeoutReached: + if !isTriggered(external: .sdkReady) { + trigger(event: .sdkReadyTimedOut) + } + case .sdkError: + if !isTriggered(external: .sdkReady) { + trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) + } } } } - // MARK: Helper functions. + // MARK: Helper functions func isTriggered(external event: SplitEvent) -> Bool { var triggered = false dataAccessQueue.sync { - if let times = executionTimes[event] { + if let times = executionTimes[event.toString()] { triggered = (times == 0) } else { triggered = false @@ -199,9 +224,9 @@ class DefaultSplitEventsManager: SplitEventsManager { self.trigger(event: .sdkReady) } } - - private func trigger(event: SplitEvent) { - let eventName = event.toString() + + private func trigger(event: SplitInternalEvent) { + let eventName = event.type.toString() // If executionTimes is zero, maximum executions has been reached if executionTimes(for: eventName) == 0 { @@ -222,7 +247,7 @@ class DefaultSplitEventsManager: SplitEventsManager { } } - private func executeTask(event: SplitEvent, task: SplitEventTask) { + private func executeTask(event: SplitEventWithMetadata, task: SplitEventTask) { let eventName = task.event.toString() @@ -232,7 +257,7 @@ class DefaultSplitEventsManager: SplitEventsManager { let queue = task.takeQueue() ?? DispatchQueue.general queue.async { TimeChecker.logInterval("Running \(eventName) in Background queue \(queue)") - task.run() + task.run(event.metadata) } return } @@ -240,13 +265,17 @@ class DefaultSplitEventsManager: SplitEventsManager { DispatchQueue.main.async { TimeChecker.logInterval("Running event on main: \(eventName)") // UI Updates - task.run() + task.run(event.metadata) } } - - private func isTriggered(internal event: SplitInternalEvent) -> Bool { + + private func isTriggered(internal event: SplitInternalEventWithMetadata) -> Bool { return triggered.filter { $0 == event }.count > 0 } + + private func isTriggered(internal event: SplitInternalEvent) -> Bool { + return isTriggered(internal: SplitInternalEventWithMetadata(event, metadata: nil)) + } // MARK: Safe Data Access func executionTimes(for eventName: String) -> Int? { diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 424926a8f..c38f89cf4 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -58,4 +58,5 @@ enum SplitInternalEvent { case attributesLoadedFromCache case sdkReadyTimeoutReached case splitKilledNotification + case sdkError } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index a41a03205..b6fd71c81 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -225,7 +225,7 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { if result.success { if result.msUpdated || result.mlsUpdated { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated(nil)]) + notifyUpdate([.mySegmentsUpdated]) } } } catch { diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index 04c4b5df3..ace9ad58b 100644 --- a/SplitTests/Collections/BlockingQueueTest.swift +++ b/SplitTests/Collections/BlockingQueueTest.swift @@ -25,7 +25,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) if local.count == 4 { endExp.fulfill() } @@ -60,7 +60,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) } catch { endExp.fulfill() interrupted = true @@ -105,8 +105,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took: \(event)") + local.append(event.type) + print("Took: \(event.type)") } catch { } } @@ -117,8 +117,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took QA1: \(event)") + local.append(event.type) + print("Took QA1: \(event.type)") } catch { print("\n\n\nERROR!!!!: \(error) \n\n\n") } @@ -129,9 +129,9 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) + local.append(event.type) Thread.sleep(forTimeInterval: 0.3) - print("Took QA2: \(event)") + print("Took QA2: \(event.type)") } catch { } } @@ -142,8 +142,8 @@ class BlockingQueueTest: XCTestCase { do { Thread.sleep(forTimeInterval: 0.5) let event = try queue.take() - local.append(event) - print("Took QA3: \(event)") + local.append(event.type) + print("Took QA3: \(event.type)") } catch { } } @@ -151,7 +151,7 @@ class BlockingQueueTest: XCTestCase { qu1.async { for _ in 1..<100000 { - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(SplitInternalEventWithMetadata(.splitsUpdated, metadata: nil)) print("qu1 add") Thread.sleep(forTimeInterval: 0.2) } @@ -160,7 +160,7 @@ class BlockingQueueTest: XCTestCase { qu2.async { for _ in 1..<10000 { print("qu2 add") - queue.add(SplitInternalEvent.sdkReadyTimeoutReached) + queue.add(SplitInternalEventWithMetadata(.sdkReadyTimeoutReached, metadata: nil)) Thread.sleep(forTimeInterval: 0.5) } } @@ -168,7 +168,7 @@ class BlockingQueueTest: XCTestCase { qu3.async { for _ in 1..<10000 { print("qu3 add") - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(SplitInternalEventWithMetadata(.splitsUpdated, metadata: nil)) Thread.sleep(forTimeInterval: 0.8) } } @@ -176,7 +176,7 @@ class BlockingQueueTest: XCTestCase { qu4.async { for _ in 1..<10000 { print("qu4 add") - queue.add(SplitInternalEvent.mySegmentsUpdated) + queue.add(SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: nil)) sleep(1) } } From 7378c38248708196f2a149f5f94df394bc9f9a28 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Fri, 8 Aug 2025 19:08:16 -0300 Subject: [PATCH 03/35] Switching branch --- Split/Events/SplitEventsManager.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index dd33d42d1..7af5f682d 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -89,7 +89,7 @@ class DefaultSplitEventsManager: SplitEventsManager { self.executeTask(event: event, task: task) return } - self.subscribe(task: task, to: event) + self.subscribe(task: task, to: event.type) } } @@ -166,7 +166,7 @@ class DefaultSplitEventsManager: SplitEventsManager { guard let event = takeEvent() else { return } - triggered.append(event.type) + triggered.append(event) switch event.type { case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: if isTriggered(external: .sdkReady) { @@ -225,8 +225,8 @@ class DefaultSplitEventsManager: SplitEventsManager { } } - private func trigger(event: SplitInternalEvent) { - let eventName = event.type.toString() + private func trigger(event: SplitEvent) { + let eventName = event.toString() // If executionTimes is zero, maximum executions has been reached if executionTimes(for: eventName) == 0 { @@ -246,6 +246,10 @@ class DefaultSplitEventsManager: SplitEventsManager { } } } + + private func executeTask(event: SplitEvent, task: SplitEventTask) { + executeTask(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) + } private func executeTask(event: SplitEventWithMetadata, task: SplitEventTask) { From f68cfb0a8d712b04d7fe3eb7c4c342d0b81645e3 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 12:25:14 -0300 Subject: [PATCH 04/35] Stable point --- Split/Api/DefaultSplitClient.swift | 21 +++++++--- Split/Api/FailHelpers.swift | 4 ++ Split/Api/LocalhostSplitClient.swift | 4 ++ Split/Api/SplitClient.swift | 1 + Split/Events/EventsManagerCoordinator.swift | 3 ++ Split/Events/SplitEventActionTask.swift | 27 ++++++++----- Split/Events/SplitEventsManager.swift | 29 +++++--------- SplitTests/Fake/InternalSplitClientStub.swift | 4 ++ .../SplitEventsManagerCoordinatorStub.swift | 4 ++ SplitTests/Fake/SplitClientStub.swift | 3 ++ SplitTests/Fake/SplitEventsManagerMock.swift | 4 ++ SplitTests/Fake/SplitEventsManagerStub.swift | 4 ++ SplitTests/Helpers/IntegrationHelper.swift | 38 +++++++++--------- SplitTests/SplitEventsManagerTest.swift | 40 ++++++++++++------- 14 files changed, 118 insertions(+), 68 deletions(-) diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..1173d3e57 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -88,15 +88,24 @@ extension DefaultSplitClient { task.event = event on(event: event, executeTask: task) } + + 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: SplitEventTask) { - if event != .sdkReadyFromCache, - eventsManager.eventAlreadyTriggered(event: event) { - Logger.w("A handler was added for \(event.toString()) on the SDK, " + - "which has already fired and won’t be emitted again. The callback won’t be executed.") + private func on(event: SplitEvent, executeTask task: SplitEventActionTask) { + if event != .sdkReadyFromCache, eventsManager.eventAlreadyTriggered(event: event) { + Logger.w("A handler was added for \(event.toString()) on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.") return } - eventsManager.register(event: event, task: task) + eventsManager.register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } } diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 6f90ba7b7..2e9a78020 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -64,6 +64,10 @@ class FailedClient: SplitClient { func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + + } func track(trafficType: String, eventType: String) -> Bool { return false diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..203370ada 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -146,6 +146,10 @@ public final class LocalhostSplitClient: NSObject, SplitClient { eventsManager.register(event: event, task: task) } } + + public func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + + } public func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index 49726585c..4c81639d6 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -35,6 +35,7 @@ public typealias SplitActionWithMetadata = (EventMetadata) -> Void func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] func on(event: SplitEvent, execute action: @escaping SplitAction) + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index fc265513d..8ce38ab1a 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() @@ -82,4 +83,6 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { } func register(event: SplitEvent, task: SplitEventTask) {} + + func register(event: SplitEventWithMetadata, task: any SplitEventTask) {} } diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index 13d96f706..3d879e83e 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -17,18 +17,21 @@ class SplitEventActionTask: SplitEventTask { var runInBackground: Bool = false var factory: SplitFactory - init(action: @escaping SplitAction, - event: SplitEvent, - runInBackground: Bool = false, - factory: SplitFactory, - queue: DispatchQueue? = nil) { - - self.eventHandler = action - self.event = event - self.runInBackground = runInBackground - self.queue = queue - self.factory = factory + init(action: @escaping SplitActionWithMetadata, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + self.eventHandlerWithMetadata = action + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory } + + init(action: @escaping SplitAction, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + self.eventHandler = action + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory + } func takeQueue() -> DispatchQueue? { defer { queue = nil } @@ -36,6 +39,8 @@ class SplitEventActionTask: SplitEventTask { } func run(_ metadata: EventMetadata?) { + eventHandler?() + if let metadata = metadata { eventHandlerWithMetadata?(metadata) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 7af5f682d..878ef97d9 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -10,6 +10,7 @@ import Foundation protocol SplitEventsManager: AnyObject { func register(event: SplitEvent, task: SplitEventTask) + func register(event: SplitEventWithMetadata, task: SplitEventTask) func notifyInternalEvent(_ event: SplitInternalEvent) func start() func stop() @@ -59,25 +60,11 @@ class DefaultSplitEventsManager: SplitEventsManager { } func notifyInternalEvent(_ event: SplitInternalEvent) { - processQueue.async { [weak self] in - if let self = self { - Logger.v("Event \(event) notified") - self.eventsQueue.add(SplitInternalEventWithMetadata(event, metadata: nil)) - } - } + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) } func register(event: SplitEvent, task: SplitEventTask) { - let eventName = event.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) - return - } - self.subscribe(task: task, to: event) - } + register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } func register(event: SplitEventWithMetadata, task: SplitEventTask) { @@ -170,7 +157,7 @@ class DefaultSplitEventsManager: SplitEventsManager { switch event.type { case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) + trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) continue } triggerSdkReadyIfNeeded() @@ -226,7 +213,11 @@ class DefaultSplitEventsManager: SplitEventsManager { } 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 { @@ -240,7 +231,7 @@ 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) } diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index 5b792fb3e..d5d0a48df 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -103,6 +103,10 @@ class InternalSplitClientStub: InternalSplitClient { func on(event: SplitEvent, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + + } func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 94cbb8991..80e9abd08 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -24,6 +24,10 @@ class SplitEventsManagerCoordinatorStub: SplitEventsManagerCoordinator { func register(event: SplitEvent, task: SplitEventTask) { + } + + func register(event: SplitEventWithMetadata, task: SplitEventTask) { + } var notifiedEvents = Set() diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index a19acdcb7..46f9739f1 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -94,6 +94,9 @@ class SplitClientStub: SplitClient { func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + } func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index 1132148df..1313a7267 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -57,6 +57,10 @@ class SplitEventsManagerMock: SplitEventsManager { registeredEvents[event] = task } + func register(event: SplitEventWithMetadata, task: any SplitEventTask) { + registeredEvents[event.type] = task + } + func start() { } diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 1d7685dc3..53a926ec7 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -43,6 +43,10 @@ class SplitEventsManagerStub: SplitEventsManager { func register(event: SplitEvent, task: SplitEventTask) { registeredEvents[event] = task } + + func register(event: SplitEventWithMetadata, task: SplitEventTask) { + registeredEvents[event.type] = task + } func start() { startCalled = true diff --git a/SplitTests/Helpers/IntegrationHelper.swift b/SplitTests/Helpers/IntegrationHelper.swift index b75db36cf..6f34a5564 100644 --- a/SplitTests/Helpers/IntegrationHelper.swift +++ b/SplitTests/Helpers/IntegrationHelper.swift @@ -209,24 +209,26 @@ class IntegrationHelper { static func describeEvent(_ event: SplitInternalEvent) -> String { switch event { - case .mySegmentsUpdated: - return "mySegmentsUpdated" - case .splitsUpdated: - return "splitsUpdated" - case .mySegmentsLoadedFromCache: - return "mySegmentsLoadedFromCache" - case .splitsLoadedFromCache: - return "splitsLoadedFromCache" - case .attributesLoadedFromCache: - return "attributesLoadedFromCache" - case .sdkReadyTimeoutReached: - return "sdkReadyTimeoutReached" - case .splitKilledNotification: - return "splitKilledNotification" - case .myLargeSegmentsUpdated: - return "myLargeSegmentsUpdated" - case .myLargeSegmentsLoadedFromCache: - return "myLargeSegmentsLoadedFromCache" + case .mySegmentsUpdated: + return "mySegmentsUpdated" + case .splitsUpdated: + return "splitsUpdated" + case .mySegmentsLoadedFromCache: + return "mySegmentsLoadedFromCache" + case .splitsLoadedFromCache: + return "splitsLoadedFromCache" + case .attributesLoadedFromCache: + return "attributesLoadedFromCache" + case .sdkReadyTimeoutReached: + return "sdkReadyTimeoutReached" + case .splitKilledNotification: + return "splitKilledNotification" + case .myLargeSegmentsUpdated: + return "myLargeSegmentsUpdated" + case .myLargeSegmentsLoadedFromCache: + return "myLargeSegmentsLoadedFromCache" + case .sdkError: + return "sdkError" } } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 11d8603ae..166d9f9bf 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -296,6 +296,25 @@ class SplitEventsManagerTest: XCTestCase { eventManager.stop() } + func testEventWithMetadata() { + + let taskExp = XCTestExpectation() + + // Build Task + let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: "TEST_FLAG") + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(metadata.type, handlerMetadata.type) + XCTAssertEqual(metadata.data, "TEST_FLAG") + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 3.0) + } + // MARK: Helpers func currentTimestamp() -> Int { return Int(Date().unixTimestamp()) @@ -306,29 +325,22 @@ class SplitEventsManagerTest: XCTestCase { } } -class TestTask: SplitEventTask { - - var event: SplitEvent = .sdkReady - - var runInBackground: Bool = false +class TestTask: SplitEventActionTask { - var queue: DispatchQueue? - var taskTriggered = false let label: String var exp: XCTestExpectation? - init(exp: XCTestExpectation?, label: String = "") { + + init(exp: XCTestExpectation?, label: String = "", action: SplitActionWithMetadata? = nil, metadata: EventMetadata? = nil) { self.exp = exp self.label = label + super.init(action: action ?? { _ in }, event: .sdkReady, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey)) } - - func takeQueue() -> DispatchQueue? { - return nil - } - - func run() { + + override func run(_ metadata: EventMetadata?) { print("run: \(self.label)") taskTriggered = true + super.run(metadata) if let exp = self.exp { exp.fulfill() } From ba7328c68e2e939e424ac78d7a7dea3aa2cc6da6 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 12:31:46 -0300 Subject: [PATCH 05/35] Simplified tests --- SplitTests/Collections/BlockingQueueTest.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index c0a927c3c..f32343f42 100644 --- a/SplitTests/Collections/BlockingQueueTest.swift +++ b/SplitTests/Collections/BlockingQueueTest.swift @@ -60,7 +60,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) } catch BlockingQueueError.noElementAvailable { continue } catch { @@ -153,7 +153,7 @@ class BlockingQueueTest: XCTestCase { qu1.async { for _ in 1..<100000 { - queue.add(SplitInternalEventWithMetadata(.splitsUpdated, metadata: nil)) + queue.add(.splitsUpdated) print("qu1 add") Thread.sleep(forTimeInterval: 0.2) } @@ -162,7 +162,7 @@ class BlockingQueueTest: XCTestCase { qu2.async { for _ in 1..<10000 { print("qu2 add") - queue.add(SplitInternalEventWithMetadata(.sdkReadyTimeoutReached, metadata: nil)) + queue.add(.sdkReadyTimeoutReached) Thread.sleep(forTimeInterval: 0.5) } } @@ -170,7 +170,7 @@ class BlockingQueueTest: XCTestCase { qu3.async { for _ in 1..<10000 { print("qu3 add") - queue.add(SplitInternalEventWithMetadata(.splitsUpdated, metadata: nil)) + queue.add(.splitsUpdated) Thread.sleep(forTimeInterval: 0.8) } } @@ -178,7 +178,7 @@ class BlockingQueueTest: XCTestCase { qu4.async { for _ in 1..<10000 { print("qu4 add") - queue.add(SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: nil)) + queue.add(.mySegmentsUpdated) sleep(1) } } From 740f7935e5c01b6e42c28abbe3612b9c492e7379 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 13:24:03 -0300 Subject: [PATCH 06/35] Equatable removed from SplitActionWithMetadata --- Split/Events/SplitEvent.swift | 5 ----- Split/Events/SplitEventsManager.swift | 2 +- Split/Events/SplitInternalEvent.swift | 4 ---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index 09e354c69..a7d067ef6 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -15,11 +15,6 @@ import Foundation 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 { diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 3c6347bdc..335e7a469 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -267,7 +267,7 @@ class DefaultSplitEventsManager: SplitEventsManager { } private func isTriggered(internal event: SplitInternalEventWithMetadata) -> Bool { - return triggered.filter { $0 == event }.count > 0 + return triggered.filter { $0.type == event.type }.count > 0 } private func isTriggered(internal event: SplitInternalEvent) -> Bool { diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index c38f89cf4..37d409257 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -17,10 +17,6 @@ struct SplitInternalEventWithMetadata { self.type = type self.metadata = metadata } - - static func == (lhs: SplitInternalEventWithMetadata, rhs: SplitInternalEventWithMetadata) -> Bool { - return lhs.type == rhs.type - } } @objc public class EventMetadata: NSObject { From 8dd3be64155bd1030db87dfcfe434d75d0dfd463 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 13:42:28 -0300 Subject: [PATCH 07/35] EventsManager Mock and Stub now record the events correctly --- SplitTests/Fake/SplitEventsManagerMock.swift | 10 +++++----- SplitTests/Fake/SplitEventsManagerStub.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index 1313a7267..e6849f193 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -52,13 +52,13 @@ class SplitEventsManagerMock: SplitEventsManager { } } - var registeredEvents = [SplitEvent: SplitEventTask]() func register(event: SplitEvent, task: SplitEventTask) { - registeredEvents[event] = task + register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } - - func register(event: SplitEventWithMetadata, task: any SplitEventTask) { - registeredEvents[event.type] = task + + var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() + func register(event: SplitEventWithMetadata, task: SplitEventTask) { + registeredEvents[event] = task } func start() { diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 53a926ec7..fe09fe197 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -39,13 +39,13 @@ class SplitEventsManagerStub: SplitEventsManager { } } - var registeredEvents = [SplitEvent: SplitEventTask]() func register(event: SplitEvent, task: SplitEventTask) { - registeredEvents[event] = task + register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } - + + var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() func register(event: SplitEventWithMetadata, task: SplitEventTask) { - registeredEvents[event.type] = task + registeredEvents[event] = task } func start() { From 0b885a4be1913b894674c0594575e544c8d8c809 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 14:03:09 -0300 Subject: [PATCH 08/35] Tests fixed --- Split/Common/Structs/BlockingQueue.swift | 8 +++--- Split/Events/SplitInternalEvent.swift | 4 +++ .../Collections/BlockingQueueTest.swift | 4 +-- SplitTests/Init/SplitClientTests.swift | 6 ++--- SplitTests/SplitEventsManagerTest.swift | 25 ++++++++++--------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index 46f93a2a1..4fb01e5d9 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -81,13 +81,13 @@ protocol InternalEventBlockingQueue { class DefaultInternalEventBlockingQueue: InternalEventBlockingQueue { let blockingQueue = GenericBlockingQueue() - func add(_ item: SplitInternalEvent) { - blockingQueue.add(SplitInternalEventWithMetadata(item, metadata: nil)) - } - func add(_ item: SplitInternalEventWithMetadata) { blockingQueue.add(item) } + + func add(_ item: SplitInternalEvent) { + blockingQueue.add(SplitInternalEventWithMetadata(item, metadata: nil)) + } func take() throws -> SplitInternalEventWithMetadata { let value = try blockingQueue.take() diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 37d409257..c38f89cf4 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -17,6 +17,10 @@ struct SplitInternalEventWithMetadata { self.type = type self.metadata = metadata } + + static func == (lhs: SplitInternalEventWithMetadata, rhs: SplitInternalEventWithMetadata) -> Bool { + return lhs.type == rhs.type + } } @objc public class EventMetadata: NSObject { diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index f32343f42..7e6b97b69 100644 --- a/SplitTests/Collections/BlockingQueueTest.swift +++ b/SplitTests/Collections/BlockingQueueTest.swift @@ -34,9 +34,9 @@ class BlockingQueueTest: XCTestCase { } } globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.mySegmentsLoadedFromCache) + queue.add(.mySegmentsLoadedFromCache) globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.splitsLoadedFromCache) + queue.add(.splitsLoadedFromCache) } } diff --git a/SplitTests/Init/SplitClientTests.swift b/SplitTests/Init/SplitClientTests.swift index 25c6b3260..6d374be3d 100644 --- a/SplitTests/Init/SplitClientTests.swift +++ b/SplitTests/Init/SplitClientTests.swift @@ -45,7 +45,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { XCTAssertTrue(false) continue } @@ -61,7 +61,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { XCTAssertTrue(false) continue } @@ -77,7 +77,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { XCTAssertTrue(false) continue } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 166d9f9bf..ae1132a89 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -298,21 +298,22 @@ class SplitEventsManagerTest: XCTestCase { func testEventWithMetadata() { - let taskExp = XCTestExpectation() + let taskExp = XCTestExpectation() + let data = "TEST_DATA_123456" - // Build Task - let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: "TEST_FLAG") + // Build Task + let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: data) - let handler: SplitActionWithMetadata = { handlerMetadata in - XCTAssertEqual(metadata.type, handlerMetadata.type) - XCTAssertEqual(metadata.data, "TEST_FLAG") - taskExp.fulfill() - } - let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(metadata.type, handlerMetadata.type) + XCTAssertEqual(metadata.data, data) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) - // Run & test - task.run(metadata) - wait(for: [taskExp], timeout: 3.0) + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 1) } // MARK: Helpers From ad58ec328d2e17ec1274902b37f8c9e649232438 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 14:45:14 -0300 Subject: [PATCH 09/35] Tests fixed --- Split/Events/SplitInternalEvent.swift | 8 ++------ SplitTests/Init/SplitClientTests.swift | 6 +++--- SplitTests/SplitEventsManagerTest.swift | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index c38f89cf4..3b583c6a9 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -17,17 +17,13 @@ struct SplitInternalEventWithMetadata { self.type = type self.metadata = metadata } - - static func == (lhs: SplitInternalEventWithMetadata, rhs: SplitInternalEventWithMetadata) -> Bool { - return lhs.type == rhs.type - } } @objc public class EventMetadata: NSObject { var type: EventMetadataType - var data: String = "" + var data: [String] = [] - init(type: EventMetadataType, data: String) { + init(type: EventMetadataType, data: [String] = []) { self.type = type self.data = data } diff --git a/SplitTests/Init/SplitClientTests.swift b/SplitTests/Init/SplitClientTests.swift index 6d374be3d..ff1658039 100644 --- a/SplitTests/Init/SplitClientTests.swift +++ b/SplitTests/Init/SplitClientTests.swift @@ -45,7 +45,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } @@ -61,7 +61,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } @@ -77,7 +77,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[SplitEventWithMetadata(type: event, metadata: nil)] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index ae1132a89..98acad400 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -299,7 +299,7 @@ class SplitEventsManagerTest: XCTestCase { func testEventWithMetadata() { let taskExp = XCTestExpectation() - let data = "TEST_DATA_123456" + let data = ["TEST_DATA_123456"] // Build Task let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: data) From 30e203a3381798e7ff4d741e3eac1626d2eb57db Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 18:11:21 -0300 Subject: [PATCH 10/35] Tests covering the new events listeners with metadata --- Split/Api/DefaultSplitClient.swift | 34 ++++++++++++------ Split/Api/FailHelpers.swift | 10 +++++- Split/Api/LocalhostSplitClient.swift | 10 +++++- Split/Api/SplitClient.swift | 7 +++- SplitTests/Fake/InternalSplitClientStub.swift | 10 +++++- SplitTests/Fake/SplitClientStub.swift | 9 +++++ SplitTests/Init/SplitClientTests.swift | 36 +++++++++++++++++++ 7 files changed, 101 insertions(+), 15 deletions(-) diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index 1173d3e57..1d3beb8be 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -89,17 +89,6 @@ extension DefaultSplitClient { on(event: event, executeTask: task) } - 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.") @@ -107,6 +96,29 @@ extension DefaultSplitClient { } eventsManager.register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) } + + // MARK: Listeners with Metadata + public func on(event: SplitEvent, executeWithMetadata action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: true, queue: nil, executeWithMetadata: action) + } + + public func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: runInBackground, queue: nil, executeWithMetadata: executeWithMetadata) + } + + public func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue? = nil, executeWithMetadata action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: runInBackground, queue: queue, action: action) + } + + public func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: true, queue: queue, action: action) + } + + private func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue? = nil, action: @escaping SplitActionWithMetadata) { + 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) + } } // MARK: Treatment / Evaluation diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 2e9a78020..0c2bf13cf 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -66,7 +66,15 @@ class FailedClient: SplitClient { } func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { - + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ } func track(trafficType: String, eventType: String) -> Bool { diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 203370ada..eb15e21fe 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -148,7 +148,15 @@ public final class LocalhostSplitClient: NSObject, SplitClient { } public func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { - + /* Intentionally unimplemented */ + } + + public func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + public func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ } public func track(trafficType: String, eventType: String) -> Bool { diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index 4c81639d6..7c87983d3 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -34,10 +34,15 @@ public typealias SplitActionWithMetadata = (EventMetadata) -> Void @objc(getTreatmentsWithConfigForSplits:attributes:evaluationOptions:) func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] + + // MARK: Events Listeners func on(event: SplitEvent, execute action: @escaping SplitAction) - func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) // MARK: Track feature func track(trafficType: String, eventType: String) -> Bool diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index d5d0a48df..26fc89caa 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -105,7 +105,15 @@ class InternalSplitClientStub: InternalSplitClient { } func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { - + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ } func track(trafficType: String, eventType: String) -> Bool { diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index 46f9739f1..2cb2b2492 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -96,6 +96,15 @@ class SplitClientStub: SplitClient { } func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ } func track(trafficType: String, eventType: String) -> Bool { diff --git a/SplitTests/Init/SplitClientTests.swift b/SplitTests/Init/SplitClientTests.swift index ff1658039..8403505dc 100644 --- a/SplitTests/Init/SplitClientTests.swift +++ b/SplitTests/Init/SplitClientTests.swift @@ -70,6 +70,42 @@ class SplitClientTests: XCTestCase { XCTAssertNil(task.takeQueue()) } } + + func testOnBgWithMetadata() { + for event in events { + client.on(event: event, executeWithMetadata: { metadata in + print("Metadata: \(metadata.data)") + }) + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertEqual(true, task.runInBackground) + XCTAssertNil(task.takeQueue()) + } + } + + func testOnMainWithMetadata() { + for event in events { + client.on(event: event, runInBackground: false) { metadata in + print("Metadata: \(metadata.data)") + } + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertEqual(false, task.runInBackground) + XCTAssertNil(task.takeQueue()) + } + } func testOnQueue() { for event in events { From ed50d1cf6fefdf6846b6184aed3b3ef1e3a83185 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 18:13:50 -0300 Subject: [PATCH 11/35] Removed code from the next feature (Events Errors) --- Split/Events/EventsManagerCoordinator.swift | 3 +-- Split/Events/SplitEvent.swift | 3 --- Split/Events/SplitEventsManager.swift | 4 ---- Split/Events/SplitInternalEvent.swift | 1 - SplitTests/Helpers/IntegrationHelper.swift | 2 -- 5 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 8ce38ab1a..cf99106a5 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -22,8 +22,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, .splitsUpdated, - .splitKilledNotification, - .sdkError] + .splitKilledNotification] ) func notifyInternalEvent(_ event: SplitInternalEvent) { diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index a7d067ef6..ab624200b 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -22,7 +22,6 @@ import Foundation case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated - case sdkError public func toString() -> String { switch self { @@ -34,8 +33,6 @@ import Foundation return "SDK_READY_TIMED_OUT" case .sdkReadyFromCache: return "SDK_READY_FROM_CACHE" - case .sdkError: - return "SDK_ERROR" } } } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 335e7a469..6dbfd94f8 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -181,10 +181,6 @@ class DefaultSplitEventsManager: SplitEventsManager { if !isTriggered(external: .sdkReady) { trigger(event: .sdkReadyTimedOut) } - case .sdkError: - if !isTriggered(external: .sdkReady) { - trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) - } } } } diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 3b583c6a9..60ce80c63 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -54,5 +54,4 @@ enum SplitInternalEvent { case attributesLoadedFromCache case sdkReadyTimeoutReached case splitKilledNotification - case sdkError } diff --git a/SplitTests/Helpers/IntegrationHelper.swift b/SplitTests/Helpers/IntegrationHelper.swift index 6f34a5564..a40e360b0 100644 --- a/SplitTests/Helpers/IntegrationHelper.swift +++ b/SplitTests/Helpers/IntegrationHelper.swift @@ -227,8 +227,6 @@ class IntegrationHelper { return "myLargeSegmentsUpdated" case .myLargeSegmentsLoadedFromCache: return "myLargeSegmentsLoadedFromCache" - case .sdkError: - return "sdkError" } } From a759fa7de8fe895adcd3ca6b462a8178e564c286 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 18:35:02 -0300 Subject: [PATCH 12/35] Added nested comment for SonarQuabe quality gate --- Split/Events/EventsManagerCoordinator.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index cf99106a5..ac65fd2a9 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -83,5 +83,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { func register(event: SplitEvent, task: SplitEventTask) {} - func register(event: SplitEventWithMetadata, task: any SplitEventTask) {} + func register(event: SplitEventWithMetadata, task: any SplitEventTask) { + /* Intentionally unimplemented */ + } } From 9270d6526d85043d77bf406f3bde101dc11a04b9 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 18:52:04 -0300 Subject: [PATCH 13/35] Tests added to conform to SonarQube quality gate --- Split.xcodeproj/project.pbxproj | 4 ++++ Split/Events/SplitInternalEvent.swift | 1 - SplitTests/SplitEventsTests.swift | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 SplitTests/SplitEventsTests.swift diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 3d7b9fe2f..3cc7ad0da 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -353,6 +353,7 @@ 59FB7C35220329B900ECC96A /* SplitFactoryBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */; }; 59FB7C3C2203795F00ECC96A /* LocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */; }; 59FB7C3E22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */; }; + 5B0162682E4A9C7A0009D3B7 /* SplitEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */; }; 5B48D8172DEA2CED00351925 /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; 5B91B8392DDE4A3B000510F0 /* SplitDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */; }; 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; @@ -1557,6 +1558,7 @@ 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFactoryBuilderTests.swift; sourceTree = ""; }; 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalhostSplitsParser.swift; sourceTree = ""; }; 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceDelimitedLocalhostSplitsParser.swift; sourceTree = ""; }; + 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitEventsTests.swift; sourceTree = ""; }; 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitDTOTests.swift; sourceTree = ""; }; 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrerequisitesMatcher.swift; sourceTree = ""; }; 5BF52DF82DE4B8CA00FEDAFE /* PrerequisitesMatcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrerequisitesMatcherTest.swift; sourceTree = ""; }; @@ -2887,6 +2889,7 @@ isa = PBXGroup; children = ( C539CAE52D947D2A0050C732 /* Common */, + 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */, 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */, C53EDFCC2DD4E10A000DCDBC /* SplitsSyncHelperWithProxyHandlerTests.swift */, C53EDFCA2DD3E257000DCDBC /* OutdatedSplitProxyHandlerTests.swift */, @@ -4429,6 +4432,7 @@ 95B1801E2763BF7E002DC9DF /* TelemetryStatsRecorderWorkerTests.swift in Sources */, 95F3F002258D3EE700084AF8 /* HttpEventsRecorderStub.swift in Sources */, 59D84BE3221734F5003DA248 /* LocalhostSplitClientTests.swift in Sources */, + 5B0162682E4A9C7A0009D3B7 /* SplitEventsTests.swift in Sources */, 59B2043924F5667A0092F2E9 /* SseNotificationProcessorTest.swift in Sources */, 592C6AC6211B718E002D120C /* SplitEventsManagerTest.swift in Sources */, 95C7569D2696457500696148 /* NotificationHelperStub.swift in Sources */, diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 60ce80c63..e05385198 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -39,7 +39,6 @@ enum EventMetadataType: Int { return "FEATURE_FLAGS_SYNC_ERROR" case .SEGMENTS_SYNC_ERROR: return "SEGMENTS_SYNC_ERROR" - } } } diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift new file mode 100644 index 000000000..e921fbf6a --- /dev/null +++ b/SplitTests/SplitEventsTests.swift @@ -0,0 +1,13 @@ +// Created by Martin Cardozo on 11/08/2025 + +import XCTest +@testable import Split + +class SplitEventsTests { + func testSplitInternalEventsWithMetadata() { + let event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") + event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + XCTAssertEqual(event.metadata!.type.toString(), "SEGMENTS_SYNC_ERROR") + } +} From 71c5f6801656785c7b3e6b59147f8dc33cf36852 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 18:52:56 -0300 Subject: [PATCH 14/35] Typo fixed: --- SplitTests/SplitEventsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift index e921fbf6a..490290514 100644 --- a/SplitTests/SplitEventsTests.swift +++ b/SplitTests/SplitEventsTests.swift @@ -5,7 +5,7 @@ import XCTest class SplitEventsTests { func testSplitInternalEventsWithMetadata() { - let event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "SEGMENTS_SYNC_ERROR") From 93c1b9660537475962882f8e616510341ae9b488 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 19:10:26 -0300 Subject: [PATCH 15/35] SplitEventsTests added to TestPlan --- SplitTests/SplitEventsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift index 490290514..0920480cd 100644 --- a/SplitTests/SplitEventsTests.swift +++ b/SplitTests/SplitEventsTests.swift @@ -3,7 +3,7 @@ import XCTest @testable import Split -class SplitEventsTests { +class SplitEventsTests: XCTestCase { func testSplitInternalEventsWithMetadata() { var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") From e9b139d0e92d0802d3c482a40fe5ba111081eb0b Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Mon, 11 Aug 2025 20:49:57 -0300 Subject: [PATCH 16/35] Test added to cover listening with metadata on a Queue --- SplitTests/Init/SplitClientTests.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SplitTests/Init/SplitClientTests.swift b/SplitTests/Init/SplitClientTests.swift index 8403505dc..4a831906f 100644 --- a/SplitTests/Init/SplitClientTests.swift +++ b/SplitTests/Init/SplitClientTests.swift @@ -122,6 +122,21 @@ class SplitClientTests: XCTestCase { XCTAssertNotNil(task.takeQueue()) } } + + func testOnQueueWithMetadata() { + for event in events { + client.on(event: event, queue: DispatchQueue(label: "queuemetadata1"), action: { _ in print("exec")}) + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertNotNil(task.takeQueue()) + } + } func testGetTreatmentWithEvaluationOptions() { testEvaluationOptionsPassedCorrectly( From eafca8fba57d1d70c15dcb755d94952e75f16a2b Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Tue, 12 Aug 2025 09:12:15 -0300 Subject: [PATCH 17/35] Added sdkError cases --- Split/Events/SplitEvent.swift | 19 +++++++++++-------- Split/Events/SplitEventsManager.swift | 4 ++++ Split/Events/SplitInternalEvent.swift | 1 + SplitTests/Helpers/IntegrationHelper.swift | 2 ++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index ab624200b..5ca699ec7 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -22,17 +22,20 @@ import Foundation case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated + case sdkError public func toString() -> String { switch self { - case .sdkReady: - return "SDK_READY" - case .sdkUpdated: - return "SDK_UPDATE" - case .sdkReadyTimedOut: - return "SDK_READY_TIMED_OUT" - case .sdkReadyFromCache: - return "SDK_READY_FROM_CACHE" + case .sdkReady: + return "SDK_READY" + case .sdkUpdated: + return "SDK_UPDATE" + case .sdkReadyTimedOut: + return "SDK_READY_TIMED_OUT" + case .sdkReadyFromCache: + return "SDK_READY_FROM_CACHE" + case .sdkError: + return "SDK_ERROR" } } } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 6dbfd94f8..ff702ca86 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -181,6 +181,10 @@ class DefaultSplitEventsManager: SplitEventsManager { if !isTriggered(external: .sdkReady) { trigger(event: .sdkReadyTimedOut) } + case .sdkError: + if !isTriggered(external: .sdkReady) { + trigger(event: SplitEventWithMetadata(type: .sdkError, metadata: event.metadata)) + } } } } diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index e05385198..3adf18ddb 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -53,4 +53,5 @@ enum SplitInternalEvent { case attributesLoadedFromCache case sdkReadyTimeoutReached case splitKilledNotification + case sdkError } diff --git a/SplitTests/Helpers/IntegrationHelper.swift b/SplitTests/Helpers/IntegrationHelper.swift index a40e360b0..6f34a5564 100644 --- a/SplitTests/Helpers/IntegrationHelper.swift +++ b/SplitTests/Helpers/IntegrationHelper.swift @@ -227,6 +227,8 @@ class IntegrationHelper { return "myLargeSegmentsUpdated" case .myLargeSegmentsLoadedFromCache: return "myLargeSegmentsLoadedFromCache" + case .sdkError: + return "sdkError" } } From 647ebe9269300441f4423bc3a718e5347332c511 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 00:00:21 -0300 Subject: [PATCH 18/35] Tests added --- Split.xcodeproj/project.pbxproj | 4 +- Split/Api/DefaultSplitClient.swift | 2 +- Split/Events/EventsManagerCoordinator.swift | 11 +- Split/Events/SplitEvent.swift | 2 +- Split/Events/SplitEventsManager.swift | 32 ++- .../Refresh/PeriodicSyncWorker.swift | 32 ++- .../Refresh/RetryableSegmentsSyncWorker.swift | 13 +- .../Refresh/RetryableSyncWorker.swift | 24 +- .../Network/Streaming/SyncUpdateWorker.swift | 20 +- .../SplitEventsManagerCoordinatorStub.swift | 20 +- SplitTests/Fake/SplitEventsManagerMock.swift | 40 +-- SplitTests/Fake/SplitEventsManagerStub.swift | 38 +-- .../Sync/SplitChangesServerErrorTest.swift | 239 ++++++++++++------ SplitTests/SplitEventsTests.swift | 2 +- 14 files changed, 312 insertions(+), 167 deletions(-) diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 3cc7ad0da..6a33dbd01 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -5236,7 +5236,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = ""; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 15.4; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -5302,7 +5302,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = ""; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 15.4; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index 1d3beb8be..6ce325d21 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -94,7 +94,7 @@ extension DefaultSplitClient { 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) + eventsManager.register(event: event, task: task) } // MARK: Listeners with Metadata diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index ac65fd2a9..c25213105 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -21,15 +21,16 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { private let queue = DispatchQueue(label: "split-event-manager-coordinator") private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, - .splitsUpdated, - .splitKilledNotification] + .splitsUpdated, + .splitKilledNotification, + .sdkError] ) func notifyInternalEvent(_ event: SplitInternalEvent) { - notifyInternalEventWithMetadata(SplitInternalEventWithMetadata(event, metadata: nil)) + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) } - func notifyInternalEventWithMetadata(_ event: SplitInternalEventWithMetadata) { + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { if !eventsToHandle.contains(event.type) { return } @@ -38,7 +39,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { self.triggered.insert(event.type) self.managers.forEach { _, manager in - manager.notifyInternalEvent(event.type) + manager.notifyInternalEvent(event) } } } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index 5ca699ec7..0b2a7872d 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -11,7 +11,7 @@ import Foundation let type: SplitEvent let metadata: EventMetadata? - @objc public init(type: SplitEvent, metadata: EventMetadata? = nil) { + @objc public init(_ type: SplitEvent, metadata: EventMetadata? = nil) { self.type = type self.metadata = metadata } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index ff702ca86..778b007f1 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -10,14 +10,15 @@ import Foundation protocol SplitEventsManager: AnyObject { func register(event: SplitEvent, task: SplitEventTask) - func register(event: SplitEventWithMetadata, task: SplitEventTask) func notifyInternalEvent(_ event: SplitInternalEvent) + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) func start() func stop() func eventAlreadyTriggered(event: SplitEvent) -> Bool } class DefaultSplitEventsManager: SplitEventsManager { + private let readingRefreshTime: Int private var sdkReadyTimeStart: Int64 @@ -41,11 +42,12 @@ class DefaultSplitEventsManager: SplitEventsManager { self.executionTimes = [String: Int]() registerMaxAllowedExecutionTimesPerEvent() + // SDK Timeout Event if config.sdkReadyTimeOut > 0 { 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) } } } @@ -60,15 +62,14 @@ class DefaultSplitEventsManager: SplitEventsManager { } func notifyInternalEvent(_ event: SplitInternalEvent) { + if event == .sdkError { + print("") + } notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) } - - func register(event: SplitEvent, task: SplitEventTask) { - register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) - } - func register(event: SplitEventWithMetadata, task: SplitEventTask) { - let eventName = event.type.toString() + func register(event: SplitEvent, task: SplitEventTask) { + let eventName = event.toString() processQueue.async { [weak self] in guard let self = self else { return } // If event is already triggered, execute the task @@ -76,7 +77,7 @@ class DefaultSplitEventsManager: SplitEventsManager { self.executeTask(event: event, task: task) return } - self.subscribe(task: task, to: event.type) + self.subscribe(task: task, to: event) } } @@ -111,7 +112,6 @@ class DefaultSplitEventsManager: SplitEventsManager { self.eventsQueue.stop() self.eventsQueue.stop() } - } } @@ -125,6 +125,7 @@ class DefaultSplitEventsManager: SplitEventsManager { executionTimes = [ SplitEvent.sdkReady.toString(): 1, SplitEvent.sdkUpdated.toString(): -1, + SplitEvent.sdkError.toString(): -1, SplitEvent.sdkReadyFromCache.toString(): 1, SplitEvent.sdkReadyTimedOut.toString(): 1] } @@ -159,7 +160,7 @@ class DefaultSplitEventsManager: SplitEventsManager { switch event.type { case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: if isTriggered(external: .sdkReady) { - trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata)) + trigger(event: .sdkUpdated) continue } triggerSdkReadyIfNeeded() @@ -183,7 +184,8 @@ class DefaultSplitEventsManager: SplitEventsManager { } case .sdkError: if !isTriggered(external: .sdkReady) { - trigger(event: SplitEventWithMetadata(type: .sdkError, metadata: event.metadata)) + let eventWithMetadata = SplitEventWithMetadata(.sdkError, metadata: event.metadata) + trigger(event: eventWithMetadata) } } } @@ -215,7 +217,7 @@ class DefaultSplitEventsManager: SplitEventsManager { } private func trigger(event: SplitEvent) { - trigger(event: SplitEventWithMetadata(type: event, metadata: nil)) + trigger(event: SplitEventWithMetadata(event, metadata: nil)) } private func trigger(event: SplitEventWithMetadata) { @@ -241,13 +243,14 @@ class DefaultSplitEventsManager: SplitEventsManager { } private func executeTask(event: SplitEvent, task: SplitEventTask) { - executeTask(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) + executeTask(event: SplitEventWithMetadata(event, metadata: nil), task: task) } private func executeTask(event: SplitEventWithMetadata, task: SplitEventTask) { let eventName = task.event.toString() + // RUN IN BG & RETURN if task.runInBackground { TimeChecker.logInterval("Previous to run \(eventName) in Background") @@ -259,6 +262,7 @@ class DefaultSplitEventsManager: SplitEventsManager { return } + // OR RUN ON MAIN DispatchQueue.main.async { TimeChecker.logInterval("Running event on main: \(eventName)") // UI Updates diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index b6fd71c81..d534e9687 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -130,10 +130,13 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { Logger.i("Fetch from remote not implemented") } - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + func notifyUpdate(_ event: SplitInternalEvent) { + let withMetadata = SplitInternalEventWithMetadata(event, metadata: nil) + notifyUpdate(withMetadata) + } + + func notifyUpdate(_ event: SplitInternalEventWithMetadata) { + eventsManager.notifyInternalEvent(event) } } @@ -181,10 +184,20 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { let changeNumber = splitsStorage.changeNumber let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) return } + if result.success, result.featureFlagsUpdated || result.rbsUpdated { - notifyUpdate([.splitsUpdated]) + // SUCCESS + notifyUpdate(.splitsUpdated) + } else if !result.success { + // FAIL + var event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) + event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + notifyUpdate(event) } } } @@ -225,10 +238,17 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { if result.success { if result.msUpdated || result.mlsUpdated { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + notifyUpdate(.mySegmentsUpdated) } + } else { + // FAIL + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) } } catch { + // FAIL + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) Logger.e("Problem fetching segments: %@", error.localizedDescription) } } diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 36973b66b..8e25e0ff8 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -46,16 +46,23 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { if result.success { if !isSdkReadyTriggered() { // Notifying both to trigger SDK Ready - notifyUpdate([.mySegmentsUpdated]) - notifyUpdate([.myLargeSegmentsUpdated]) + notifyUpdate(.mySegmentsUpdated) + notifyUpdate(.myLargeSegmentsUpdated) } else if result.msUpdated || result.mlsUpdated { // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + notifyUpdate(.mySegmentsUpdated) } return true + } else { + // FAIL + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) } } catch { + // FAIL Logger.e("Error while fetching segments in method: \(error.localizedDescription)") + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + notifyUpdate(event) errorHandler?(error) } return false diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 28c0a6f9c..41d5c2581 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -79,10 +79,12 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { } } - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + func notifyUpdate(_ event: SplitInternalEvent) { + notifyUpdate(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyUpdate(_ event: SplitInternalEventWithMetadata) { + eventsManager.notifyInternalEvent(event) } func isSdkReadyTriggered() -> Bool { @@ -143,13 +145,18 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { if result.success { if !isSdkReadyTriggered() || result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true + } else { + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + notifyUpdate(eventWithMetadata) } } catch { Logger.e("Error while fetching splits in method: \(error.localizedDescription)") + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + notifyUpdate(eventWithMetadata) errorHandler?(error) } return false @@ -218,13 +225,18 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { headers: ServiceConstants.controlNoCacheHeader) if result.success { if result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true + } else { + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + notifyUpdate(eventWithMetadata) } } catch { Logger.e("Error while fetching splits in method \(#function): \(error.localizedDescription)") + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + notifyUpdate(eventWithMetadata) errorHandler?(error) } Logger.d("Feature flag changes are not updated yet") diff --git a/Split/Network/Streaming/SyncUpdateWorker.swift b/Split/Network/Streaming/SyncUpdateWorker.swift index 5aede0758..6b148d204 100644 --- a/Split/Network/Streaming/SyncUpdateWorker.swift +++ b/Split/Network/Streaming/SyncUpdateWorker.swift @@ -129,9 +129,9 @@ class SplitsUpdateWorker: UpdateWorker { previousChangeNumber: Int64, changeNumber: Int64) -> Bool { do { - let split = try self.payloadDecoder.decode( + let split = try payloadDecoder.decode( payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + compressionUtil: decomProvider.decompressor(for: compressionType)) if !allRuleBasedSegmentsExist(in: split) { return false @@ -143,11 +143,11 @@ class SplitsUpdateWorker: UpdateWorker { Logger.v("Split update received: \(change)") - if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { - self.synchronizer.notifyFeatureFlagsUpdated() + if self.splitsStorage.update(splitChange: splitChangeProcessor.process(change)) { + synchronizer.notifyFeatureFlagsUpdated() } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding feature flags payload from notification: \(error)") @@ -161,9 +161,9 @@ class SplitsUpdateWorker: UpdateWorker { previousChangeNumber: Int64, changeNumber: Int64) -> Bool { do { - let rbs = try self.ruleBasedSegmentsPayloadDecoder.decode( + let rbs = try ruleBasedSegmentsPayloadDecoder.decode( payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + compressionUtil: decomProvider.decompressor(for: compressionType)) let change = RuleBasedSegmentChange(segments: [rbs], since: previousChangeNumber, @@ -173,13 +173,13 @@ class SplitsUpdateWorker: UpdateWorker { let processedChange = ruleBasedSegmentsChangeProcessor.process(change) - if self.ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, + if ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, toRemove: processedChange.toRemove, changeNumber: processedChange.changeNumber) { - self.synchronizer.notifyFeatureFlagsUpdated() + synchronizer.notifyFeatureFlagsUpdated() } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding rule based segments payload from notification: \(error)") diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 80e9abd08..8f56ba42e 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -11,28 +11,32 @@ import Foundation @testable import Split class SplitEventsManagerCoordinatorStub: SplitEventsManagerCoordinator { - + var managers = [Key: SplitEventsManager]() - + func add(_ manager: SplitEventsManager, forKey key: Key) { managers[key] = manager } - + func remove(forKey key: Key) { managers[key] = nil } - + func register(event: SplitEvent, task: SplitEventTask) { - + } func register(event: SplitEventWithMetadata, task: SplitEventTask) { } - - var notifiedEvents = Set() + func notifyInternalEvent(_ event: SplitInternalEvent) { - notifiedEvents.insert(IntegrationHelper.describeEvent(event)) + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + var notifiedEvents: [SplitInternalEventWithMetadata] = [] + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + notifiedEvents.append(event) } var startCalled = false diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index e6849f193..a82e250fa 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -32,28 +32,32 @@ class SplitEventsManagerMock: SplitEventsManager { var isSdkReadyChecked = false - func notifyInternalEvent(_ event:SplitInternalEvent) { - switch event { - case .mySegmentsUpdated: - isSegmentsReadyFired = true - case .splitsUpdated: - isSplitsReadyFired = true - isSplitUpdatedTriggered = true - if let exp = readyExp { - exp.fulfill() - } - case .sdkReadyTimeoutReached: - isSdkTimeoutFired = true - if let exp = timeoutExp { - exp.fulfill() - } - default: - print("\(event)") + func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + switch event.type { + case .mySegmentsUpdated: + isSegmentsReadyFired = true + case .splitsUpdated: + isSplitsReadyFired = true + isSplitUpdatedTriggered = true + if let exp = readyExp { + exp.fulfill() + } + case .sdkReadyTimeoutReached: + isSdkTimeoutFired = true + if let exp = timeoutExp { + exp.fulfill() + } + default: + print("\(event)") } } func register(event: SplitEvent, task: SplitEventTask) { - register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) + register(event: SplitEventWithMetadata(event, metadata: nil), task: task) } var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index fe09fe197..44dc2a4ab 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -20,27 +20,31 @@ class SplitEventsManagerStub: SplitEventsManager { var stopCalled = false func notifyInternalEvent(_ event: SplitInternalEvent) { - switch event { - case .mySegmentsLoadedFromCache: - mySegmentsLoadedEventFiredCount+=1 - if let exp = mySegmentsLoadedEventExp { - exp.fulfill() - } - case .splitsLoadedFromCache: - splitsLoadedEventFiredCount+=1 - - case .splitKilledNotification: - splitsKilledEventFiredCount+=1 - - case .splitsUpdated: - splitsUpdatedEventFiredCount+=1 - default: - print("internal event fired: \(event)") + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + switch event.type { + case .mySegmentsLoadedFromCache: + mySegmentsLoadedEventFiredCount+=1 + if let exp = mySegmentsLoadedEventExp { + exp.fulfill() + } + case .splitsLoadedFromCache: + splitsLoadedEventFiredCount+=1 + + case .splitKilledNotification: + splitsKilledEventFiredCount+=1 + + case .splitsUpdated: + splitsUpdatedEventFiredCount+=1 + default: + print("internal event fired: \(event)") } } func register(event: SplitEvent, task: SplitEventTask) { - register(event: SplitEventWithMetadata(type: event, metadata: nil), task: task) + register(event: SplitEventWithMetadata(event, metadata: nil), task: task) } var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 63169787f..e55eefd6c 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -15,32 +15,156 @@ class SplitChangesServerErrorTest: XCTestCase { let kChangeNbInterval: Int64 = 86400 var reqChangesIndex = 0 var lastChangeNumber: Int64 = 0 - + let spExp = [ XCTestExpectation(description: "upd 0"), XCTestExpectation(description: "error 1"), XCTestExpectation(description: "upd 2"), XCTestExpectation(description: "upd 3") ] - + var serverUrl = "localhost" - let impExp = XCTestExpectation(description: "impressions") - var impHit: [ImpressionsTest]? + // Client config var httpClient: HttpClient! var streamingBinding: TestStreamResponseBinding? - + var splitConfig: SplitClientConfig? + let apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" + let key: Key = Key(matchingKey: "CUSTOMER_ID", bucketingKey: nil) + let builder = DefaultSplitFactoryBuilder() + override func setUp() { + splitConfig = SplitClientConfig() + splitConfig!.streamingEnabled = false + splitConfig!.featuresRefreshRate = 3 + splitConfig!.impressionRefreshRate = kNeverRefreshRate + splitConfig!.sdkReadyTimeOut = 60000 + splitConfig!.trafficType = "client" + splitConfig!.streamingEnabled = false + splitConfig!.serviceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() + + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + } + + // MARK: Getting changes from server and test treatments and change number + func testChangesError() throws { + var treatments = [String]() + let sdkReady = XCTestExpectation(description: "SDK READY Expectation") + + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + + let client = factory!.client + + var sdkReadyFired = false + + client.on(event: SplitEvent.sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + wait(for: [sdkReady], timeout: 10) + + for i in 0..<4 { + wait(for: [spExp[i]], timeout: 40) + treatments.append(client.getTreatment("test_feature")) + } + + XCTAssertTrue(sdkReadyFired) + + XCTAssertEqual("on_0", treatments[0]) + XCTAssertEqual("on_0", treatments[1]) + XCTAssertEqual("on_0", treatments[2]) + XCTAssertEqual("off_1", treatments[3]) + + cleanup(client, &factory) + } + + // MARK: Getting segments from server and getting a server error + func testResponseSegmentsSyncError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile())) // Valid Splits + } + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 500) // Error for Segments + } + return TestDispatcherResponse(code: 500) + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + + cleanup(client, &factory) + } + + // MARK: Getting Flags from server and getting a server error + func testResponseFlagsSyncError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // Valid for Segments + } + return TestDispatcherResponse(code: 500) // Error for Splits + } let session = HttpSessionMock() - let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), - streamingHandler: buildStreamingHandler()) + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + + cleanup(client, &factory) } +} +// MARK: Test Helpers +extension SplitChangesServerErrorTest { + private func buildTestDispatcher() -> HttpClientTestDispatcher { - let respData = responseSplitChanges() var responses = [TestDispatcherResponse]() responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson( @@ -49,8 +173,7 @@ class SplitChangesServerErrorTest: XCTestCase { responses.append(TestDispatcherResponse(code: 500)) responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson( TargetingRulesChange(featureFlags: respData[1], ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1))).utf8))) - - + return { request in if request.isSplitEndpoint() { let index = self.reqChangesIndex @@ -66,26 +189,26 @@ class SplitChangesServerErrorTest: XCTestCase { let since = Int(self.lastChangeNumber) return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges(since: since, till: since).utf8)) } - + if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) } - + if request.isAuthEndpoint() { return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.dummySseResponse().utf8)) } - + if request.isImpressionsEndpoint() { return TestDispatcherResponse(code: 200) } - + if request.isEventsEndpoint() { return TestDispatcherResponse(code: 200) } return TestDispatcherResponse(code: 500) } } - + private func buildStreamingHandler() -> TestStreamResponseBindingHandler { return { request in self.streamingBinding = TestStreamResponseBinding.createFor(request: request, code: 200) @@ -93,64 +216,7 @@ class SplitChangesServerErrorTest: XCTestCase { } } - // MARK: Test - /// Getting changes from server and test treatments and change number - func testChangesError() throws { - let apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" - let matchingKey = "CUSTOMER_ID" - let trafficType = "client" - var treatments = [String]() - - let sdkReady = XCTestExpectation(description: "SDK READY Expectation") - - let splitConfig: SplitClientConfig = SplitClientConfig() - splitConfig.streamingEnabled = false - splitConfig.featuresRefreshRate = 3 - splitConfig.impressionRefreshRate = kNeverRefreshRate - splitConfig.sdkReadyTimeOut = 60000 - splitConfig.trafficType = trafficType - splitConfig.streamingEnabled = false - splitConfig.serviceEndpoints = ServiceEndpoints.builder() - .set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() - - let key: Key = Key(matchingKey: matchingKey, bucketingKey: nil) - let builder = DefaultSplitFactoryBuilder() - _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) - _ = builder.setHttpClient(httpClient) - var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() - - let client = factory!.client - - var sdkReadyFired = false - - client.on(event: SplitEvent.sdkReady) { - sdkReadyFired = true - sdkReady.fulfill() - } - - wait(for: [sdkReady], timeout: 10) - - for i in 0..<4 { - wait(for: [spExp[i]], timeout: 40) - treatments.append(client.getTreatment("test_feature")) - } - - XCTAssertTrue(sdkReadyFired) - - XCTAssertEqual("on_0", treatments[0]) - XCTAssertEqual("on_0", treatments[1]) - XCTAssertEqual("on_0", treatments[2]) - XCTAssertEqual("off_1", treatments[3]) - - let semaphore = DispatchSemaphore(value: 0) - client.destroy(completion: { - _ = semaphore.signal() - }) - semaphore.wait() - factory = nil - } - - private func responseSplitChanges() -> [SplitChange] { + private func responseSplitChanges() -> [SplitChange] { var changes = [SplitChange]() for i in 0..<2 { @@ -172,13 +238,36 @@ class SplitChangesServerErrorTest: XCTestCase { } return changes } + + private func updatedSegments(index: Int) -> String { + var resp = [String]() + let cn = 5 + for i in (1.. SplitChange? { - return FileHelper.loadSplitChangeFile(sourceClass: self, fileName: "splitchanges_int_test") + FileHelper.loadSplitChangeFile(sourceClass: self, fileName: "splitchanges_int_test") } private func buildImpressionsFromJson(content: String) throws -> [ImpressionsTest] { - return try Json.decodeFrom(json: content, to: [ImpressionsTest].self) + try Json.decodeFrom(json: content, to: [ImpressionsTest].self) + } + + private func cleanup(_ client: SplitClient, _ factory: inout SplitFactory?) { + let semaphore = DispatchSemaphore(value: 0) + client.destroy(completion: { + _ = semaphore.signal() + }) + semaphore.wait() + factory = nil } } diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift index 0920480cd..76f87ce19 100644 --- a/SplitTests/SplitEventsTests.swift +++ b/SplitTests/SplitEventsTests.swift @@ -4,7 +4,7 @@ import XCTest @testable import Split class SplitEventsTests: XCTestCase { - func testSplitInternalEventsWithMetadata() { + func testInternalEventsWithMetadataErrorType() { var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) From 5ca81f30660a044d87a5ffcab94054f5f2d9da6f Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 00:07:55 -0300 Subject: [PATCH 19/35] Removing empty line changes for better readability --- Split.xcodeproj/project.pbxproj | 4 ++-- .../SplitEventsManagerCoordinatorStub.swift | 10 +++++----- .../Sync/SplitChangesServerErrorTest.swift | 18 ++++++++---------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 6a33dbd01..3cc7ad0da 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -5236,7 +5236,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = ""; TVOS_DEPLOYMENT_TARGET = 15.4; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -5302,7 +5302,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = ""; TVOS_DEPLOYMENT_TARGET = 15.4; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 8f56ba42e..cd32813fd 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -11,19 +11,19 @@ import Foundation @testable import Split class SplitEventsManagerCoordinatorStub: SplitEventsManagerCoordinator { - + var managers = [Key: SplitEventsManager]() - + func add(_ manager: SplitEventsManager, forKey key: Key) { managers[key] = manager } - + func remove(forKey key: Key) { managers[key] = nil } - + func register(event: SplitEvent, task: SplitEventTask) { - + } func register(event: SplitEventWithMetadata, task: SplitEventTask) { diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index e55eefd6c..80fda19f5 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -15,14 +15,14 @@ class SplitChangesServerErrorTest: XCTestCase { let kChangeNbInterval: Int64 = 86400 var reqChangesIndex = 0 var lastChangeNumber: Int64 = 0 - + let spExp = [ XCTestExpectation(description: "upd 0"), XCTestExpectation(description: "error 1"), XCTestExpectation(description: "upd 2"), XCTestExpectation(description: "upd 3") ] - + var serverUrl = "localhost" let impExp = XCTestExpectation(description: "impressions") var impHit: [ImpressionsTest]? @@ -44,6 +44,7 @@ class SplitChangesServerErrorTest: XCTestCase { splitConfig!.trafficType = "client" splitConfig!.streamingEnabled = false splitConfig!.serviceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), streamingHandler: buildStreamingHandler()) @@ -55,7 +56,6 @@ class SplitChangesServerErrorTest: XCTestCase { var treatments = [String]() let sdkReady = XCTestExpectation(description: "SDK READY Expectation") - _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) _ = builder.setHttpClient(httpClient) var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() @@ -103,7 +103,6 @@ class SplitChangesServerErrorTest: XCTestCase { httpClient = DefaultHttpClient(session: session, requestManager: reqManager) // Client config - _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) _ = builder.setHttpClient(httpClient) var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() let client = factory!.client @@ -139,7 +138,6 @@ class SplitChangesServerErrorTest: XCTestCase { httpClient = DefaultHttpClient(session: session, requestManager: reqManager) // Client config - _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) _ = builder.setHttpClient(httpClient) var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() let client = factory!.client @@ -189,26 +187,26 @@ extension SplitChangesServerErrorTest { let since = Int(self.lastChangeNumber) return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges(since: since, till: since).utf8)) } - + if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) } - + if request.isAuthEndpoint() { return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.dummySseResponse().utf8)) } - + if request.isImpressionsEndpoint() { return TestDispatcherResponse(code: 200) } - + if request.isEventsEndpoint() { return TestDispatcherResponse(code: 200) } return TestDispatcherResponse(code: 500) } } - + private func buildStreamingHandler() -> TestStreamResponseBindingHandler { return { request in self.streamingBinding = TestStreamResponseBinding.createFor(request: request, code: 200) From d0201be3e1d9d15ac16feef6a528dfd85b801a86 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 10:12:45 -0300 Subject: [PATCH 20/35] Added two more test to cover the case were response from server is 200, but the JSON has the wrong structure --- .../Sync/SplitChangesServerErrorTest.swift | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 80fda19f5..8a102a1e8 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -91,7 +91,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile())) // Valid Splits + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // Valid Splits } if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments @@ -157,13 +157,89 @@ class SplitChangesServerErrorTest: XCTestCase { cleanup(client, &factory) } + + // MARK: Getting malformed flags from server + func testResponseFlagsParserror() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // Valid for Segments + } + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // Valid Splits, but wrong JSON + } + return TestDispatcherResponse(code: 500) // Error for Splits + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + + cleanup(client, &factory) + } + + // MARK: Getting malformed segments from server + func testResponseSegmentsParserror() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data("".utf8)) // Valid response for Segments, but bad JSON + } + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // Valid Splits + } + return TestDispatcherResponse(code: 500) // Error for Splits + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + + cleanup(client, &factory) + } } // MARK: Test Helpers extension SplitChangesServerErrorTest { private func buildTestDispatcher() -> HttpClientTestDispatcher { - let respData = responseSplitChanges() + let respData = responseSplitChanges("splitchanges_int_test") var responses = [TestDispatcherResponse]() responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson( TargetingRulesChange(featureFlags: respData[0], ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1))).utf8))) @@ -214,11 +290,11 @@ extension SplitChangesServerErrorTest { } } - private func responseSplitChanges() -> [SplitChange] { + private func responseSplitChanges(_ filename: String) -> [SplitChange] { var changes = [SplitChange]() for i in 0..<2 { - let c = loadSplitsChangeFile()! + let c = loadSplitsChangeFile(filename)! var prevChangeNumber = c.since c.since = prevChangeNumber + kChangeNbInterval c.till = c.since @@ -251,8 +327,8 @@ extension SplitChangesServerErrorTest { return json } - private func loadSplitsChangeFile() -> SplitChange? { - FileHelper.loadSplitChangeFile(sourceClass: self, fileName: "splitchanges_int_test") + private func loadSplitsChangeFile(_ filename: String) -> SplitChange? { + FileHelper.loadSplitChangeFile(sourceClass: self, fileName: filename) } private func buildImpressionsFromJson(content: String) throws -> [ImpressionsTest] { From b4c9e44bdb973fc3cbee167f25140b6dbf4f0b86 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 11:32:16 -0300 Subject: [PATCH 21/35] Update Split/Events/SplitEventsManager.swift Co-authored-by: gthea --- Split/Events/SplitEventsManager.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 778b007f1..7f15213e8 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -62,9 +62,6 @@ class DefaultSplitEventsManager: SplitEventsManager { } func notifyInternalEvent(_ event: SplitInternalEvent) { - if event == .sdkError { - print("") - } notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) } From a2ddfc6b33976ab24ffa6a7ac8a807f2b6651ae9 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 11:34:40 -0300 Subject: [PATCH 22/35] Better comments --- Split/Events/SplitEventsManager.swift | 9 ++------- .../Sync/SplitChangesServerErrorTest.swift | 18 +++++++++--------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index 778b007f1..3bca15aaa 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -62,9 +62,6 @@ class DefaultSplitEventsManager: SplitEventsManager { } func notifyInternalEvent(_ event: SplitInternalEvent) { - if event == .sdkError { - print("") - } notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) } @@ -183,10 +180,8 @@ class DefaultSplitEventsManager: SplitEventsManager { trigger(event: .sdkReadyTimedOut) } case .sdkError: - if !isTriggered(external: .sdkReady) { - let eventWithMetadata = SplitEventWithMetadata(.sdkError, metadata: event.metadata) - trigger(event: eventWithMetadata) - } + let eventWithMetadata = SplitEventWithMetadata(.sdkError, metadata: event.metadata) + trigger(event: eventWithMetadata) } } } diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 8a102a1e8..0d89a366d 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -91,7 +91,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // Valid Splits + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits } if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments @@ -129,7 +129,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isMySegmentsEndpoint() { - return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // Valid for Segments + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // OK Segments } return TestDispatcherResponse(code: 500) // Error for Splits } @@ -164,12 +164,12 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isMySegmentsEndpoint() { - return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // Valid for Segments + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // OK for Segments } if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // Valid Splits, but wrong JSON + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // OK for Splits, but bad JSON } - return TestDispatcherResponse(code: 500) // Error for Splits + return TestDispatcherResponse(code: 500) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) @@ -197,17 +197,17 @@ class SplitChangesServerErrorTest: XCTestCase { } // MARK: Getting malformed segments from server - func testResponseSegmentsParserror() throws { + func testResponseSegmentsParseError() throws { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isMySegmentsEndpoint() { - return TestDispatcherResponse(code: 200, data: Data("".utf8)) // Valid response for Segments, but bad JSON + return TestDispatcherResponse(code: 200, data: Data("".utf8)) // OK for Segments, but bad JSON } if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // Valid Splits + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits } - return TestDispatcherResponse(code: 500) // Error for Splits + return TestDispatcherResponse(code: 500) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) From eefa1953fc8dae7cf5dcc280122cc0bc6cbc0167 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 12:14:15 -0300 Subject: [PATCH 23/35] Cleanup of Workers --- Split/Events/SplitInternalEvent.swift | 8 +- .../Refresh/PeriodicSyncWorker.swift | 46 ++-- .../Refresh/RetryableSegmentsSyncWorker.swift | 201 +++++++----------- .../Refresh/RetryableSyncWorker.swift | 14 +- .../Sync/SplitChangesServerErrorTest.swift | 12 +- SplitTests/SplitEventsManagerTest.swift | 2 +- SplitTests/SplitEventsTests.swift | 4 +- 7 files changed, 118 insertions(+), 169 deletions(-) diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 3adf18ddb..a26d4ccb6 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -30,14 +30,14 @@ struct SplitInternalEventWithMetadata { } enum EventMetadataType: Int { - case FEATURE_FLAGS_SYNC_ERROR - case SEGMENTS_SYNC_ERROR + case featureFlagsSyncError + case segmentsSyncError public func toString() -> String { switch self { - case .FEATURE_FLAGS_SYNC_ERROR: + case .featureFlagsSyncError: return "FEATURE_FLAGS_SYNC_ERROR" - case .SEGMENTS_SYNC_ERROR: + case .segmentsSyncError: return "SEGMENTS_SYNC_ERROR" } } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index d534e9687..f603a3653 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -140,6 +140,7 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { } } +// MARK: Sync Splits (Targeting Rules) class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { private let splitFetcher: HttpSplitFetcher @@ -171,37 +172,36 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) - super.init(timer: timer, - eventsManager: eventsManager) + super.init(timer: timer, eventsManager: eventsManager) } override func fetchFromRemote() { - // Polling should be done once sdk ready is fired in initial sync - if !isSdkReadyFired() { - return - } + if !isSdkReadyFired() { return } // Polling should be done once sdk ready is fired in initial sync let changeNumber = splitsStorage.changeNumber let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber + + // Try to Sync guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(event) return } + // Result if result.success, result.featureFlagsUpdated || result.rbsUpdated { - // SUCCESS + // Success notifyUpdate(.splitsUpdated) } else if !result.success { - // FAIL - var event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) - notifyUpdate(event) - event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(event) } } } +// MARK: Sync Segments class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { private let mySegmentsStorage: ByKeyMySegmentsStorage @@ -226,30 +226,26 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { } override func fetchFromRemote() { - // Polling should be done once sdk ready is fired in initial sync - if !isSdkReadyFired() { - return - } + if !isSdkReadyFired() { return } // Polling should be done once sdk ready is fired in initial sync do { - let result = try syncHelper.sync(msTill: mySegmentsStorage.changeNumber, - mlsTill: myLargeSegmentsStorage.changeNumber, - headers: nil) + let result = try syncHelper.sync(msTill: mySegmentsStorage.changeNumber, mlsTill: myLargeSegmentsStorage.changeNumber, headers: nil) + if result.success { + // Success if result.msUpdated || result.mlsUpdated { - // For now is not necessary specify which entity was updated notifyUpdate(.mySegmentsUpdated) } } else { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) } } catch { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) - notifyUpdate(event) + // Fail Logger.e("Problem fetching segments: %@", error.localizedDescription) + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + notifyUpdate(event) } } } diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 8e25e0ff8..1a543f403 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -1,10 +1,7 @@ -// // RetryableSegmentsSyncWorker.swift // Split // // Created by Javier Avrudsky on 15-Sep-2020 -// -// import Foundation @@ -21,47 +18,37 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { var changeChecker: MySegmentsChangesChecker - init(telemetryProducer: TelemetryRuntimeProducer?, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - avoidCache: Bool, - changeNumbers: SegmentsChangeNumber, - syncHelper: SegmentsSyncHelper) { - + init(telemetryProducer: TelemetryRuntimeProducer?, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, avoidCache: Bool, changeNumbers: SegmentsChangeNumber, syncHelper: SegmentsSyncHelper) { self.telemetryProducer = telemetryProducer self.changeChecker = DefaultMySegmentsChangesChecker() self.avoidCache = avoidCache self.changeNumbers = changeNumbers self.syncHelper = syncHelper - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { do { - let result = try syncHelper.sync(msTill: changeNumbers.msChangeNumber, - mlsTill: changeNumbers.mlsChangeNumber, - headers: getHeaders()) + let result = try syncHelper.sync(msTill: changeNumbers.msChangeNumber, mlsTill: changeNumbers.mlsChangeNumber, headers: getHeaders()) + if result.success { - if !isSdkReadyTriggered() { - // Notifying both to trigger SDK Ready + 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 + } else if result.msUpdated || result.mlsUpdated { notifyUpdate(.mySegmentsUpdated) } return true } else { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) } } catch { - // FAIL + // Fail Logger.e("Error while fetching segments in method: \(error.localizedDescription)") - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) errorHandler?(error) } @@ -70,25 +57,10 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { } private func getHeaders() -> [String: String]? { - return avoidCache ? ServiceConstants.controlNoCacheHeader : nil + avoidCache ? ServiceConstants.controlNoCacheHeader : nil } } -struct SegmentsSyncResult { - let success: Bool - let msChangeNumber: Int64 - let mlsChangeNumber: Int64 - let msUpdated: Bool - let mlsUpdated: Bool -} - -protocol SegmentsSyncHelper { - func sync(msTill: Int64, - mlsTill: Int64, - headers: HttpHeaders?) throws -> SegmentsSyncResult - -} - class DefaultSegmentsSyncHelper: SegmentsSyncHelper { struct FetchResult { let msTill: Int64 @@ -96,33 +68,19 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { let msUpdated: Bool let mlsUdated: Bool } - + private let segmentsFetcher: HttpMySegmentsFetcher private let mySegmentsStorage: ByKeyMySegmentsStorage private let myLargeSegmentsStorage: ByKeyMySegmentsStorage private let splitConfig: SplitClientConfig private let userKey: String private let changeChecker: MySegmentsChangesChecker - - private var maxAttempts: Int { - return splitConfig.cdnByPassMaxAttempts - } - - private var backoffTimeBaseInSecs: Int { - return splitConfig.cdnBackoffTimeBaseInSecs - } - - private var backoffTimeMaxInSecs: Int { - return splitConfig.cdnBackoffTimeMaxInSecs - } - - init(userKey: String, - segmentsFetcher: HttpMySegmentsFetcher, - mySegmentsStorage: ByKeyMySegmentsStorage, - myLargeSegmentsStorage: ByKeyMySegmentsStorage, - changeChecker: MySegmentsChangesChecker, - splitConfig: SplitClientConfig) { - + + private var maxAttempts: Int { splitConfig.cdnByPassMaxAttempts } + private var backoffTimeBaseInSecs: Int { splitConfig.cdnBackoffTimeBaseInSecs } + private var backoffTimeMaxInSecs: Int { splitConfig.cdnBackoffTimeMaxInSecs } + + init(userKey: String, segmentsFetcher: HttpMySegmentsFetcher, mySegmentsStorage: ByKeyMySegmentsStorage, myLargeSegmentsStorage: ByKeyMySegmentsStorage, changeChecker: MySegmentsChangesChecker, splitConfig: SplitClientConfig) { self.userKey = userKey self.segmentsFetcher = segmentsFetcher self.mySegmentsStorage = mySegmentsStorage @@ -130,43 +88,33 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { self.splitConfig = splitConfig self.changeChecker = changeChecker } - - func sync(msTill: Int64 = -1, - mlsTill: Int64 = -1, - headers: HttpHeaders? = nil) throws -> SegmentsSyncResult { + + // MARK: Entry Point + func sync(msTill: Int64 = -1, mlsTill: Int64 = -1, headers: HttpHeaders? = nil) throws -> SegmentsSyncResult { do { - let res = try tryToSync(msTill: msTill, - mlsTill: mlsTill, - headers: headers) - - if res.success { - return res + let result = try tryToSync(msTill: msTill, mlsTill: mlsTill, headers: headers) + + if result.success { + return result + } else { + return try tryToSync(msTill: result.msChangeNumber, mlsTill: result.mlsChangeNumber, headers: headers, useTillParam: true) } - - return try tryToSync(msTill: res.msChangeNumber, - mlsTill: res.mlsChangeNumber, - headers: headers, - useTillParam: true) } catch let error { Logger.e("Problem fetching segments %@", error.localizedDescription) throw error } } - - private func tryToSync(msTill: Int64, - mlsTill: Int64, - headers: HttpHeaders? = nil, - useTillParam: Bool = false) throws -> SegmentsSyncResult { - - let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: backoffTimeBaseInSecs, - maxTimeLimit: backoffTimeMaxInSecs) + + // MARK: Retry Logic w/ Backoff + private func tryToSync(msTill: Int64, mlsTill: Int64, headers: HttpHeaders? = nil, useTillParam: Bool = false) throws -> SegmentsSyncResult { + let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: backoffTimeBaseInSecs, maxTimeLimit: backoffTimeMaxInSecs) var attemptCount = 0 let goalTill = SegmentsChangeNumber(msChangeNumber: msTill, mlsChangeNumber: mlsTill) let till = useTillParam ? goalTill.max() : nil + while attemptCount < maxAttempts { - let result = try fetchUntil(till: till, - headers: headers) - + let result = try fetchUntil(till: till, headers: headers) + if goalReached(goalTill: goalTill, result: result) { return SegmentsSyncResult(success: true, msChangeNumber: result.msTill, @@ -174,67 +122,54 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { msUpdated: result.msUpdated, mlsUpdated: result.mlsUdated) } + + // Backoff attemptCount+=1 if attemptCount < maxAttempts { Thread.sleep(forTimeInterval: backoffCounter.getNextRetryTime()) } } - return SegmentsSyncResult(success: false, - msChangeNumber: -1, - mlsChangeNumber: -1, - msUpdated: false, - mlsUpdated: false) + + return SegmentsSyncResult(success: false, msChangeNumber: -1, mlsChangeNumber: -1, msUpdated: false, mlsUpdated: false) } - - private func fetchUntil(till: Int64?, - headers: HttpHeaders? = nil) throws -> FetchResult { - - let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), - changeNumber: mySegmentsStorage.changeNumber) - - let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), - changeNumber: myLargeSegmentsStorage.changeNumber) - - var prevChange = AllSegmentsChange(mySegmentsChange: oldChange, - myLargeSegmentsChange: oldLargeChange) + + // MARK: Sync loop (check if outdated, and re-sync until latest version) + private func fetchUntil(till: Int64?, headers: HttpHeaders? = nil) throws -> FetchResult { + + let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), changeNumber: mySegmentsStorage.changeNumber) + let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), changeNumber: myLargeSegmentsStorage.changeNumber) + 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") } - - let mySegmentsChange = change.mySegmentsChange - 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: change.mySegmentsChange) + let mlsChanged = changeChecker.mySegmentsHaveChanged(old: oldLargeChange, new: change.myLargeSegmentsChange) Logger.d("Checking my segments update") - checkAndUpdate(isChanged: msChanged, change: mySegmentsChange, storage: mySegmentsStorage) + checkAndUpdate(isChanged: msChanged, change: change.mySegmentsChange, storage: mySegmentsStorage) Logger.d("Checking my large segments update") - checkAndUpdate(isChanged: mlsChanged, change: myLargeSegmentsChange, storage: myLargeSegmentsStorage) - - return FetchResult(msTill: mySegmentsChange.unwrappedChangeNumber, - mlsTill: myLargeSegmentsChange.unwrappedChangeNumber, + checkAndUpdate(isChanged: mlsChanged, change: change.myLargeSegmentsChange, storage: myLargeSegmentsStorage) + + return FetchResult(msTill: change.mySegmentsChange.unwrappedChangeNumber, + mlsTill: change.myLargeSegmentsChange.unwrappedChangeNumber, msUpdated: msChanged, mlsUdated: mlsChanged) } prevChange = change } } +} - private func isOutdated(_ change: AllSegmentsChange, - _ prevChange: AllSegmentsChange?) -> Bool { - - guard let prevChange = prevChange else { - return true - } +// MARK: Segments Sync Helpers +extension DefaultSegmentsSyncHelper { + private func isOutdated(_ change: AllSegmentsChange, _ prevChange: AllSegmentsChange?) -> Bool { + guard let prevChange = prevChange else { return true } return change.changeNumbers.msChangeNumber < prevChange.changeNumbers.msChangeNumber || - change.changeNumbers.mlsChangeNumber < prevChange.changeNumbers.mlsChangeNumber + change.changeNumbers.mlsChangeNumber < prevChange.changeNumbers.mlsChangeNumber } private func checkAndUpdate(isChanged: Bool, change: SegmentChange, storage: ByKeyMySegmentsStorage) { @@ -246,6 +181,18 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { } private func goalReached(goalTill: SegmentsChangeNumber, result: FetchResult) -> Bool { - return (result.msTill >= goalTill.msChangeNumber && result.mlsTill >= goalTill.mlsChangeNumber) + (result.msTill >= goalTill.msChangeNumber && result.mlsTill >= goalTill.mlsChangeNumber) } } + +struct SegmentsSyncResult { + let success: Bool + let msChangeNumber: Int64 + let mlsChangeNumber: Int64 + let msUpdated: Bool + let mlsUpdated: Bool +} + +protocol SegmentsSyncHelper { + func sync(msTill: Int64, mlsTill: Int64, headers: HttpHeaders?) throws -> SegmentsSyncResult +} diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 41d5c2581..3ba683b4d 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -143,6 +143,7 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) if result.success { + // Success if !isSdkReadyTriggered() || result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) @@ -150,12 +151,14 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { resetBackoffCounter() return true } else { - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method: \(error.localizedDescription)") - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) errorHandler?(error) } @@ -224,18 +227,21 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) if result.success { + // Success if result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true } else { - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method \(#function): \(error.localizedDescription)") - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) errorHandler?(error) } diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 0d89a366d..5df805b43 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -96,7 +96,7 @@ class SplitChangesServerErrorTest: XCTestCase { if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments } - return TestDispatcherResponse(code: 500) + return TestDispatcherResponse(code: 200) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) @@ -118,7 +118,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + XCTAssertEqual(errorType, .segmentsSyncError) cleanup(client, &factory) } @@ -153,7 +153,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + XCTAssertEqual(errorType, .featureFlagsSyncError) cleanup(client, &factory) } @@ -169,7 +169,7 @@ class SplitChangesServerErrorTest: XCTestCase { if request.isSplitEndpoint() { return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // OK for Splits, but bad JSON } - return TestDispatcherResponse(code: 500) + return TestDispatcherResponse(code: 200) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) @@ -191,7 +191,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + XCTAssertEqual(errorType, .featureFlagsSyncError) cleanup(client, &factory) } @@ -229,7 +229,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + XCTAssertEqual(errorType, .segmentsSyncError) cleanup(client, &factory) } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 98acad400..8efc0267a 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -302,7 +302,7 @@ class SplitEventsManagerTest: XCTestCase { let data = ["TEST_DATA_123456"] // Build Task - let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: data) + let metadata = EventMetadata(type: .featureFlagsSyncError, data: data) let handler: SplitActionWithMetadata = { handlerMetadata in XCTAssertEqual(metadata.type, handlerMetadata.type) diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift index 76f87ce19..c575d7162 100644 --- a/SplitTests/SplitEventsTests.swift +++ b/SplitTests/SplitEventsTests.swift @@ -5,9 +5,9 @@ import XCTest class SplitEventsTests: XCTestCase { func testInternalEventsWithMetadataErrorType() { - var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") - event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .segmentsSyncError, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "SEGMENTS_SYNC_ERROR") } } From b845d465ebe6acefadfe78de3f7b1e0691045604 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 12:28:31 -0300 Subject: [PATCH 24/35] Better comments. Errors now on camel case to follow convention. --- Split/Events/SplitInternalEvent.swift | 8 ++++---- .../Refresh/PeriodicSyncWorker.swift | 18 ++++++++---------- .../Refresh/RetryableSegmentsSyncWorker.swift | 8 ++++---- .../Refresh/RetryableSyncWorker.swift | 14 ++++++++++---- .../Sync/SplitChangesServerErrorTest.swift | 14 +++++++------- SplitTests/SplitEventsManagerTest.swift | 2 +- SplitTests/SplitEventsTests.swift | 4 ++-- 7 files changed, 36 insertions(+), 32 deletions(-) diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 3adf18ddb..a26d4ccb6 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -30,14 +30,14 @@ struct SplitInternalEventWithMetadata { } enum EventMetadataType: Int { - case FEATURE_FLAGS_SYNC_ERROR - case SEGMENTS_SYNC_ERROR + case featureFlagsSyncError + case segmentsSyncError public func toString() -> String { switch self { - case .FEATURE_FLAGS_SYNC_ERROR: + case .featureFlagsSyncError: return "FEATURE_FLAGS_SYNC_ERROR" - case .SEGMENTS_SYNC_ERROR: + case .segmentsSyncError: return "SEGMENTS_SYNC_ERROR" } } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index d534e9687..5a93e76ed 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -184,19 +184,17 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { let changeNumber = splitsStorage.changeNumber let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) return } if result.success, result.featureFlagsUpdated || result.rbsUpdated { - // SUCCESS + // Success notifyUpdate(.splitsUpdated) } else if !result.success { - // FAIL - var event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) - notifyUpdate(event) - event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(event) } } @@ -241,13 +239,13 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { notifyUpdate(.mySegmentsUpdated) } } else { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) } } catch { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) Logger.e("Problem fetching segments: %@", error.localizedDescription) } diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 8e25e0ff8..1bb831c53 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -54,14 +54,14 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { } return true } else { - // FAIL - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) } } catch { - // FAIL + // Fail Logger.e("Error while fetching segments in method: \(error.localizedDescription)") - let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) notifyUpdate(event) errorHandler?(error) } diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 41d5c2581..3ba683b4d 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -143,6 +143,7 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) if result.success { + // Success if !isSdkReadyTriggered() || result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) @@ -150,12 +151,14 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { resetBackoffCounter() return true } else { - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method: \(error.localizedDescription)") - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) errorHandler?(error) } @@ -224,18 +227,21 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) if result.success { + // Success if result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true } else { - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method \(#function): \(error.localizedDescription)") - let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) notifyUpdate(eventWithMetadata) errorHandler?(error) } diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 0d89a366d..1511c4c3c 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -96,7 +96,7 @@ class SplitChangesServerErrorTest: XCTestCase { if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments } - return TestDispatcherResponse(code: 500) + return TestDispatcherResponse(code: 200) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) @@ -118,7 +118,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + XCTAssertEqual(errorType, .segmentsSyncError) cleanup(client, &factory) } @@ -153,13 +153,13 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + XCTAssertEqual(errorType, EventMetadataType.featureFlagsSyncError) cleanup(client, &factory) } // MARK: Getting malformed flags from server - func testResponseFlagsParserror() throws { + func testResponseFlagsParseError() throws { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in @@ -169,7 +169,7 @@ class SplitChangesServerErrorTest: XCTestCase { if request.isSplitEndpoint() { return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // OK for Splits, but bad JSON } - return TestDispatcherResponse(code: 500) + return TestDispatcherResponse(code: 200) } let session = HttpSessionMock() let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) @@ -191,7 +191,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.FEATURE_FLAGS_SYNC_ERROR) + XCTAssertEqual(errorType, EventMetadataType.featureFlagsSyncError) cleanup(client, &factory) } @@ -229,7 +229,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.SEGMENTS_SYNC_ERROR) + XCTAssertEqual(errorType,.segmentsSyncError) cleanup(client, &factory) } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 98acad400..8efc0267a 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -302,7 +302,7 @@ class SplitEventsManagerTest: XCTestCase { let data = ["TEST_DATA_123456"] // Build Task - let metadata = EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: data) + let metadata = EventMetadata(type: .featureFlagsSyncError, data: data) let handler: SplitActionWithMetadata = { handlerMetadata in XCTAssertEqual(metadata.type, handlerMetadata.type) diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift index 76f87ce19..c575d7162 100644 --- a/SplitTests/SplitEventsTests.swift +++ b/SplitTests/SplitEventsTests.swift @@ -5,9 +5,9 @@ import XCTest class SplitEventsTests: XCTestCase { func testInternalEventsWithMetadataErrorType() { - var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .FEATURE_FLAGS_SYNC_ERROR, data: [])) + var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") - event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .SEGMENTS_SYNC_ERROR, data: [])) + event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .segmentsSyncError, data: [])) XCTAssertEqual(event.metadata!.type.toString(), "SEGMENTS_SYNC_ERROR") } } From e6d87a5c610857c5d4e4353346bfc4f663634914 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 12:32:08 -0300 Subject: [PATCH 25/35] Removed unnecessary types to let Swift infer it --- .../Integration/Sync/SplitChangesServerErrorTest.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 1511c4c3c..cc6d3fb12 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -153,7 +153,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.featureFlagsSyncError) + XCTAssertEqual(errorType, .featureFlagsSyncError) cleanup(client, &factory) } @@ -191,7 +191,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType, EventMetadataType.featureFlagsSyncError) + XCTAssertEqual(errorType, .featureFlagsSyncError) cleanup(client, &factory) } @@ -229,7 +229,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Test wait(for: [sdkError], timeout: 5) - XCTAssertEqual(errorType,.segmentsSyncError) + XCTAssertEqual(errorType, .segmentsSyncError) cleanup(client, &factory) } From 20385cbca163f52e52bae6689605be184d6ee4fd Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 12:54:52 -0300 Subject: [PATCH 26/35] New comments --- .../Refresh/PeriodicSyncWorker.swift | 28 +++++---------- .../Refresh/RetryableSyncWorker.swift | 34 +++++++++---------- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index f603a3653..d0b0c37f3 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -35,9 +35,8 @@ class DefaultPeriodicTimer: PeriodicTimer { func trigger() { if !isRunning.getAndSet(true) { - fetchTimer.schedule(deadline: .now() + .seconds(deadLineInSecs), - repeating: .seconds(intervalInSecs)) -// fetchTimer.resume() + fetchTimer.schedule(deadline: .now() + .seconds(deadLineInSecs), repeating: .seconds(intervalInSecs)) + // fetchTimer.resume() } } @@ -77,14 +76,12 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { private let eventsManager: SplitEventsManager private var isPaused: Atomic = Atomic(false) - init(timer: PeriodicTimer, - eventsManager: SplitEventsManager) { + init(timer: PeriodicTimer, eventsManager: SplitEventsManager) { self.eventsManager = eventsManager self.fetchTimer = timer self.fetchTimer.handler { [weak self] in - guard let self = self else { - return - } + guard let self = self else { return } + if self.isPaused.value { return } @@ -181,7 +178,7 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { let changeNumber = splitsStorage.changeNumber let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber - // Try to Sync + // 1. Try to Sync guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { // Fail let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) @@ -189,7 +186,7 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { return } - // Result + // 2. Process Result if result.success, result.featureFlagsUpdated || result.rbsUpdated { // Success notifyUpdate(.splitsUpdated) @@ -209,20 +206,13 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { private let telemetryProducer: TelemetryRuntimeProducer? private let syncHelper: SegmentsSyncHelper - init(mySegmentsStorage: ByKeyMySegmentsStorage, - myLargeSegmentsStorage: ByKeyMySegmentsStorage, - telemetryProducer: TelemetryRuntimeProducer?, - timer: PeriodicTimer, - eventsManager: SplitEventsManager, - syncHelper: SegmentsSyncHelper) { - + init(mySegmentsStorage: ByKeyMySegmentsStorage, myLargeSegmentsStorage: ByKeyMySegmentsStorage, telemetryProducer: TelemetryRuntimeProducer?, timer: PeriodicTimer, eventsManager: SplitEventsManager, syncHelper: SegmentsSyncHelper) { self.mySegmentsStorage = mySegmentsStorage self.myLargeSegmentsStorage = myLargeSegmentsStorage self.telemetryProducer = telemetryProducer self.syncHelper = syncHelper - super.init(timer: timer, - eventsManager: eventsManager) + super.init(timer: timer, eventsManager: eventsManager) } override func fetchFromRemote() { diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 3ba683b4d..b43e5269b 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -32,9 +32,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { private var isRunning: Atomic = Atomic(false) private let syncQueue = DispatchQueue.general - init(eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter) { - + init(eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter) { self.eventsManager = eventsManager self.reconnectBackoffCounter = reconnectBackoffCounter } @@ -65,6 +63,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { private func fetchFromRemoteLoop() throws { var success = false + while isRunning.value, !success { success = try fetchFromRemote() if !success { @@ -73,7 +72,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { ThreadUtils.delay(seconds: retryTimeInSeconds) } } - self.isRunning.set(false) + isRunning.set(false) if let handler = completion { handler(success) } @@ -133,19 +132,21 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { do { let changeNumber = splitsStorage.changeNumber let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber + + // 1. Try to Sync let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) + + // 2. Process Result if result.success { // Success - if !isSdkReadyTriggered() || - result.featureFlagsUpdated { + if !isSdkReadyTriggered() || result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) } resetBackoffCounter() @@ -203,11 +204,11 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { + let storedChangeNumber = splitsStorage.changeNumber let flagsChangeNumber = changeNumber.flags if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { @@ -221,16 +222,13 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { } do { - let result = try syncHelper.sync(since: storedChangeNumber, - rbSince: storedRbChangeNumber, - till: flagsChangeNumber ?? rbsChangeNumber, - clearBeforeUpdate: false, - headers: ServiceConstants.controlNoCacheHeader) + // 1. Try to sync + let result = try syncHelper.sync(since: storedChangeNumber, rbSince: storedRbChangeNumber, till: flagsChangeNumber ?? rbsChangeNumber, clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) + + // 2. Process result if result.success { // Success - if result.featureFlagsUpdated { - notifyUpdate(.splitsUpdated) - } + if result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true } else { From d0b2b852908b7dca75d5c39c15d0f40bd2be1fe4 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 13:06:01 -0300 Subject: [PATCH 27/35] Initializers shrinked for better readability --- .../Refresh/PeriodicSyncWorker.swift | 28 +++------- .../Refresh/RetryableSyncWorker.swift | 53 +++---------------- 2 files changed, 14 insertions(+), 67 deletions(-) diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index d0b0c37f3..ee2aa2862 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -1,12 +1,10 @@ -// // PeriodicSplitsSyncWorker.swift // Split // // Created by Javier Avrudsky on 26-Sep-2020 -// -// import Foundation + protocol PeriodicTimer { func trigger() func stop() @@ -60,8 +58,8 @@ class DefaultPeriodicTimer: PeriodicTimer { } protocol PeriodicSyncWorker { - // typealias SyncCompletion = (Bool) -> Void - // var completion: SyncCompletion? { get set } + // typealias SyncCompletion = (Bool) -> Void + // var completion: SyncCompletion? { get set } func start() func pause() func resume() @@ -147,28 +145,14 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { private let ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper - init(splitFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - generalInfoStorage: GeneralInfoStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, - timer: PeriodicTimer, - eventsManager: SplitEventsManager, - splitConfig: SplitClientConfig) { - + init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, generalInfoStorage: GeneralInfoStorage, ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, timer: PeriodicTimer, eventsManager: SplitEventsManager, splitConfig: SplitClientConfig) { self.splitFetcher = splitFetcher self.splitsStorage = splitsStorage self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor self.ruleBasedSegmentsChangeProcessor = ruleBasedSegmentsChangeProcessor - self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, - splitsStorage: splitsStorage, - ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, - splitChangeProcessor: splitChangeProcessor, - ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) + super.init(timer: timer, eventsManager: eventsManager) } diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index b43e5269b..7947654b6 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -1,10 +1,7 @@ -// // RetryableSyncWorker.swift // Split // // Created by Javier Avrudsky on 15-Sep-2020 -// -// import Foundation @@ -110,28 +107,14 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { private let ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper - init(splitFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - generalInfoStorage: GeneralInfoStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - splitConfig: SplitClientConfig) { - + init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, generalInfoStorage: GeneralInfoStorage, ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, splitConfig: SplitClientConfig) { self.splitFetcher = splitFetcher self.splitsStorage = splitsStorage self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor - self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, - splitsStorage: splitsStorage, - ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, - splitChangeProcessor: splitChangeProcessor, - ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, generalInfoStorage: generalInfoStorage,splitConfig: splitConfig) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } @@ -178,17 +161,7 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { private let syncHelper: SplitsSyncHelper var changeChecker: SplitsChangesChecker - init(splitsFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - generalInfoStorage: GeneralInfoStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, - changeNumber: SplitsUpdateChangeNumber, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - splitConfig: SplitClientConfig) { - + init(splitsFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, generalInfoStorage: GeneralInfoStorage, splitChangeProcessor: SplitChangeProcessor, ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, changeNumber: SplitsUpdateChangeNumber, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, splitConfig: SplitClientConfig) { self.splitsFetcher = splitsFetcher self.splitsStorage = splitsStorage self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage @@ -196,14 +169,8 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor self.changeNumber = changeNumber self.changeChecker = DefaultSplitsChangesChecker() - - self.syncHelper = SplitsSyncHelper(splitFetcher: splitsFetcher, - splitsStorage: splitsStorage, - ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, - splitChangeProcessor: splitChangeProcessor, - ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitsFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage,splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor,generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } @@ -211,15 +178,11 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { let storedChangeNumber = splitsStorage.changeNumber let flagsChangeNumber = changeNumber.flags - if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { - return true - } + if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { return true } let storedRbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber let rbsChangeNumber = changeNumber.rbs - if let rbsChangeNumber, rbsChangeNumber <= storedRbChangeNumber { - return true - } + if let rbsChangeNumber, rbsChangeNumber <= storedRbChangeNumber { return true } do { // 1. Try to sync From 8544340e8c36a14fb4d52f7a0dd4d2369e614568 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 13:08:24 -0300 Subject: [PATCH 28/35] Methods requeriments condensed --- .../FetcherEngine/Refresh/PeriodicSyncWorker.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index ee2aa2862..6f01481e1 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -39,8 +39,7 @@ class DefaultPeriodicTimer: PeriodicTimer { } func stop() { - // Not suspending the timer to avoid crashes - isRunning.set(false) + isRunning.set(false) // Not suspending the timer to avoid crashes } func destroy() { @@ -77,12 +76,10 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { init(timer: PeriodicTimer, eventsManager: SplitEventsManager) { self.eventsManager = eventsManager self.fetchTimer = timer + self.fetchTimer.handler { [weak self] in - guard let self = self else { return } - - if self.isPaused.value { - return - } + guard let self = self, self.isPaused.value else { return } + self.fetchQueue.async { self.fetchFromRemote() } @@ -203,8 +200,10 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { if !isSdkReadyFired() { return } // Polling should be done once sdk ready is fired in initial sync do { + // 1. Try to sync let result = try syncHelper.sync(msTill: mySegmentsStorage.changeNumber, mlsTill: myLargeSegmentsStorage.changeNumber, headers: nil) + // 2. Process result if result.success { // Success if result.msUpdated || result.mlsUpdated { From 9ca00101921d256f6081dae735b93d326b122c1b Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Wed, 13 Aug 2025 13:10:56 -0300 Subject: [PATCH 29/35] Minor details --- Split/FetcherEngine/Refresh/RetryableSyncWorker.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 7947654b6..06afff315 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -36,12 +36,11 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { func start() { syncQueue.async { [weak self] in - guard let self = self else { return } - if self.isRunning.value { - return - } + guard let self = self, self.isRunning.value else { return } + self.isRunning.set(true) self.reconnectBackoffCounter.resetCounter() + do { try self.fetchFromRemoteLoop() } catch { From 42c1dd130197c734cb43c175db50dbc1146114fc Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Thu, 14 Aug 2025 12:01:17 -0300 Subject: [PATCH 30/35] Unintended Synchronization bug removed --- Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift | 6 +++++- Split/FetcherEngine/Refresh/RetryableSyncWorker.swift | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index 6f01481e1..a518e7d80 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -78,7 +78,11 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { self.fetchTimer = timer self.fetchTimer.handler { [weak self] in - guard let self = self, self.isPaused.value else { return } + guard let self = self else { return } + + if !self.isPaused.value { + self.isPaused.set(true) + } self.fetchQueue.async { self.fetchFromRemote() diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 06afff315..b9d700cc9 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -36,9 +36,12 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { func start() { syncQueue.async { [weak self] in - guard let self = self, self.isRunning.value else { return } + guard let self = self else { return } - self.isRunning.set(true) + if !self.isRunning.value { + self.isRunning.set(true) + } + self.reconnectBackoffCounter.resetCounter() do { From a444c3bf7bc5bd197dbf09e919ec8fb2973e8e03 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Thu, 14 Aug 2025 22:12:19 -0300 Subject: [PATCH 31/35] Retry on failure for all tests --- SplitiOSFull.xctestplan | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SplitiOSFull.xctestplan b/SplitiOSFull.xctestplan index cb7f2205a..19c4fcc72 100644 --- a/SplitiOSFull.xctestplan +++ b/SplitiOSFull.xctestplan @@ -32,7 +32,8 @@ "containerPath" : "container:Split.xcodeproj", "identifier" : "3B6DEE5920EA6A4E0067435E", "name" : "Split" - } + }, + "testRepetitionMode" : "retryOnFailure" }, "testTargets" : [ { From 74b6ce57ac129b2a98cca8d44b3e12b4b81d840b Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Thu, 14 Aug 2025 22:44:16 -0300 Subject: [PATCH 32/35] Flaky test of wrong JSON for Segments fixed --- .../Integration/Sync/SplitChangesServerErrorTest.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index cc6d3fb12..da9f1cecc 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -31,8 +31,8 @@ class SplitChangesServerErrorTest: XCTestCase { var httpClient: HttpClient! var streamingBinding: TestStreamResponseBinding? var splitConfig: SplitClientConfig? - let apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" - let key: Key = Key(matchingKey: "CUSTOMER_ID", bucketingKey: nil) + var apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" + var key: Key = Key(matchingKey: "CUSTOMER_ID", bucketingKey: nil) let builder = DefaultSplitFactoryBuilder() override func setUp() { @@ -91,7 +91,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges.utf8)) // OK Splits } if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments From ed87a883a8c2deb13716753c70d9f77bd51300e3 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Thu, 14 Aug 2025 23:11:48 -0300 Subject: [PATCH 33/35] Test Fixed --- SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index da9f1cecc..10ed21b3d 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -91,7 +91,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges.utf8)) // OK Splits + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits } if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments From 6102e9f33e7cbe86dd189378d6df7f8842b45a86 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Fri, 15 Aug 2025 09:27:41 -0300 Subject: [PATCH 34/35] Flaky test fixed --- SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift | 2 +- SplitiOSFull.xctestplan | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 10ed21b3d..f6723f9d2 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -91,7 +91,7 @@ class SplitChangesServerErrorTest: XCTestCase { // Networking setup let dispatcher: HttpClientTestDispatcher = { request in if request.isSplitEndpoint() { - return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_1"))) // OK Splits } if request.isMySegmentsEndpoint() { return TestDispatcherResponse(code: 500) // Error for Segments diff --git a/SplitiOSFull.xctestplan b/SplitiOSFull.xctestplan index 19c4fcc72..cb7f2205a 100644 --- a/SplitiOSFull.xctestplan +++ b/SplitiOSFull.xctestplan @@ -32,8 +32,7 @@ "containerPath" : "container:Split.xcodeproj", "identifier" : "3B6DEE5920EA6A4E0067435E", "name" : "Split" - }, - "testRepetitionMode" : "retryOnFailure" + } }, "testTargets" : [ { From 0715bff5634a9e01230bceb0412f301891791f33 Mon Sep 17 00:00:00 2001 From: Martin Cardozo Date: Fri, 15 Aug 2025 10:42:14 -0300 Subject: [PATCH 35/35] StorageHelper restored --- Split.xcodeproj/project.pbxproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 0a3dcd9c2..8271df2ee 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -354,6 +354,7 @@ 59FB7C3C2203795F00ECC96A /* LocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */; }; 59FB7C3E22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */; }; 5B0162682E4A9C7A0009D3B7 /* SplitEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */; }; + 5B26B0D72E4F70A00025AAB7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; 5B279CF92E340FC600B73A36 /* splitschanges_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */; }; 5B343EAD2E26E93B006BEBE7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; 5B48D8172DEA2CED00351925 /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; @@ -4251,7 +4252,7 @@ 59F4AA9B24FE93E300A1C69A /* NotificationManagerKeeper.swift in Sources */, 95C1600D27D28CF4008562E3 /* PersistentAttributesStorage.swift in Sources */, 95726075262F548500350CCA /* SplitBgSynchronizer.swift in Sources */, - 5B343EAE2E26E93B006BEBE7 /* StorageHelper.swift in Sources */, + 5B26B0D72E4F70A00025AAB7 /* StorageHelper.swift in Sources */, 95C1600B27D28CB8008562E3 /* OneKeyPersistentAttributesStorage.swift in Sources */, 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */, 9519A91127D6935700278AEC /* ByKeyAttributesStorage.swift in Sources */,