diff --git a/Examples/apps/BasicExample/BasicExample.xcodeproj/project.pbxproj b/Examples/apps/BasicExample/BasicExample.xcodeproj/project.pbxproj index 87e3f1f7..b0d77e27 100644 --- a/Examples/apps/BasicExample/BasicExample.xcodeproj/project.pbxproj +++ b/Examples/apps/BasicExample/BasicExample.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 4663C72A267A8D6B00ADDD1A /* BasicExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BasicExample.entitlements; sourceTree = ""; }; 469F7AF8265C25890038E773 /* EventData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventData.swift; sourceTree = ""; }; 46E38361265837EA00BA2502 /* BasicExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BasicExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46E38364265837EA00BA2502 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -63,6 +64,7 @@ 46E38363265837EA00BA2502 /* BasicExample */ = { isa = PBXGroup; children = ( + 4663C72A267A8D6B00ADDD1A /* BasicExample.entitlements */, 46E38364265837EA00BA2502 /* AppDelegate.swift */, 46E38366265837EA00BA2502 /* SceneDelegate.swift */, 469F7AF8265C25890038E773 /* EventData.swift */, @@ -309,7 +311,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_ENTITLEMENTS = BasicExample/BasicExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; INFOPLIST_FILE = BasicExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -317,6 +321,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.segment.BasicExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -327,7 +333,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_ENTITLEMENTS = BasicExample/BasicExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; INFOPLIST_FILE = BasicExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -335,6 +343,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.segment.BasicExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/Examples/apps/BasicExample/BasicExample/BasicExample.entitlements b/Examples/apps/BasicExample/BasicExample/BasicExample.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/Examples/apps/BasicExample/BasicExample/BasicExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift index 0748fce4..df4f0e5a 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift +++ b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift @@ -16,7 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - let configuration = Configuration(writeKey: "") + let configuration = Configuration(writeKey: "1234") .trackApplicationLifecycleEvents(true) .flushInterval(10) diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard index 55b776ab..101cad41 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard +++ b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard @@ -1,6 +1,6 @@ - + @@ -13,7 +13,7 @@ - + + + + + + + + + + diff --git a/Examples/apps/MacExample/MacExample/Info.plist b/Examples/apps/MacExample/MacExample/Info.plist new file mode 100644 index 00000000..cfbbdb70 --- /dev/null +++ b/Examples/apps/MacExample/MacExample/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/Examples/apps/MacExample/MacExample/MacExample.entitlements b/Examples/apps/MacExample/MacExample/MacExample.entitlements new file mode 100644 index 00000000..f2ef3ae0 --- /dev/null +++ b/Examples/apps/MacExample/MacExample/MacExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Examples/apps/MacExample/MacExample/ViewController.swift b/Examples/apps/MacExample/MacExample/ViewController.swift new file mode 100644 index 00000000..9ccc81d7 --- /dev/null +++ b/Examples/apps/MacExample/MacExample/ViewController.swift @@ -0,0 +1,30 @@ +// +// ViewController.swift +// MacExample +// +// Created by Brandon Sneed on 6/16/21. +// + +import Cocoa + +class ViewController: NSViewController { + var analytics = NSApplication.shared.delegate?.analytics + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override var representedObject: Any? { + didSet { + // Update the view, if already loaded. + } + } + + + @IBAction func trackButton(_ sender: Any) { + analytics?.track(name: "test event") + } +} + diff --git a/Segment.xcodeproj/project.pbxproj b/Segment.xcodeproj/project.pbxproj index 4d2a326c..2d05a971 100644 --- a/Segment.xcodeproj/project.pbxproj +++ b/Segment.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 46210811260538BE00EBC4A8 /* KeyPath_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46210810260538BE00EBC4A8 /* KeyPath_Tests.swift */; }; 46210836260BBEE400EBC4A8 /* DeviceToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46210835260BBEE400EBC4A8 /* DeviceToken.swift */; }; 4658175425BA4C20006B2809 /* HTTPClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4658175325BA4C20006B2809 /* HTTPClient_Tests.swift */; }; + 4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4663C728267A799100ADDD1A /* QueueTimer.swift */; }; 46A018C225E5857D00F9CCD8 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018C125E5857D00F9CCD8 /* Context.swift */; }; 46A018D425E6C9C200F9CCD8 /* LinuxUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018D325E6C9C200F9CCD8 /* LinuxUtils.swift */; }; 46A018DA25E97FDF00F9CCD8 /* AppleUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A018D925E97FDF00F9CCD8 /* AppleUtils.swift */; }; @@ -109,6 +110,7 @@ 46210810260538BE00EBC4A8 /* KeyPath_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPath_Tests.swift; sourceTree = ""; }; 46210835260BBEE400EBC4A8 /* DeviceToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceToken.swift; sourceTree = ""; }; 4658175325BA4C20006B2809 /* HTTPClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient_Tests.swift; sourceTree = ""; }; + 4663C728267A799100ADDD1A /* QueueTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTimer.swift; sourceTree = ""; }; 46A018C125E5857D00F9CCD8 /* Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; 46A018D325E6C9C200F9CCD8 /* LinuxUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinuxUtils.swift; sourceTree = ""; }; 46A018D925E97FDF00F9CCD8 /* AppleUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleUtils.swift; sourceTree = ""; }; @@ -267,6 +269,7 @@ 4621080B2605332D00EBC4A8 /* KeyPath.swift */, 46022770261F7A4800A9E913 /* Atomic.swift */, 46E382E62654429A00BA2502 /* Utils.swift */, + 4663C728267A799100ADDD1A /* QueueTimer.swift */, ); path = Utilities; sourceTree = ""; @@ -457,6 +460,7 @@ A31A16E12579779600C9CDDF /* Version.swift in Sources */, 46210836260BBEE400EBC4A8 /* DeviceToken.swift in Sources */, 9692724E25A4E5B7009B5298 /* Startup.swift in Sources */, + 4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */, 46FE4C9C25A3F41C003A7362 /* LinuxLifecycleMonitor.swift in Sources */, 460227422612987300A9E913 /* watchOSLifecycleEvents.swift in Sources */, A31A162F2576B73F00C9CDDF /* State.swift in Sources */, diff --git a/Sources/Segment/Plugins.swift b/Sources/Segment/Plugins.swift index 5582769a..b8979bd0 100644 --- a/Sources/Segment/Plugins.swift +++ b/Sources/Segment/Plugins.swift @@ -141,16 +141,6 @@ extension Analytics { */ @discardableResult public func add(plugin: Plugin) -> String { - // we need to know if the system is already started. - var wasStarted = false - if let system: System = store.currentState(), system.started { - wasStarted = system.started - // if it was, we need to stop it temporarily. - store.dispatch(action: System.SetStartedAction(started: false)) - // adding the plugin to the timeline below will eventually call - // update(settings:) at which point, we can start it up again. - } - plugin.configure(analytics: self) timeline.add(plugin: plugin) if plugin is DestinationPlugin && !(plugin is SegmentDestination) { @@ -158,12 +148,6 @@ extension Analytics { store.dispatch(action: System.AddIntegrationAction(pluginName: plugin.name)) } - // if the timeline had started before, set it back to started since - // update(settings:) will have been called by now. - if wasStarted { - store.dispatch(action: System.SetStartedAction(started: true)) - } - return plugin.name } diff --git a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift index 6ad8ad4d..f752ddf1 100644 --- a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift @@ -230,11 +230,14 @@ class macOSLifecycleMonitor: PlatformPlugin { } extension SegmentDestination: macOSLifecycle { + public func applicationDidBecomeActive() { + flushTimer?.resume() + } - public func applicationDidEnterBackground() { - // TODO: Look into mac background tasks - //analytics.beginBackgroundTask() + public func applicationWillResignActive() { + flushTimer?.suspend() flush() } } + #endif diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSAppBackground.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSAppBackground.swift index f9fb1014..e3e492aa 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSAppBackground.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSAppBackground.swift @@ -5,7 +5,7 @@ // Created by Cody Garvin on 1/14/21. // -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) import UIKit class iOSAppBackground: PlatformPlugin { diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSDelegation.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSDelegation.swift index 6439b235..28df4f38 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSDelegation.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSDelegation.swift @@ -7,7 +7,7 @@ import Foundation -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) import UIKit diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift index 503311f9..e111aef7 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift @@ -7,7 +7,7 @@ import Foundation -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) import UIKit diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 51f3d046..82bfddad 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -5,7 +5,7 @@ // Created by Cody Garvin on 12/4/20. // -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) import Foundation import UIKit @@ -169,9 +169,15 @@ class iOSLifecycleMonitor: PlatformPlugin { } } +// MARK: - Segment Destination Extension + extension SegmentDestination: iOSLifecycle { + public func applicationWillEnterForeground(application: UIApplication) { + flushTimer?.resume() + } public func applicationDidEnterBackground() { + flushTimer?.suspend() analytics?.beginBackgroundTask() flush() } diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index 8de9b242..873cdb1a 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -27,7 +27,7 @@ public class SegmentDestination: DestinationPlugin { private var apiHost: String? = nil @Atomic private var eventCount: Int = 0 - private var flushTimer: Timer? = nil + internal var flushTimer: QueueTimer? = nil internal enum Constants: String { case integrationName = "Segment.io" @@ -43,9 +43,9 @@ public class SegmentDestination: DestinationPlugin { guard let analytics = self.analytics else { return } storage = analytics.storage httpClient = HTTPClient(analytics: analytics) - flushTimer = Timer.scheduledTimer(withTimeInterval: analytics.configuration.values.flushInterval, repeats: true, block: { _ in + flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) { self.flush() - }) + } } public func update(settings: Settings) { @@ -149,3 +149,4 @@ public class SegmentDestination: DestinationPlugin { } } } + diff --git a/Sources/Segment/Plugins/StartupQueue.swift b/Sources/Segment/Plugins/StartupQueue.swift index 0ade41e6..efb9b107 100644 --- a/Sources/Segment/Plugins/StartupQueue.swift +++ b/Sources/Segment/Plugins/StartupQueue.swift @@ -8,17 +8,17 @@ import Foundation import Sovran -class StartupQueue: Plugin, Subscriber { +internal class StartupQueue: Plugin, Subscriber { static var specificName = "Segment_StartupQueue" static let maxSize = 1000 - @Atomic var started: Bool = false + @Atomic var running: Bool = false let type: PluginType = .before let name: String = specificName var analytics: Analytics? = nil { didSet { - analytics?.store.subscribe(self, handler: systemUpdate) + analytics?.store.subscribe(self, handler: runningUpdate) } } @@ -29,7 +29,7 @@ class StartupQueue: Plugin, Subscriber { } func execute(event: T?) -> T? { - if started == false, let e = event { + if running == false, let e = event { // timeline hasn't started, so queue it up. if queuedEvents.count >= Self.maxSize { // if we've exceeded the max queue size start dropping events @@ -44,11 +44,11 @@ class StartupQueue: Plugin, Subscriber { } extension StartupQueue { - internal func systemUpdate(state: System) { - started = state.started - if started { + internal func runningUpdate(state: System) { + if state.running { replayEvents() } + running = state.running } internal func replayEvents() { diff --git a/Sources/Segment/Settings.swift b/Sources/Segment/Settings.swift index 01e95af3..70582022 100644 --- a/Sources/Segment/Settings.swift +++ b/Sources/Segment/Settings.swift @@ -23,7 +23,7 @@ public struct Settings: Codable { public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - integrations = try? values.decode(JSON.self, forKey: CodingKeys.integrations) + self.integrations = try? values.decode(JSON.self, forKey: CodingKeys.integrations) self.plan = try? values.decode(JSON.self, forKey: CodingKeys.plan) self.edgeFunctions = try? values.decode(JSON.self, forKey: CodingKeys.edgeFunctions) } @@ -52,18 +52,62 @@ public struct Settings: Codable { } +extension Settings: Equatable { + public static func == (lhs: Settings, rhs: Settings) -> Bool { + let l = lhs.prettyPrint() + let r = rhs.prettyPrint() + return l == r + } +} + extension Analytics { - func checkSettings() { + internal func update(settings: Settings) { + apply { (plugin) in + // tell all top level plugins to update. + update(plugin: plugin, settings: settings) + } + } + + internal func update(plugin: Plugin, settings: Settings) { + plugin.update(settings: settings) + // if it's a destination, tell it's plugins to update as well. + if let dest = plugin as? DestinationPlugin { + dest.apply { (subPlugin) in + subPlugin.update(settings: settings) + } + } + } + + internal func checkSettings() { + if isUnitTesting { + // we don't really wanna wait for this network call during tests... + // but we should make it work similarly. + store.dispatch(action: System.ToggleRunningAction(running: false)) + DispatchQueue.main.async { + if let state: System = self.store.currentState(), let settings = state.settings { + self.store.dispatch(action: System.UpdateSettingsAction(settings: settings)) + } + self.store.dispatch(action: System.ToggleRunningAction(running: true)) + } + return + } + let writeKey = self.configuration.values.writeKey let httpClient = HTTPClient(analytics: self, cdnHost: configuration.values.cdnHost) + // stop things; queue in case our settings have changed. + store.dispatch(action: System.ToggleRunningAction(running: false)) httpClient.settingsFor(writeKey: writeKey) { (success, settings) in if success { if let s = settings { // put the new settings in the state store. // this will cause them to be cached. self.store.dispatch(action: System.UpdateSettingsAction(settings: s)) + // let plugins know we just received some settings.. + self.update(settings: s) } } + // we're good to go back to a running state. + self.store.dispatch(action: System.ToggleRunningAction(running: true)) } } } diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index f0e6c068..2293e79d 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -11,6 +11,8 @@ import Sovran extension Analytics: Subscriber { internal func platformStartup() { + add(plugin: StartupQueue(name: StartupQueue.specificName)) + // add segment destination plugin unless // asked not to via configuration. if configuration.values.autoAddSegmentDestination { @@ -27,37 +29,12 @@ extension Analytics: Subscriber { } } - // prepare our subscription for settings updates from segment.com - store.subscribe(self, initialState: true) { (state: System) in - if let settings = state.settings { - self.update(settings: settings) - } - self.store.dispatch(action: System.SetStartedAction(started: true)) - } - // plugins will receive any settings we currently have as they are added. // ... but lets go check if we have new stuff .... // start checking periodically for settings changes from segment.com setupSettingsCheck() } - internal func update(settings: Settings) { - apply { (plugin) in - // tell all top level plugins to update. - update(plugin: plugin, settings: settings) - } - } - - internal func update(plugin: Plugin, settings: Settings) { - plugin.update(settings: settings) - // if it's a destination, tell it's plugins to update as well. - if let dest = plugin as? DestinationPlugin { - dest.apply { (subPlugin) in - subPlugin.update(settings: settings) - } - } - } - internal func platformPlugins() -> [PlatformPlugin.Type]? { var plugins = [PlatformPlugin.Type]() @@ -89,12 +66,18 @@ extension Analytics: Subscriber { } } -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) import UIKit extension Analytics { internal func setupSettingsCheck() { + // do the first one + checkSettings() + // set up return-from-background to do it again. NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { (notification) in - self.checkSettings() + guard let app = notification.object as? UIApplication else { return } + if app.applicationState == .background { + self.checkSettings() + } } } } @@ -102,13 +85,19 @@ extension Analytics { extension Analytics { internal func setupSettingsCheck() { // TBD: we don't know what to do here yet. + checkSettings() } } #elseif os(macOS) import Cocoa extension Analytics { internal func setupSettingsCheck() { - NotificationCenter.default.addObserver(forName: NSApplication.willBecomeActiveNotification, object: nil, queue: OperationQueue.main) { (notification) in + // do the first one + checkSettings() + // now set up a timer to do it every 24 hrs. + // mac apps change focus a lot more than iOS apps, so this + // seems more appropriate here. + QueueTimer.schedule(interval: .days(1), queue: .main) { self.checkSettings() } } @@ -116,7 +105,7 @@ extension Analytics { #elseif os(Linux) extension Analytics { internal func setupSettingsCheck() { - // TBD: we don't know what to do here. + checkSettings() } } #endif diff --git a/Sources/Segment/State.swift b/Sources/Segment/State.swift index 665ffd78..40e39890 100644 --- a/Sources/Segment/State.swift +++ b/Sources/Segment/State.swift @@ -8,14 +8,13 @@ import Foundation import Sovran - // MARK: - System (Overall) struct System: State { let configuration: Configuration let integrations: JSON? let settings: Settings? - let started: Bool + let running: Bool struct UpdateSettingsAction: Action { let settings: Settings @@ -24,7 +23,7 @@ struct System: State { let result = System(configuration: state.configuration, integrations: state.integrations, settings: settings, - started: state.started) + running: state.running) return result } } @@ -42,7 +41,7 @@ struct System: State { let result = System(configuration: state.configuration, integrations: jsonIntegrations, settings: state.settings, - started: state.started) + running: state.running) return result } } @@ -60,7 +59,7 @@ struct System: State { let result = System(configuration: state.configuration, integrations: jsonIntegrations, settings: state.settings, - started: state.started) + running: state.running) return result } } @@ -68,15 +67,14 @@ struct System: State { } } - struct SetStartedAction: Action { - let started: Bool + struct ToggleRunningAction: Action { + let running: Bool func reduce(state: System) -> System { - let result = System(configuration: state.configuration, - integrations: state.integrations, - settings: state.settings, - started: started) - return result + return System(configuration: state.configuration, + integrations: state.integrations, + settings: state.settings, + running: running) } } } @@ -142,7 +140,7 @@ extension System { } } let integrationDictionary = try! JSON([String: Any]()) - return System(configuration: configuration, integrations: integrationDictionary, settings: settings, started: false) + return System(configuration: configuration, integrations: integrationDictionary, settings: settings, running: false) } } diff --git a/Sources/Segment/Utilities/QueueTimer.swift b/Sources/Segment/Utilities/QueueTimer.swift new file mode 100644 index 00000000..18324c71 --- /dev/null +++ b/Sources/Segment/Utilities/QueueTimer.swift @@ -0,0 +1,86 @@ +// +// QueueTimer.swift +// Segment +// +// Created by Brandon Sneed on 6/16/21. +// + +import Foundation + +internal class QueueTimer { + enum State { + case suspended + case resumed + } + + let interval: TimeInterval + let timer: DispatchSourceTimer + let queue: DispatchQueue + let handler: () -> Void + + @Atomic var state: State = .suspended + + static var timers = [QueueTimer]() + + static func schedule(interval: TimeInterval, queue: DispatchQueue = .main, handler: @escaping () -> Void) { + let timer = QueueTimer(interval: interval, queue: queue, handler: handler) + Self.timers.append(timer) + } + + init(interval: TimeInterval, queue: DispatchQueue = .main, handler: @escaping () -> Void) { + self.interval = interval + self.queue = queue + self.handler = handler + + timer = DispatchSource.makeTimerSource(flags: [], queue: queue) + timer.schedule(deadline: .now() + self.interval, repeating: self.interval) + timer.setEventHandler { [weak self] in + self?.handler() + } + resume() + } + + deinit { + timer.setEventHandler { + // do nothing ... + } + // if timer is suspended, we must resume if we're going to cancel. + timer.cancel() + resume() + } + + func suspend() { + if state == .suspended { + return + } + state = .suspended + timer.suspend() + } + + func resume() { + if state == .resumed { + return + } + state = .resumed + timer.resume() + } +} + + +extension TimeInterval { + static func milliseconds(_ value: Int) -> TimeInterval { + return TimeInterval(value / 1000) + } + + static func seconds(_ value: Int) -> TimeInterval { + return TimeInterval(value) + } + + static func hours(_ value: Int) -> TimeInterval { + return TimeInterval(60 * value) + } + + static func days(_ value: Int) -> TimeInterval { + return TimeInterval((60 * value) * 24) + } +} diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index a362f4f9..a251e954 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -62,6 +62,8 @@ final class Analytics_Tests: XCTestCase { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + + waitUntilStarted(analytics: analytics) analytics.setDeviceToken("1234") analytics.track(name: "token check") @@ -78,6 +80,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + let dataToken = UUID().asData() analytics.registeredForRemoteNotifications(deviceToken: dataToken) analytics.track(name: "token check") @@ -94,6 +98,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.track(name: "test track") let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent @@ -106,6 +112,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent @@ -119,6 +127,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent @@ -143,6 +153,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.screen(screenTitle: "screen1", category: "category1") let screen1Event: ScreenEvent? = outputReader.lastEvent as? ScreenEvent @@ -163,6 +175,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.group(groupId: "1234") let group1Event: GroupEvent? = outputReader.lastEvent as? GroupEvent @@ -181,6 +195,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent @@ -203,6 +219,9 @@ final class Analytics_Tests: XCTestCase { func testFlush() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) + + waitUntilStarted(analytics: analytics) + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let currentBatchCount = analytics.storage.eventFiles(includeUnfinished: true).count @@ -220,6 +239,8 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin(name: "outputReader") analytics.add(plugin: outputReader) + waitUntilStarted(analytics: analytics) + analytics.track(name: "whataversion") let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 28600a15..ed6c4aa5 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -6,7 +6,7 @@ // import Foundation -import Segment +@testable import Segment extension UUID{ public func asUInt8Array() -> [UInt8]{ @@ -104,6 +104,19 @@ class OutputReaderPlugin: Plugin { func execute(event: T?) -> T? where T : RawEvent { lastEvent = event + if let t = lastEvent as? TrackEvent { + print("EVENT: \(t.event)") + } return event } } + +func waitUntilStarted(analytics: Analytics?) { + guard let analytics = analytics else { return } + // wait until the startup queue has emptied it's events. + if let startupQueue = analytics.find(pluginName: StartupQueue.specificName) as? StartupQueue { + while startupQueue.running != true { + RunLoop.main.run(until: Date.distantPast) + } + } +}