From 313f5a2a570caf6d2d055f2fa4e47ce355414256 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Mon, 27 Nov 2023 19:45:13 -0500 Subject: [PATCH 01/19] Everything compiles --- .github/workflows/swift.yml | 22 +- .../other_plugins/NotificationTracking.swift | 5 +- Sources/Segment/Analytics.swift | 91 ++-- Sources/Segment/Configuration.swift | 40 +- Sources/Segment/ObjC/ObjCAnalytics.swift | 30 +- Sources/Segment/ObjC/ObjCConfiguration.swift | 21 +- Sources/Segment/ObjC/ObjCEvents.swift | 116 ++--- Sources/Segment/ObjC/ObjCPlugin.swift | 17 +- .../Segment/Plugins/SegmentDestination.swift | 46 +- Sources/Segment/Startup.swift | 27 +- Sources/Segment/Utilities/HTTPClient.swift | 48 +- .../Segment/Utilities/OutputFileStream.swift | 18 +- Sources/Segment/Utilities/Utils.swift | 16 +- Tests/Segment-Tests/Analytics_Tests.swift | 422 +++++++++--------- Tests/Segment-Tests/MemoryLeak_Tests.swift | 32 +- Tests/Segment-Tests/ObjC_Tests.swift | 80 ++-- Tests/Segment-Tests/StressTests.swift | 28 +- .../Segment-Tests/Support/TestUtilities.swift | 44 +- 18 files changed, 563 insertions(+), 540 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 4b0fe803..2657907a 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -13,7 +13,7 @@ jobs: - uses: styfle/cancel-workflow-action@0.9.1 with: workflow_id: ${{ github.event.workflow.id }} - + build_and_test_spm_mac: needs: cancel_previous runs-on: macos-latest @@ -46,6 +46,23 @@ jobs: - name: Run tests run: swift test --enable-test-discovery + build_and_test_spm_windows: + needs: cancel_previous + runs-on: windows-latest + steps: + - uses: compnerd/gha-setup-swift@main + with: + branch: swift-5.8-release + tag: 5.8-RELEASE + - uses: actions/checkout@v2 + - uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }} + - name: Build + run: swift build + - name: Run tests + run: swift test --enable-test-discovery + build_and_test_ios: needs: cancel_previous runs-on: macos-latest @@ -85,7 +102,7 @@ jobs: with: ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }} - run: xcodebuild -scheme Segment test -sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)' - + build_and_test_examples: needs: cancel_previous runs-on: macos-latest @@ -117,4 +134,3 @@ jobs: run: | cd Examples/apps/SegmentUIKitExample xcodebuild -workspace "SegmentUIKitExample.xcworkspace" -scheme "SegmentUIKitExample" -destination 'platform=macOS,variant=Mac Catalyst' - \ No newline at end of file diff --git a/Examples/other_plugins/NotificationTracking.swift b/Examples/other_plugins/NotificationTracking.swift index 8346b1ec..294adbab 100644 --- a/Examples/other_plugins/NotificationTracking.swift +++ b/Examples/other_plugins/NotificationTracking.swift @@ -33,7 +33,7 @@ // MARK: Common -#if !os(Linux) && !os(macOS) +#if !os(Linux) && !os(macOS) && !os(Windows) import Foundation import Segment @@ -41,7 +41,7 @@ import Segment class NotificationTracking: Plugin { var type: PluginType = .utility weak var analytics: Analytics? - + func trackNotification(_ properties: [String: Codable], fromLaunch launch: Bool) { if launch { analytics?.track(name: "Push Notification Tapped", properties: properties) @@ -95,4 +95,3 @@ extension NotificationTracking: iOSLifecycle { } #endif - diff --git a/Sources/Segment/Analytics.swift b/Sources/Segment/Analytics.swift index 4ea2b92b..85d5e0d2 100644 --- a/Sources/Segment/Analytics.swift +++ b/Sources/Segment/Analytics.swift @@ -20,21 +20,21 @@ public class Analytics { } internal var store: Store internal var storage: Storage - + /// Enabled/disables debug logging to trace your data going through the SDK. public static var debugLogsEnabled = false - + public var timeline: Timeline - + static internal let deadInstance = "DEADINSTANCE" static internal weak var firstInstance: Analytics? = nil - + /** This method isn't a traditional singleton implementation. It's provided here to ease migration from analytics-ios to analytics-swift. Rather than return a singleton, it returns the first instance of Analytics created, OR an instance who's writekey is "DEADINSTANCE". - + In the case of a dead instance, an assert will be thrown when in DEBUG builds to assist developers in knowning that `shared()` is being called too soon. */ @@ -44,16 +44,16 @@ public class Analytics { return a } } - + #if DEBUG if isUnitTesting == false { assert(true == false, "An instance of Analytice does not exist!") } #endif - + return Analytics(configuration: Configuration(writeKey: deadInstance)) } - + /// Initialize this instance of Analytics with a given configuration setup. /// - Parameters: /// - configuration: The configuration to use @@ -61,36 +61,36 @@ public class Analytics { store = Store() storage = Storage(store: self.store, writeKey: configuration.values.writeKey) timeline = Timeline() - + // provide our default state store.provide(state: System.defaultState(configuration: configuration, from: storage)) store.provide(state: UserInfo.defaultState(from: storage)) - + storage.analytics = self - + checkSharedInstance() - + // Get everything running platformStartup() } - + internal func process(incomingEvent: E) { guard enabled == true else { return } let event = incomingEvent.applyRawEventData(store: store) - + _ = timeline.process(incomingEvent: event) - + let flushPolicies = configuration.values.flushPolicies for policy in flushPolicies { policy.updateState(event: event) - + if (policy.shouldFlush() == true) { flush() policy.reset() } } } - + /// Process a raw event through the system. Useful when one needs to queue and replay events at a later time. /// - Parameters: /// - event: An event conforming to RawEvent that will be processed. @@ -129,7 +129,7 @@ extension Analytics { store.dispatch(action: System.ToggleEnabledAction(enabled: value)) } } - + /// Returns the anonymousId currently in use. public var anonymousId: String { if let userInfo: UserInfo = store.currentState() { @@ -137,7 +137,7 @@ extension Analytics { } return "" } - + /// Returns the userId that was specified in the last identify call. public var userId: String? { if let userInfo: UserInfo = store.currentState() { @@ -145,12 +145,12 @@ extension Analytics { } return nil } - + /// Returns the current operating mode this instance was given. public var operatingMode: OperatingMode { return configuration.values.operatingMode } - + /// Adjusts the flush interval post configuration. public var flushInterval: TimeInterval { get { @@ -163,7 +163,7 @@ extension Analytics { } } } - + /// Adjusts the flush-at count post configuration. public var flushAt: Int { get { @@ -176,14 +176,14 @@ extension Analytics { } } } - + /// Returns a list of currently active flush policies. public var flushPolicies: [FlushPolicy] { get { configuration.values.flushPolicies } } - + /// Returns the traits that were specified in the last identify call. public func traits() -> T? { if let userInfo: UserInfo = store.currentState() { @@ -191,7 +191,7 @@ extension Analytics { } return nil } - + /// Returns the traits that were specified in the last identify call, as a dictionary. public func traits() -> [String: Any]? { if let userInfo: UserInfo = store.currentState() { @@ -199,18 +199,18 @@ extension Analytics { } return nil } - + /// Tells this instance of Analytics to flush any queued events up to Segment.com. This command will also /// be sent to each plugin present in the system. A completion handler can be optionally given and will be /// called when flush has completed. public func flush(completion: (() -> Void)? = nil) { // only flush if we're enabled. guard enabled == true else { return } - + let flushGroup = DispatchGroup() // gotta call enter at least once before we ask to be notified. flushGroup.enter() - + apply { plugin in // we want to enter as soon as possible. waiting to do it from // another queue just takes too long. @@ -228,9 +228,9 @@ extension Analytics { } } } - + flushGroup.leave() // matches our initial enter(). - + // if we ARE in sync mode, we need to wait on the group. // This effectively ends up being a `sync` operation. if operatingMode == .synchronous { @@ -257,7 +257,7 @@ extension Analytics { } } } - + /// Resets this instance of Analytics to a clean slate. Traits, UserID's, anonymousId, etc are all cleared or reset. This /// command will also be sent to each plugin present in the system. public func reset() { @@ -268,13 +268,13 @@ extension Analytics { } } } - + /// Retrieve the version of this library in use. /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. public func version() -> String { return Analytics.version() } - + /// Retrieve the version of this library in use. /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. public static func version() -> String { @@ -292,7 +292,7 @@ extension Analytics { } return settings } - + /// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all. /// This will allow the destination to be processed in the same way within this library. /// - Parameters: @@ -319,15 +319,15 @@ extension Analytics { return true } } - + return false } - + /// Provides a list of finished, but unsent events. public var pendingUploads: [URL]? { return storage.read(Storage.Constants.events) } - + /// Purge all pending event upload files. public func purgeStorage() { if let files = pendingUploads { @@ -336,12 +336,12 @@ extension Analytics { } } } - + /// Purge a single event upload file. public func purgeStorage(fileURL: URL) { try? FileManager.default.removeItem(at: fileURL) } - + /// Wait until the Analytics object has completed startup. /// This method is primarily useful for command line utilities where /// it's desirable to wait until the system is up and running @@ -361,7 +361,7 @@ extension Analytics { Call openURL as needed or when instructed to by either UIApplicationDelegate or UISceneDelegate. This is necessary to track URL referrers across events. This method will also iterate any plugins that are watching for openURL events. - + Example: ``` func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { @@ -376,12 +376,12 @@ extension Analytics { guard let dict = jsonProperties.dictionaryValue else { return } openURL(url, options: dict) } - + /** Call openURL as needed or when instructed to by either UIApplicationDelegate or UISceneDelegate. This is necessary to track URL referrers across events. This method will also iterate any plugins that are watching for openURL events. - + Example: ``` func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { @@ -392,14 +392,14 @@ extension Analytics { */ public func openURL(_ url: URL, options: [String: Any] = [:]) { store.dispatch(action: UserInfo.SetReferrerAction(url: url)) - + // let any conforming plugins know apply { plugin in if let p = plugin as? OpeningURLs { p.openURL(url, options: options) } } - + var jsonProperties: JSON? = nil if let json = try? JSON(options) { jsonProperties = json @@ -428,7 +428,7 @@ extension Analytics { Self.firstInstance = self } } - + /// Determines if an instance is dead. internal var isDead: Bool { return configuration.values.writeKey == Self.deadInstance @@ -453,4 +453,3 @@ extension OperatingMode { } } } - diff --git a/Sources/Segment/Configuration.swift b/Sources/Segment/Configuration.swift index e77591cf..6333a550 100644 --- a/Sources/Segment/Configuration.swift +++ b/Sources/Segment/Configuration.swift @@ -7,7 +7,7 @@ import Foundation import JSONSafeEncoder -#if os(Linux) +#if os(Linux) || os(Windows) import FoundationNetworking #endif @@ -18,7 +18,7 @@ public enum OperatingMode { case synchronous /// The operation of the Analytics client are asynchronous. case asynchronous - + static internal let defaultQueue = DispatchQueue(label: "com.segment.operatingModeQueue", qos: .utility) } @@ -43,11 +43,11 @@ public class Configuration { var userAgent: String? = nil var jsonNonConformingNumberStrategy: JSONSafeEncoder.NonConformingFloatEncodingStrategy = .zero } - + internal var values: Values /// Initialize a configuration object to pass along to an Analytics instance. - /// + /// /// - Parameter writeKey: Your Segment write key value public init(writeKey: String) { self.values = Values(writeKey: writeKey) @@ -57,7 +57,7 @@ public class Configuration { settings.integrations = try? JSON([ "Segment.io": true ]) - + self.defaultSettings(settings) } } @@ -66,7 +66,7 @@ public class Configuration { // MARK: - Analytics Configuration public extension Configuration { - + /// Sets a reference to your application. This can be useful in instances /// where referring back to your application is necessary, such as within plugins /// or async code. The default value is `nil`. @@ -78,7 +78,7 @@ public extension Configuration { values.application = value return self } - + /// Opt-in/out of tracking lifecycle events. The default value is `false`. /// /// - Parameter enabled: A bool value @@ -88,7 +88,7 @@ public extension Configuration { values.trackApplicationLifecycleEvents = enabled return self } - + /// Set the number of events necessary to automatically flush. The default /// value is `20`. /// @@ -99,7 +99,7 @@ public extension Configuration { values.flushAt = count return self } - + /// Set a time interval (in seconds) by which to trigger an automatic flush. /// The default value is `30`. /// @@ -110,7 +110,7 @@ public extension Configuration { values.flushInterval = interval return self } - + /// Sets a default set of Settings. Normally these will come from Segment's /// api.segment.com/v1/projects//settings, however in instances such /// as first app launch, it can be useful to have a pre-set batch of settings to @@ -127,14 +127,14 @@ public extension Configuration { /// let config = Configuration(writeKey: "1234").defaultSettings(defaults) /// ``` /// - /// - Parameter settings: + /// - Parameter settings: /// - Returns: The current Configuration. @discardableResult func defaultSettings(_ settings: Settings?) -> Configuration { values.defaultSettings = settings return self } - + /// Enable/Disable the automatic adding of Segment as a destination. /// This can be useful in instances such as Consent Management, or in device /// mode only setups. The default value is `true`. @@ -146,7 +146,7 @@ public extension Configuration { values.autoAddSegmentDestination = value return self } - + /// Sets an alternative API host. This is useful when a proxy is in use, or /// events need to be routed to certain locales at all times (such as the EU). /// The default value is `api.segment.io/v1`. @@ -158,7 +158,7 @@ public extension Configuration { values.apiHost = value return self } - + /// Sets an alternative CDN host for settings retrieval. This is useful when /// a proxy is in use, or settings need to be queried from certain locales at /// all times (such as the EU). The default value is `cdn-settings.segment.com/v1`. @@ -170,7 +170,7 @@ public extension Configuration { values.cdnHost = value return self } - + /// Sets a block to be used when generating outgoing HTTP requests. Useful in /// proxying, or adding additional header information for outbound traffic. /// @@ -181,7 +181,7 @@ public extension Configuration { values.requestFactory = value return self } - + /// Sets an error handler to be called when errors are encountered by the Segment /// library. See `AnalyticsError` for a list of possible errors that can be /// encountered. @@ -193,13 +193,13 @@ public extension Configuration { values.errorHandler = value return self } - + @discardableResult func flushPolicies(_ policies: [FlushPolicy]) -> Configuration { values.flushPolicies = policies return self } - + /// Informs the Analytics instance of its operating mode/context. /// Use `.server` when operating in a web service, or when synchronous operation /// is desired. Use `.client` when operating in a long lived process, @@ -209,7 +209,7 @@ public extension Configuration { values.operatingMode = mode return self } - + /// Specify a custom queue to use when performing a flush operation. The default /// value is a Segment owned background queue. @discardableResult @@ -224,7 +224,7 @@ public extension Configuration { values.userAgent = userAgent return self } - + /// This option specifies how NaN/Infinity are handled when encoding JSON. /// The default is .zero. See JSONSafeEncoder.NonConformingFloatEncodingStrategy for more informatino. @discardableResult diff --git a/Sources/Segment/ObjC/ObjCAnalytics.swift b/Sources/Segment/ObjC/ObjCAnalytics.swift index 93f32d4c..1c91b199 100644 --- a/Sources/Segment/ObjC/ObjCAnalytics.swift +++ b/Sources/Segment/ObjC/ObjCAnalytics.swift @@ -1,11 +1,11 @@ // // ObjCAnalytics.swift -// +// // // Created by Cody Garvin on 6/10/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation import JSONSafeEncoder @@ -16,12 +16,12 @@ import JSONSafeEncoder public class ObjCAnalytics: NSObject { /// The underlying Analytics object we're working with public let analytics: Analytics - + @objc public init(configuration: ObjCConfiguration) { self.analytics = Analytics(configuration: configuration.configuration) } - + /// Get a workable ObjC instance by wrapping a Swift instance /// Useful when you want additional flexibility or to share /// a single instance between ObjC<>Swift. @@ -38,7 +38,7 @@ extension ObjCAnalytics { public func track(name: String) { track(name: name, properties: nil) } - + @objc(track:properties:) public func track(name: String, properties: [String: Any]?) { @@ -77,7 +77,7 @@ extension ObjCAnalytics { analytics.process(incomingEvent: event) } } - + /// Track a screen change with a title, category and other properties. /// - Parameters: /// - title: The title of the screen being tracked. @@ -85,7 +85,7 @@ extension ObjCAnalytics { public func screen(title: String) { screen(title: title, category: nil, properties: nil) } - + /// Track a screen change with a title, category and other properties. /// - Parameters: /// - title: The title of the screen being tracked. @@ -120,7 +120,7 @@ extension ObjCAnalytics { public func group(groupId: String, traits: [String: Any]?) { analytics.group(groupId: groupId, traits: traits) } - + @objc(alias:) /// The alias method is used to merge two user identities, effectively connecting two sets of user data /// as one. This is an advanced method, but it is required to manage user identities successfully in some of our destinations. @@ -139,27 +139,27 @@ extension ObjCAnalytics { public var anonymousId: String { return analytics.anonymousId } - + @objc public var userId: String? { return analytics.userId } - + @objc public func traits() -> [String: Any]? { return analytics.traits() } - + @objc public func flush() { analytics.flush() } - + @objc public func reset() { analytics.reset() } - + @objc public func settings() -> [String: Any]? { var result: [String: Any]? = nil @@ -177,12 +177,12 @@ extension ObjCAnalytics { } return result } - + @objc public func openURL(_ url: URL, options: [String: Any] = [:]) { analytics.openURL(url, options: options) } - + @objc public func version() -> String { return analytics.version() diff --git a/Sources/Segment/ObjC/ObjCConfiguration.swift b/Sources/Segment/ObjC/ObjCConfiguration.swift index 261e9da6..ccf352ba 100644 --- a/Sources/Segment/ObjC/ObjCConfiguration.swift +++ b/Sources/Segment/ObjC/ObjCConfiguration.swift @@ -1,11 +1,11 @@ // // ObjCConfiguration.swift -// +// // // Created by Brandon Sneed on 8/13/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation import JSONSafeEncoder @@ -13,7 +13,7 @@ import JSONSafeEncoder @objc(SEGConfiguration) public class ObjCConfiguration: NSObject { internal var configuration: Configuration - + /// Sets a reference to your application. This can be useful in instances /// where referring back to your application is necessary, such as within plugins /// or async code. The default value is `nil`. @@ -26,7 +26,7 @@ public class ObjCConfiguration: NSObject { configuration.application(value) } } - + /// Opt-in/out of tracking lifecycle events. The default value is `false`. @objc public var trackApplicationLifecycleEvents: Bool { @@ -37,7 +37,7 @@ public class ObjCConfiguration: NSObject { configuration.trackApplicationLifecycleEvents(value) } } - + /// Set the number of events necessary to automatically flush. The default /// value is `20`. @objc @@ -49,7 +49,7 @@ public class ObjCConfiguration: NSObject { configuration.flushAt(value) } } - + /// Set a time interval (in seconds) by which to trigger an automatic flush. /// The default value is `30`. @objc @@ -61,7 +61,7 @@ public class ObjCConfiguration: NSObject { configuration.flushInterval(value) } } - + /// Sets a default set of Settings. Normally these will come from Segment's /// api.segment.com/v1/projects//settings, however in instances such /// as first app launch, it can be useful to have a pre-set batch of settings to @@ -98,7 +98,7 @@ public class ObjCConfiguration: NSObject { } } } - + /// Enable/Disable the automatic adding of Segment as a destination. /// This can be useful in instances such as Consent Management, or in device /// mode only setups. The default value is `true`. @@ -111,7 +111,7 @@ public class ObjCConfiguration: NSObject { configuration.autoAddSegmentDestination(value) } } - + /// Sets an alternative API host. This is useful when a proxy is in use, or /// events need to be routed to certain locales at all times (such as the EU). /// The default value is `api.segment.io/v1`. @@ -137,7 +137,7 @@ public class ObjCConfiguration: NSObject { configuration.cdnHost(value) } } - + /// Sets a block to be used when generating outgoing HTTP requests. Useful in /// proxying, or adding additional header information for outbound traffic. /// @@ -163,4 +163,3 @@ public class ObjCConfiguration: NSObject { } #endif - diff --git a/Sources/Segment/ObjC/ObjCEvents.swift b/Sources/Segment/ObjC/ObjCEvents.swift index 019a7b6a..ca5d48f5 100644 --- a/Sources/Segment/ObjC/ObjCEvents.swift +++ b/Sources/Segment/ObjC/ObjCEvents.swift @@ -1,11 +1,11 @@ // // ObjCEvents.swift -// +// // // Created by Brandon Sneed on 4/17/23. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation @@ -17,27 +17,27 @@ internal protocol ObjCEvent { @objc(SEGDestinationMetadata) public class ObjCDestinationMetadata: NSObject { internal var _metadata: DestinationMetadata - + public var bundled: [String] { get { return _metadata.bundled } set(v) { _metadata.bundled = v } } - + public var unbundled: [String] { get { return _metadata.unbundled } set(v) { _metadata.unbundled = v } } - + public var bundledIds: [String] { get { return _metadata.bundledIds } set(v) { _metadata.bundledIds = v } } - + internal init?(_metadata: DestinationMetadata?) { guard let m = _metadata else { return nil } self._metadata = m } - + init(bundled: [String], unbundled: [String], bundledIds: [String]) { _metadata = DestinationMetadata(bundled: bundled, unbundled: unbundled, bundledIds: bundledIds) } @@ -50,7 +50,7 @@ public protocol ObjCRawEvent: NSObjectProtocol { var timestamp: String? { get } var anonymousId: String? { get set } var userId: String? { get set } - + var context: [String: Any]? { get set } var integrations: [String: Any]? { get set } @@ -83,46 +83,46 @@ internal func objcEventFromEvent(_ event: T?) -> ObjCRawEvent? { @objc(SEGTrackEvent) public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: TrackEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } - + public var anonymousId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var event: String { get { return _event.event } set(v) { _event.event = v } } - + @objc public var properties: [String: Any]? { get { return _event.properties?.dictionaryValue } @@ -133,7 +133,7 @@ public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(name: String, properties: [String: Any]? = nil) { _event = TrackEvent(event: name, properties: try? JSON(nilOrObject: properties)) } - + internal init(event: EventType) { self._event = event } @@ -142,9 +142,9 @@ public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGIdentifyEvent) public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: IdentifyEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -153,29 +153,29 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var traits: [String: Any]? { get { return _event.traits?.dictionaryValue } @@ -186,7 +186,7 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(userId: String, traits: [String: Any]? = nil) { _event = IdentifyEvent(userId: userId, traits: try? JSON(nilOrObject: traits)) } - + internal init(event: EventType) { self._event = event } @@ -195,9 +195,9 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGScreenEvent) public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: ScreenEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -206,41 +206,41 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var name: String? { get { return _event.name } set(v) { _event.name = v} } - + @objc public var category: String? { get { return _event.category } set(v) { _event.category = v} } - + @objc public var properties: [String: Any]? { get { return _event.properties?.dictionaryValue } @@ -251,7 +251,7 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(name: String, category: String?, properties: [String: Any]? = nil) { _event = ScreenEvent(title: name, category: category, properties: try? JSON(nilOrObject: properties)) } - + internal init(event: EventType) { self._event = event } @@ -260,9 +260,9 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGGroupEvent) public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: GroupEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -271,35 +271,35 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var groupId: String? { get { return _event.groupId } set(v) { _event.groupId = v} } - + @objc public var traits: [String: Any]? { get { return _event.traits?.dictionaryValue } @@ -310,7 +310,7 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(groupId: String?, traits: [String: Any]? = nil) { _event = GroupEvent(groupId: groupId, traits: try? JSON(nilOrObject: traits)) } - + internal init(event: EventType) { self._event = event } @@ -319,9 +319,9 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGAliasEvent) public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: AliasEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -330,29 +330,29 @@ public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var previousId: String? { get { return _event.previousId } @@ -363,7 +363,7 @@ public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(newId: String?) { _event = AliasEvent(newId: newId) } - + internal init(event: EventType) { self._event = event } diff --git a/Sources/Segment/ObjC/ObjCPlugin.swift b/Sources/Segment/ObjC/ObjCPlugin.swift index 9504b824..57b660c2 100644 --- a/Sources/Segment/ObjC/ObjCPlugin.swift +++ b/Sources/Segment/ObjC/ObjCPlugin.swift @@ -1,12 +1,12 @@ // // ObjCPlugin.swift -// +// // // Created by Brandon Sneed on 4/17/23. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation @@ -31,7 +31,7 @@ public class ObjCSegmentMixpanel: NSObject, ObjCPlugin, ObjCPluginShim { public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { public var type: PluginType = .enrichment public var analytics: Analytics? = nil - + @objc(executeEvent:) public func execute(event: ObjCRawEvent?) -> ObjCRawEvent? { #if DEBUG @@ -39,7 +39,7 @@ public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { #endif return event } - + public func execute(event: T?) -> T? where T : RawEvent { let objcEvent = objcEventFromEvent(event) let result = execute(event: objcEvent) @@ -51,12 +51,12 @@ public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { @objc(SEGBlockPlugin) public class ObjCBlockPlugin: ObjCEventPlugin { let block: (ObjCRawEvent?) -> ObjCRawEvent? - + @objc(executeEvent:) public override func execute(event: ObjCRawEvent?) -> ObjCRawEvent? { return block(event) } - + @objc(initWithBlock:) public init(block: @escaping (ObjCRawEvent?) -> ObjCRawEvent?) { self.block = block @@ -73,11 +73,11 @@ extension ObjCAnalytics { analytics.add(plugin: p) } } - + @objc(addPlugin:destinationKey:) public func add(plugin: ObjCPlugin?, destinationKey: String) { guard let d = analytics.find(key: destinationKey) else { return } - + if let p = plugin as? ObjCPluginShim { _ = d.add(plugin: p.instance()) } else if let p = plugin as? ObjCEventPlugin { @@ -87,4 +87,3 @@ extension ObjCAnalytics { } #endif - diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index 0cf6fea6..d8757205 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -8,8 +8,8 @@ import Foundation import Sovran -#if os(Linux) -// Whoever is doing swift/linux development over there +#if os(Linux) || os(Windows) +// Whoever is doing swift/linux/Windows development over there // decided that it'd be a good idea to split out a TON // of stuff into another framework that NO OTHER PLATFORM // has; I guess to be special. :man-shrugging: @@ -22,7 +22,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion case apiHost = "apiHost" case apiKey = "apiKey" } - + public let type = PluginType.destination public let key: String = Constants.integrationName.rawValue public let timeline = Timeline() @@ -39,23 +39,23 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion typealias CleanupClosure = () -> Void var cleanup: CleanupClosure? = nil } - + internal var httpClient: HTTPClient? private var uploads = [UploadTaskInfo]() private let uploadsQueue = DispatchQueue(label: "uploadsQueue.segment.com") private var storage: Storage? - + @Atomic internal var eventCount: Int = 0 - + internal func initialSetup() { guard let analytics = self.analytics else { return } storage = analytics.storage httpClient = HTTPClient(analytics: analytics) - + // Add DestinationMetadata enrichment plugin add(plugin: DestinationMetadataPlugin()) } - + public func update(settings: Settings, type: UpdateType) { guard let analytics = analytics else { return } let segmentInfo = settings.integrationSettings(forKey: self.key) @@ -86,7 +86,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion } } } - + // MARK: - Event Handling Methods public func execute(event: T?) -> T? { guard let event = event else { return nil } @@ -96,14 +96,14 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion } return result } - + // MARK: - Abstracted Lifecycle Methods internal func enterForeground() { } - + internal func enterBackground() { flush() } - + // MARK: - Event Parsing Methods private func queueEvent(event: T) { guard let storage = self.storage else { return } @@ -111,30 +111,30 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion storage.write(.events, value: event) eventCount += 1 } - + public func flush() { // unused .. see flush(group:completion:) } - + public func flush(group: DispatchGroup, completion: @escaping (DestinationPlugin) -> Void) { guard let storage = self.storage else { return } guard let analytics = self.analytics else { return } guard let httpClient = self.httpClient else { return } - + // don't flush if analytics is disabled. guard analytics.enabled == true else { return } // enter for the high level flush, allow us time to run through any existing files.. group.enter() - + // Read events from file system guard let data = storage.read(Storage.Constants.events) else { group.leave(); return } - + eventCount = 0 cleanupUploads() - + analytics.log(message: "Uploads in-progress: \(pendingUploads)") - + if pendingUploads == 0 { for url in data { // enter for this url we're going to kick off @@ -149,7 +149,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion default: break } - + analytics.log(message: "Processed: \(url.lastPathComponent)") // the upload we have here has just finished. // make sure it gets removed and it's cleanup() called rather @@ -168,7 +168,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion } else { analytics.log(message: "Skipping processing; Uploads in progress.") } - + // leave for the high level flush group.leave() } @@ -196,7 +196,7 @@ extension SegmentDestination { analytics?.log(message: "Cleaned up \(before - after) non-running uploads.") } } - + internal var pendingUploads: Int { var uploadsCount = 0 uploadsQueue.sync { @@ -204,7 +204,7 @@ extension SegmentDestination { } return uploadsCount } - + internal func add(uploadTask: UploadTaskInfo) { uploadsQueue.sync { uploads.append(uploadTask) diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index b6cfa3bc..67eb767e 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -9,10 +9,10 @@ import Foundation import Sovran extension Analytics: Subscriber { - + internal func platformStartup() { add(plugin: StartupQueue()) - + // add segment destination plugin unless // asked not to via configuration. if configuration.values.autoAddSegmentDestination { @@ -20,31 +20,31 @@ extension Analytics: Subscriber { segmentDestination.analytics = self add(plugin: segmentDestination) } - + // Setup platform specific plugins if let platformPlugins = platformPlugins() { for plugin in platformPlugins { add(plugin: plugin) } } - + for policy in configuration.values.flushPolicies { policy.configure(analytics: self) } - + // 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 platformPlugins() -> [PlatformPlugin]? { var plugins = [PlatformPlugin]() - + // add context plugin as well as it's platform specific internally. // this must come first. plugins.append(Context()) - + plugins += VendorSystem.current.requiredPlugins // setup lifecycle if desired @@ -62,8 +62,11 @@ extension Analytics: Subscriber { // placeholder - not sure what this is yet //plugins.append(LinuxLifecycleMonitor()) #endif + #if os(Windows) + // placeholder - not sure what this is yet + #endif } - + if plugins.isEmpty { return nil } else { @@ -114,4 +117,10 @@ extension Analytics { checkSettings() } } +#elseif os(Windows) +extension Analytics { + internal func setupSettingsCheck() { + checkSettings() + } +} #endif diff --git a/Sources/Segment/Utilities/HTTPClient.swift b/Sources/Segment/Utilities/HTTPClient.swift index f3d9a509..b4242192 100644 --- a/Sources/Segment/Utilities/HTTPClient.swift +++ b/Sources/Segment/Utilities/HTTPClient.swift @@ -6,7 +6,7 @@ // import Foundation -#if os(Linux) +#if os(Linux) || os(Windows) import FoundationNetworking #endif @@ -20,31 +20,31 @@ enum HTTPClientErrors: Error { public class HTTPClient { private static let defaultAPIHost = "api.segment.io/v1" private static let defaultCDNHost = "cdn-settings.segment.com/v1" - + internal var session: URLSession private var apiHost: String private var apiKey: String private var cdnHost: String - + private weak var analytics: Analytics? - + init(analytics: Analytics) { self.analytics = analytics - + self.apiKey = analytics.configuration.values.writeKey self.apiHost = analytics.configuration.values.apiHost self.cdnHost = analytics.configuration.values.cdnHost - + self.session = Self.configuredSession(for: self.apiKey) } - + func segmentURL(for host: String, path: String) -> URL? { let s = "https://\(host)\(path)" let result = URL(string: s) return result } - - + + /// Starts an upload of events. Responds appropriately if successful or not. If not, lets the respondant /// know if the task should be retried or not based on the response. /// - Parameters: @@ -58,7 +58,7 @@ public class HTTPClient { completion(.failure(HTTPClientErrors.failedToOpenBatch)) return nil } - + let urlRequest = configuredRequest(for: uploadURL, method: "POST") let dataTask = session.uploadTask(with: urlRequest, fromFile: batch) { [weak self] (data, response, error) in @@ -83,19 +83,19 @@ public class HTTPClient { } } } - + dataTask.resume() return dataTask } - + func settingsFor(writeKey: String, completion: @escaping (Bool, Settings?) -> Void) { guard let settingsURL = segmentURL(for: cdnHost, path: "/projects/\(writeKey)/settings") else { completion(false, nil) return } - + let urlRequest = configuredRequest(for: settingsURL, method: "GET") - + let dataTask = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in if let error = error { self?.analytics?.reportInternalError(AnalyticsError.networkUnknown(error)) @@ -116,7 +116,7 @@ public class HTTPClient { completion(false, nil) return } - + do { let responseJSON = try JSONDecoder.default.decode(Settings.self, from: data) completion(true, responseJSON) @@ -125,12 +125,12 @@ public class HTTPClient { completion(false, nil) return } - + } - + dataTask.resume() } - + deinit { // finish any tasks that may be processing session.finishTasksAndInvalidate() @@ -147,29 +147,29 @@ extension HTTPClient { } return returnHeader } - + internal static func getDefaultAPIHost() -> String { return Self.defaultAPIHost } - + internal static func getDefaultCDNHost() -> String { return Self.defaultCDNHost } - + internal func configuredRequest(for url: URL, method: String) -> URLRequest { var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60) request.httpMethod = method request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("analytics-ios/\(Analytics.version())", forHTTPHeaderField: "User-Agent") request.addValue("gzip", forHTTPHeaderField: "Accept-Encoding") - + if let requestFactory = analytics?.configuration.values.requestFactory { request = requestFactory(request) } - + return request } - + internal static func configuredSession(for writeKey: String) -> URLSession { let configuration = URLSessionConfiguration.ephemeral configuration.httpMaximumConnectionsPerHost = 2 diff --git a/Sources/Segment/Utilities/OutputFileStream.swift b/Sources/Segment/Utilities/OutputFileStream.swift index 4d631d5d..5825c995 100644 --- a/Sources/Segment/Utilities/OutputFileStream.swift +++ b/Sources/Segment/Utilities/OutputFileStream.swift @@ -1,6 +1,6 @@ // // OutputFileStream.swift -// +// // // Created by Brandon Sneed on 10/15/22. // @@ -11,6 +11,8 @@ import Foundation #if os(Linux) import Glibc +#elseif os(Windows) +import WinSDK #else import Darwin.C #endif @@ -23,16 +25,16 @@ internal class OutputFileStream { case unableToCreate(String) case unableToClose(String) } - + var fileHandle: FileHandle? = nil let fileURL: URL - + init(fileURL: URL) throws { self.fileURL = fileURL let path = fileURL.path guard path.isEmpty == false else { throw OutputStreamError.invalidPath(path) } } - + /// Create attempts to create + open func create() throws { let path = fileURL.path @@ -47,7 +49,7 @@ internal class OutputFileStream { } } } - + /// Open simply opens the file, no attempt at creation is made. func open() throws { if fileHandle != nil { return } @@ -65,7 +67,7 @@ internal class OutputFileStream { throw OutputStreamError.unableToOpen(fileURL.path) } } - + func write(_ data: Data) throws { guard data.isEmpty == false else { return } if #available(macOS 10.15.4, iOS 13.4, macCatalyst 13.4, tvOS 13.4, watchOS 13.4, *) { @@ -79,14 +81,14 @@ internal class OutputFileStream { fileHandle?.write(data) } } - + func write(_ string: String) throws { guard string.isEmpty == false else { return } if let data = string.data(using: .utf8) { try write(data) } } - + func close() throws { do { let existing = fileHandle diff --git a/Sources/Segment/Utilities/Utils.swift b/Sources/Segment/Utilities/Utils.swift index db7612ea..6dc30212 100644 --- a/Sources/Segment/Utilities/Utils.swift +++ b/Sources/Segment/Utilities/Utils.swift @@ -7,7 +7,7 @@ import Foundation -#if os(Linux) +#if os(Linux) || os(Windows) extension DispatchQueue { func asyncAndWait(execute workItem: DispatchWorkItem) { async { @@ -73,33 +73,33 @@ extension Optional: Flattenable { #if DEBUG class TrackingDispatchGroup: CustomStringConvertible { internal let group = DispatchGroup() - + var description: String { return "DispatchGroup Enters: \(enters), Leaves: \(leaves)" } - + var enters: Int = 0 var leaves: Int = 0 var current: Int = 0 - + func enter() { enters += 1 current += 1 group.enter() } - + func leave() { leaves += 1 current -= 1 group.leave() } - + init() { } - + func wait() { group.wait() } - + public func notify(qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], queue: DispatchQueue, execute work: @escaping @convention(block) () -> Void) { group.notify(qos: qos, flags: flags, queue: queue, execute: work) } diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 7fc373ad..40404410 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -2,39 +2,39 @@ import XCTest @testable import Segment final class Analytics_Tests: XCTestCase { - + func testBaseEventCreation() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) - + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: myDestination) - + let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) } - + func testPluginConfigure() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let ziggy = ZiggyPlugin() let myDestination = MyDestination() let goober = GooberPlugin() myDestination.add(plugin: goober) - + analytics.add(plugin: ziggy) analytics.add(plugin: myDestination) - + XCTAssertNotNil(ziggy.analytics) XCTAssertNotNil(myDestination.analytics) XCTAssertNotNil(goober.analytics) } - + func testPluginRemove() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) - + let expectation = XCTestExpectation(description: "Ziggy Expectation") let ziggy = ZiggyPlugin() ziggy.completion = { @@ -42,24 +42,24 @@ final class Analytics_Tests: XCTestCase { } analytics.add(plugin: ziggy) analytics.add(plugin: myDestination) - + let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) analytics.remove(plugin: ziggy) - + wait(for: [expectation], timeout: 1.0) } - + func testDestinationInitialUpdateOnlyOnce() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() return true } - + var settings = Settings(writeKey: "test") if let existing = settings.integrations?.dictionaryValue { var newIntegrations = existing @@ -69,41 +69,41 @@ final class Analytics_Tests: XCTestCase { let configuration = Configuration(writeKey: "test") configuration.defaultSettings(settings) let analytics = Analytics(configuration: configuration) - + let ziggy1 = ZiggyPlugin() analytics.add(plugin: myDestination) analytics.add(plugin: ziggy1) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + let ziggy2 = ZiggyPlugin() analytics.add(plugin: ziggy2) - + let dest = analytics.find(key: myDestination.key) XCTAssertNotNil(dest) XCTAssertTrue(dest is MyDestination) - + wait(for: [expectation], timeout: 1.0) - + XCTAssertEqual(myDestination.receivedInitialUpdate, 1) XCTAssertEqual(ziggy1.receivedInitialUpdate, 1) XCTAssertEqual(ziggy2.receivedInitialUpdate, 1) - + } - + func testDestinationEnabled() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() return true } - + var settings = Settings(writeKey: "test") if let existing = settings.integrations?.dictionaryValue { var newIntegrations = existing @@ -113,60 +113,60 @@ final class Analytics_Tests: XCTestCase { let configuration = Configuration(writeKey: "test") configuration.defaultSettings(settings) let analytics = Analytics(configuration: configuration) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + let dest = analytics.find(key: myDestination.key) XCTAssertNotNil(dest) XCTAssertTrue(dest is MyDestination) - + wait(for: [expectation], timeout: 1.0) } - - // Linux doesn't support XCTExpectFailure -#if !os(Linux) + + // Linux & Windows don't support XCTExpectFailure +#if !os(Linux) && !os(Windows) func testDestinationNotEnabled() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination(disabled: true) { expectation.fulfill() return true } - + let configuration = Configuration(writeKey: "test") let analytics = Analytics(configuration: configuration) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + XCTExpectFailure { wait(for: [expectation], timeout: 1.0) } } #endif - + func testAnonymousId() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let anonId = analytics.anonymousId - + XCTAssertTrue(anonId != "") XCTAssertTrue(anonId.count == 36) // it's a UUID y0. } - + func testContext() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + #if !os(watchOS) && !os(Linux) // prime the pump for userAgent, since it's retrieved async. let vendorSystem = VendorSystem.current @@ -174,14 +174,14 @@ final class Analytics_Tests: XCTestCase { RunLoop.main.run(until: Date.distantPast) } #endif - + waitUntilStarted(analytics: analytics) - + // add a referrer analytics.openURL(URL(string: "https://google.com")!) - + analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue // Verify that context isn't empty here. @@ -193,30 +193,30 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotNil(context?["timezone"], "timezone missing!") XCTAssertNotNil(context?["library"], "library missing!") XCTAssertNotNil(context?["device"], "device missing!") - + let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") - + // this key not present on watchOS (doesn't have webkit) #if !os(watchOS) XCTAssertNotNil(context?["userAgent"], "userAgent missing!") #endif - - // these keys not present on linux -#if !os(Linux) + + // these keys not present on linux or Windows +#if !os(Linux) && !os(Windows) XCTAssertNotNil(context?["app"], "app missing!") XCTAssertNotNil(context?["locale"], "locale missing!") #endif } - - + + func testContextWithUserAgent() { let configuration = Configuration(writeKey: "test") configuration.userAgent("testing user agent") let analytics = Analytics(configuration: configuration) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + #if !os(watchOS) && !os(Linux) // prime the pump for userAgent, since it's retrieved async. let vendorSystem = VendorSystem.current @@ -224,14 +224,14 @@ final class Analytics_Tests: XCTestCase { RunLoop.main.run(until: Date.distantPast) } #endif - + waitUntilStarted(analytics: analytics) - + // add a referrer analytics.openURL(URL(string: "https://google.com")!) - + analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue // Verify that context isn't empty here. @@ -243,170 +243,170 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotNil(context?["timezone"], "timezone missing!") XCTAssertNotNil(context?["library"], "library missing!") XCTAssertNotNil(context?["device"], "device missing!") - + let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") XCTAssertEqual(context?["userAgent"] as! String, "testing user agent") - - // these keys not present on linux -#if !os(Linux) + + // these keys not present on linux or Windows +#if !os(Linux) && !os(Windows) XCTAssertNotNil(context?["app"], "app missing!") XCTAssertNotNil(context?["locale"], "locale missing!") #endif } - + func testDeviceToken() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.setDeviceToken("1234") analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let device = trackEvent?.context?.dictionaryValue let token = device?[keyPath: "device.token"] as? String XCTAssertTrue(token == "1234") } - + #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) func testDeviceTokenData() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + let dataToken = UUID().asData() analytics.registeredForRemoteNotifications(deviceToken: dataToken) analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let device = trackEvent?.context?.dictionaryValue let token = device?[keyPath: "device.token"] as? String XCTAssertTrue(token?.count == 32) // it's a uuid w/o the dashes. 36 becomes 32. } #endif - + func testTrack() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "test track") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent?.event == "test track") XCTAssertTrue(trackEvent?.type == "track") } - + func testIdentify() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") } - + func testUserIdAndTraitsPersistCorrectly() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + analytics.track(name: "test") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent?.userId == "brandon") let trackTraits = trackEvent?.context?.dictionaryValue?["traits"] as? [String: Any] XCTAssertNil(trackTraits) - + let analyticsTraits: MyTraits? = analytics.traits() XCTAssertEqual("blah@blah.com", analyticsTraits?.email) } - - + + func testScreen() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.screen(title: "screen1", category: "category1") - + let screen1Event: ScreenEvent? = outputReader.lastEvent as? ScreenEvent XCTAssertTrue(screen1Event?.name == "screen1") XCTAssertTrue(screen1Event?.category == "category1") - + analytics.screen(title: "screen2", category: "category2", properties: MyTraits(email: "blah@blah.com")) - + let screen2Event: ScreenEvent? = outputReader.lastEvent as? ScreenEvent XCTAssertTrue(screen2Event?.name == "screen2") XCTAssertTrue(screen2Event?.category == "category2") let props = screen2Event?.properties?.dictionaryValue XCTAssertTrue(props?["email"] as? String == "blah@blah.com") } - + func testGroup() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.group(groupId: "1234") - + let group1Event: GroupEvent? = outputReader.lastEvent as? GroupEvent XCTAssertTrue(group1Event?.groupId == "1234") - + analytics.group(groupId: "4567", traits: MyTraits(email: "blah@blah.com")) - + let group2Event: GroupEvent? = outputReader.lastEvent as? GroupEvent XCTAssertTrue(group2Event?.groupId == "4567") let props = group2Event?.traits?.dictionaryValue XCTAssertTrue(props?["email"] as? String == "blah@blah.com") } - + func testReset() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + let currentAnonId = analytics.anonymousId let currentUserInfo: UserInfo? = analytics.store.currentState() - + analytics.reset() - + let newAnonId = analytics.anonymousId let newUserInfo: UserInfo? = analytics.store.currentState() XCTAssertNotEqual(currentAnonId, newAnonId) @@ -414,172 +414,172 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotEqual(currentUserInfo?.userId, newUserInfo?.userId) XCTAssertNotEqual(currentUserInfo?.traits, newUserInfo?.traits) } - + func testFlush() { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_do_not_reuse_this_writekey").flushInterval(9999).flushAt(9999)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let currentBatchCount = analytics.storage.eventFiles(includeUnfinished: true).count - + analytics.flush() analytics.track(name: "test") - + let batches = analytics.storage.eventFiles(includeUnfinished: true) let newBatchCount = batches.count // 1 new temp file XCTAssertTrue(newBatchCount == currentBatchCount + 1, "New Count (\(newBatchCount)) should be \(currentBatchCount) + 1") } - + func testEnabled() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "enabled") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent!.event == "enabled") - + outputReader.lastEvent = nil analytics.enabled = false analytics.track(name: "notEnabled") - + let noEvent = outputReader.lastEvent XCTAssertNil(noEvent) - + analytics.enabled = true analytics.track(name: "enabled") - + let newEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(newEvent!.event == "enabled") } - + func testSetFlushIntervalAfter() { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) let intervalPolicy = IntervalBasedFlushPolicy(interval: 35) analytics.add(flushPolicy: intervalPolicy) - + waitUntilStarted(analytics: analytics) - + XCTAssertTrue(intervalPolicy.flushTimer!.interval == 35) - + analytics.flushInterval = 60 - + RunLoop.main.run(until: Date.distantPast) - + XCTAssertTrue(intervalPolicy.flushTimer!.interval == 60) } - + func testSetFlushAtAfter() { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) let countPolicy = CountBasedFlushPolicy(count: 23) analytics.add(flushPolicy: countPolicy) - + waitUntilStarted(analytics: analytics) - + XCTAssertTrue(analytics.configuration.values.flushAt == 23) - + analytics.flushAt = 1 - + let event = TrackEvent(event: "blah", properties: nil) - + countPolicy.updateState(event: event) - + RunLoop.main.run(until: Date.distantPast) - + XCTAssertTrue(countPolicy.shouldFlush() == true) XCTAssertTrue(analytics.configuration.values.flushAt == 1) } - + func testPurgeStorage() { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_do_not_reuse_this_writekey_either") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let currentPendingCount = analytics.pendingUploads!.count - + XCTAssertEqual(currentPendingCount, 1) - + analytics.flush() analytics.track(name: "test") - + analytics.flush() analytics.track(name: "test") - + analytics.flush() analytics.track(name: "test") - + var newPendingCount = analytics.pendingUploads!.count XCTAssertEqual(newPendingCount, 1) - + let pending = analytics.pendingUploads! analytics.purgeStorage(fileURL: pending.first!) - + newPendingCount = analytics.pendingUploads!.count XCTAssertEqual(newPendingCount, 0) - + analytics.purgeStorage() newPendingCount = analytics.pendingUploads!.count XCTAssertEqual(newPendingCount, 0) } - + func testVersion() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "whataversion") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue let eventVersion = context?[keyPath: "library.version"] as? String let analyticsVersion = analytics.version() - + XCTAssertEqual(eventVersion, analyticsVersion) } - + class AnyDestination: DestinationPlugin { var timeline: Timeline let type: PluginType let key: String var analytics: Analytics? - + init(key: String) { self.key = key self.type = .destination self.timeline = Timeline() } } - + // Test to ensure bundled and unbundled integrations are populated correctly func testDestinationMetadata() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let mixpanel = AnyDestination(key: "Mixpanel") let outputReader = OutputReaderPlugin() - + // we want the output reader on the segment plugin // cuz that's the only place the metadata is getting added. let segmentDest = analytics.find(pluginType: SegmentDestination.self) segmentDest?.add(plugin: outputReader) - + analytics.add(plugin: mixpanel) var settings = Settings(writeKey: "123") let integrations = try? JSON([ @@ -595,30 +595,30 @@ final class Analytics_Tests: XCTestCase { ]) settings.integrations = integrations analytics.store.dispatch(action: System.UpdateSettingsAction(settings: settings)) - + waitUntilStarted(analytics: analytics) - - + + analytics.track(name: "sampleEvent") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let metadata = trackEvent?._metadata - + XCTAssertEqual(metadata?.bundled, ["Mixpanel"]) XCTAssertEqual(metadata?.unbundled.sorted(), ["Amplitude", "Customer.io"]) } - + // Test to ensure bundled and active integrations are populated correctly func testDestinationMetadataUnbundled() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let mixpanel = AnyDestination(key: "Mixpanel") let outputReader = OutputReaderPlugin() - + // we want the output reader on the segment plugin // cuz that's the only place the metadata is getting added. let segmentDest = analytics.find(pluginType: SegmentDestination.self) segmentDest?.add(plugin: outputReader) - + analytics.add(plugin: mixpanel) var settings = Settings(writeKey: "123") let integrations = try? JSON([ @@ -634,19 +634,19 @@ final class Analytics_Tests: XCTestCase { ]) settings.integrations = integrations analytics.store.dispatch(action: System.UpdateSettingsAction(settings: settings)) - + waitUntilStarted(analytics: analytics) - - + + analytics.track(name: "sampleEvent") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let metadata = trackEvent?._metadata - + XCTAssertEqual(metadata?.bundled, ["Mixpanel"]) XCTAssertEqual(metadata?.unbundled.sorted(), ["Amplitude", "Customer.io", "dest1"]) } - + func testRequestFactory() { let config = Configuration(writeKey: "testSequential").requestFactory { request in XCTAssertEqual(request.value(forHTTPHeaderField: "Accept-Encoding"), "gzip") @@ -666,16 +666,16 @@ final class Analytics_Tests: XCTestCase { analytics.storage.hardReset(doYouKnowHowToUseThis: true) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "something") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) } - + func testEnrichment() { var sourceHit: Bool = false let sourceEnrichment: EnrichmentClosure = { event in @@ -683,72 +683,72 @@ final class Analytics_Tests: XCTestCase { sourceHit = true return event } - + var destHit: Bool = true let destEnrichment: EnrichmentClosure = { event in print("destination enrichment applied") destHit = true return event } - + let config = Configuration(writeKey: "testEnrichments") let analytics = Analytics(configuration: config) analytics.storage.hardReset(doYouKnowHowToUseThis: true) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + analytics.add(enrichment: sourceEnrichment) - + let segment = analytics.find(pluginType: SegmentDestination.self) segment?.add(enrichment: destEnrichment) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "something") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) - + XCTAssertTrue(sourceHit) XCTAssertTrue(destHit) } - + func testSharedInstance() { Analytics.firstInstance = nil - + let dead = Analytics.shared() XCTAssertTrue(dead.isDead) - + let alive = Analytics(configuration: Configuration(writeKey: "1234")) XCTAssertFalse(alive.isDead) - + let shared = Analytics.shared() XCTAssertFalse(shared.isDead) - + XCTAssertTrue(alive === shared) - + let alive2 = Analytics(configuration: Configuration(writeKey: "ABCD")) let shared2 = Analytics.shared() XCTAssertFalse(alive2 === shared2) XCTAssertTrue(shared2 === shared) - + } - + func testAsyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_asyncMode") .flushInterval(9999) .flushAt(9999) .operatingMode(.asynchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) @Atomic var completionCalled = false - + // put an event in the pipe ... analytics.track(name: "completion test1") // flush it, that'll get us an upload going @@ -756,28 +756,28 @@ final class Analytics_Tests: XCTestCase { // verify completion is called. completionCalled = true } - + while !completionCalled { RunLoop.main.run(until: Date.distantPast) } - + XCTAssertTrue(completionCalled) XCTAssertEqual(analytics.pendingUploads!.count, 0) } - + func testSyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_syncMode") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) @Atomic var completionCalled = false - + // put an event in the pipe ... analytics.track(name: "completion test1") // flush it, that'll get us an upload going @@ -785,26 +785,26 @@ final class Analytics_Tests: XCTestCase { // verify completion is called. completionCalled = true } - + // completion shouldn't be called before flush returned. XCTAssertTrue(completionCalled) XCTAssertEqual(analytics.pendingUploads!.count, 0) - + // put another event in the pipe. analytics.track(name: "completion test2") analytics.flush() - + // flush shouldn't return until all uploads are done, cuz // it's running in sync mode. XCTAssertEqual(analytics.pendingUploads!.count, 0) } - + func testFindAll() throws { let analytics = Analytics(configuration: Configuration(writeKey: "testFindAll") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: ZiggyPlugin()) @@ -812,14 +812,14 @@ final class Analytics_Tests: XCTestCase { let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) myDestination.add(plugin: GooberPlugin()) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + let ziggysFound = analytics.findAll(pluginType: ZiggyPlugin.self) let goobersFound = myDestination.findAll(pluginType: GooberPlugin.self) - + XCTAssertEqual(ziggysFound!.count, 3) XCTAssertEqual(goobersFound!.count, 2) } diff --git a/Tests/Segment-Tests/MemoryLeak_Tests.swift b/Tests/Segment-Tests/MemoryLeak_Tests.swift index cff3e72e..7a8ba984 100644 --- a/Tests/Segment-Tests/MemoryLeak_Tests.swift +++ b/Tests/Segment-Tests/MemoryLeak_Tests.swift @@ -1,6 +1,6 @@ // // MemoryLeak_Tests.swift -// +// // // Created by Brandon Sneed on 10/17/22. // @@ -20,19 +20,19 @@ final class MemoryLeak_Tests: XCTestCase { func testLeaksVerbose() throws { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) - + waitUntilStarted(analytics: analytics) analytics.track(name: "test") - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) - + let segmentDest = analytics.find(pluginType: SegmentDestination.self)! let destMetadata = segmentDest.timeline.find(pluginType: DestinationMetadataPlugin.self)! let startupQueue = analytics.find(pluginType: StartupQueue.self)! - + let context = analytics.find(pluginType: Context.self)! - - #if !os(Linux) + + #if !os(Linux) && !os(Windows) let deviceToken = analytics.find(pluginType: DeviceToken.self)! #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -45,7 +45,7 @@ final class MemoryLeak_Tests: XCTestCase { let macLifecycle = analytics.find(pluginType: macOSLifecycleEvents.self)! let macMonitor = analytics.find(pluginType: macOSLifecycleMonitor.self)! #endif - + // test that enrichment closure isn't leaked. was previously a retain loop. analytics.add { event in return event @@ -54,9 +54,9 @@ final class MemoryLeak_Tests: XCTestCase { analytics.remove(plugin: startupQueue) analytics.remove(plugin: segmentDest) segmentDest.remove(plugin: destMetadata) - + analytics.remove(plugin: context) - #if !os(Linux) + #if !os(Linux) && !os(Windows) analytics.remove(plugin: deviceToken) #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -75,9 +75,9 @@ final class MemoryLeak_Tests: XCTestCase { checkIfLeaked(segmentDest) checkIfLeaked(destMetadata) checkIfLeaked(startupQueue) - + checkIfLeaked(context) - #if !os(Linux) + #if !os(Linux) && !os(Windows) checkIfLeaked(deviceToken) #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -90,16 +90,16 @@ final class MemoryLeak_Tests: XCTestCase { checkIfLeaked(macLifecycle) checkIfLeaked(macMonitor) #endif - + checkIfLeaked(analytics) } - + func testLeaksSimple() throws { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) - + waitUntilStarted(analytics: analytics) analytics.track(name: "test") - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) checkIfLeaked(analytics) diff --git a/Tests/Segment-Tests/ObjC_Tests.swift b/Tests/Segment-Tests/ObjC_Tests.swift index 55d232c0..8198946c 100644 --- a/Tests/Segment-Tests/ObjC_Tests.swift +++ b/Tests/Segment-Tests/ObjC_Tests.swift @@ -5,7 +5,7 @@ // Created by Brandon Sneed on 8/13/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import XCTest @testable import Segment @@ -24,60 +24,60 @@ class ObjC_Tests: XCTestCase { NOTE: These tests only cover non-trivial methods. Most ObjC methods pass straight through to their swift counterparts however, there are some where some data conversion needs to happen in order to be made accessible. - + */ func testWrapping() { let a = Analytics(configuration: Configuration(writeKey: "WRITE_KEY")) let objc = ObjCAnalytics(wrapping: a) - + XCTAssertTrue(objc.analytics === a) } - + func testNonTrivialConfiguration() { let config = ObjCConfiguration(writeKey: "WRITE_KEY") config.defaultSettings = ["integrations": ["Amplitude": true]] - + let defaults = config.defaultSettings let integrations = defaults["integrations"] as? [String: Any] - + XCTAssertTrue(integrations != nil) XCTAssertTrue(integrations?["Amplitude"] as? Bool == true) } - + func testNonTrivialAnalytics() { Storage.hardSettingsReset(writeKey: "WRITE_KEY") let config = ObjCConfiguration(writeKey: "WRITE_KEY") config.defaultSettings = ["integrations": ["Amplitude": true]] - + let analytics = ObjCAnalytics(configuration: config) analytics.reset() - + analytics.identify(userId: "testPerson", traits: ["email" : "blah@blah.com"]) - + waitUntilStarted(analytics: analytics.analytics) - + let settings = analytics.settings() let integrations = settings?["integrations"] as? [String: Any] - + XCTAssertTrue(integrations != nil) XCTAssertTrue(integrations?["Amplitude"] as? Bool == true) - + let traits = analytics.traits() XCTAssertTrue(traits != nil) XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + let userId = analytics.userId XCTAssertTrue(userId == "testPerson") } - + func testTraitsAndUserIdOptionality() { let config = ObjCConfiguration(writeKey: "WRITE_KEY") let analytics = ObjCAnalytics(configuration: config) analytics.reset() - + analytics.identify(userId: nil, traits: ["email" : "blah@blah.com"]) - + waitUntilStarted(analytics: analytics.analytics) let userId = analytics.userId XCTAssertNil(userId) @@ -85,66 +85,66 @@ class ObjC_Tests: XCTestCase { XCTAssertTrue(traits != nil) XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") } - + func testObjCMiddlewares() { var sourceHit: Bool = false var destHit: Bool = false - + Storage.hardSettingsReset(writeKey: "WRITE_KEY") - + let config = ObjCConfiguration(writeKey: "WRITE_KEY") let analytics = ObjCAnalytics(configuration: config) analytics.analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.reset() - + let outputReader = OutputReaderPlugin() analytics.analytics.add(plugin: outputReader) - + let sourcePlugin = ObjCBlockPlugin { event in print("source enrichment applied") sourceHit = true return event } analytics.add(plugin: sourcePlugin) - + let destPlugin = ObjCBlockPlugin { event in print("destination enrichment applied") destHit = true return event } analytics.add(plugin: destPlugin, destinationKey: "Segment.io") - + waitUntilStarted(analytics: analytics.analytics) - + analytics.identify(userId: "batman") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) - + XCTAssertTrue(sourceHit) XCTAssertTrue(destHit) - + let lastEvent = outputReader.lastEvent XCTAssertTrue(lastEvent is IdentifyEvent) XCTAssertTrue((lastEvent as! IdentifyEvent).userId == "batman") } - + func testObjCDictionaryPassThru() { Storage.hardSettingsReset(writeKey: "WRITE_KEY2") - + let config = ObjCConfiguration(writeKey: "WRITE_KEY2") let analytics = ObjCAnalytics(configuration: config) analytics.analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.reset() - + let outputReader = OutputReaderPlugin() analytics.analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics.analytics) - + let dict = [ "ancientAliens": [ "guy1": "hair guy", @@ -152,7 +152,7 @@ class ObjC_Tests: XCTestCase { "guy3": "old bald guy", "guy4": 4] as [String : Any], "channel": "hIsToRy cHaNnEL"] as [String : Any] - + analytics.track(name: "test", properties: dict) RunLoop.main.run(until: Date.distantPast) let trackEvent = outputReader.lastEvent as? TrackEvent @@ -160,7 +160,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(trackEvent) XCTAssertTrue(props?.count == 2) XCTAssertTrue((props?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.identify(userId: "test", traits: dict) RunLoop.main.run(until: Date.distantPast) let identifyEvent = outputReader.lastEvent as? IdentifyEvent @@ -176,7 +176,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(identifyEvent2) XCTAssertTrue(traits2?.count == 2) XCTAssertTrue((traits2?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.screen(title: "blah", category: nil, properties: dict) RunLoop.main.run(until: Date.distantPast) let screenEvent = outputReader.lastEvent as? ScreenEvent @@ -184,7 +184,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(screenEvent) XCTAssertTrue(props2?.count == 2) XCTAssertTrue((props2?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.group(groupId: "123", traits: dict) RunLoop.main.run(until: Date.distantPast) let groupEvent = outputReader.lastEvent as? GroupEvent diff --git a/Tests/Segment-Tests/StressTests.swift b/Tests/Segment-Tests/StressTests.swift index 651955c3..fd28f985 100644 --- a/Tests/Segment-Tests/StressTests.swift +++ b/Tests/Segment-Tests/StressTests.swift @@ -17,13 +17,13 @@ class StressTests: XCTestCase { override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } - + // Linux doesn't know what URLProtocol is and on watchOS it somehow works differently and isn't hit. - #if !os(Linux) && !os(watchOS) + #if !os(Linux) && !os(watchOS) && !os(Windows) func testStorageStress() throws { // register our network blocker guard URLProtocol.registerClass(BlockNetworkCalls.self) else { XCTFail(); return } - + let analytics = Analytics(configuration: Configuration(writeKey: "stressTest").errorHandler({ error in XCTFail("Storage Error: \(error)") })) @@ -39,7 +39,7 @@ class StressTests: XCTestCase { } waitUntilStarted(analytics: analytics) - + // set the httpclient to use our blocker session let segment = analytics.find(pluginType: SegmentDestination.self) let configuration = URLSessionConfiguration.ephemeral @@ -53,15 +53,15 @@ class StressTests: XCTestCase { "User-Agent": "analytics-ios/\(Analytics.version())"] let blockSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil) segment?.httpClient?.session = blockSession - + let writeQueue1 = DispatchQueue(label: "write queue 1") let writeQueue2 = DispatchQueue(label: "write queue 2") let flushQueue = DispatchQueue(label: "flush queue") - + @Atomic var ready = false @Atomic var queue1Done = false @Atomic var queue2Done = false - + writeQueue1.async { while (ready == false) { usleep(1) } var eventsWritten = 0 @@ -75,7 +75,7 @@ class StressTests: XCTestCase { print("queue 1 wrote \(eventsWritten) events.") queue1Done = true } - + writeQueue2.async { while (ready == false) { usleep(1) } var eventsWritten = 0 @@ -89,7 +89,7 @@ class StressTests: XCTestCase { print("queue 2 wrote \(eventsWritten) events.") queue2Done = true } - + flushQueue.async { while (ready == false) { usleep(1) } var counter = 0 @@ -105,16 +105,16 @@ class StressTests: XCTestCase { print("flushed \(counter) times.") ready = false } - + ready = true - + while (ready) { RunLoop.main.run(until: Date.distantPast) } } #endif - - + + /*func testStressXTimes() throws { for i in 0..<50 { print("Stress test #\(i):") @@ -122,5 +122,5 @@ class StressTests: XCTestCase { print("\n") } }*/ - + } diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 8d9ce2d2..33cf17e0 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -27,11 +27,11 @@ struct MyTraits: Codable { class GooberPlugin: EventPlugin { let type: PluginType var analytics: Analytics? - + init() { self.type = .enrichment } - + func identify(event: IdentifyEvent) -> IdentifyEvent? { var newEvent = IdentifyEvent(existing: event) newEvent.userId = "goober" @@ -43,30 +43,30 @@ class ZiggyPlugin: EventPlugin { let type: PluginType var analytics: Analytics? var receivedInitialUpdate: Int = 0 - + var completion: (() -> Void)? - + required init() { self.type = .enrichment } - + func update(settings: Settings, type: UpdateType) { if type == .initial { receivedInitialUpdate += 1 } } - + func identify(event: IdentifyEvent) -> IdentifyEvent? { var newEvent = IdentifyEvent(existing: event) newEvent.userId = "ziggy" return newEvent //return nil } - + func shutdown() { completion?() } } -#if !os(Linux) +#if !os(Linux) && !os(Windows) @objc(SEGMyDestination) public class ObjCMyDestination: NSObject, ObjCPlugin, ObjCPluginShim { @@ -81,10 +81,10 @@ class MyDestination: DestinationPlugin { let key: String var analytics: Analytics? let trackCompletion: (() -> Bool)? - + let disabled: Bool var receivedInitialUpdate: Int = 0 - + init(disabled: Bool = false, trackCompletion: (() -> Bool)? = nil) { self.key = "MyDestination" self.type = .destination @@ -92,7 +92,7 @@ class MyDestination: DestinationPlugin { self.trackCompletion = trackCompletion self.disabled = disabled } - + func update(settings: Settings, type: UpdateType) { if type == .initial { receivedInitialUpdate += 1 } if disabled == false { @@ -100,7 +100,7 @@ class MyDestination: DestinationPlugin { analytics?.manuallyEnableDestination(plugin: self) } } - + func track(event: TrackEvent) -> TrackEvent? { var returnEvent: TrackEvent? = event if let completion = trackCompletion { @@ -115,14 +115,14 @@ class MyDestination: DestinationPlugin { class OutputReaderPlugin: Plugin { let type: PluginType var analytics: Analytics? - + var events = [RawEvent]() var lastEvent: RawEvent? = nil - + init() { self.type = .after } - + func execute(event: T?) -> T? where T : RawEvent { lastEvent = event if let t = lastEvent as? TrackEvent { @@ -154,28 +154,28 @@ extension XCTestCase { } } -#if !os(Linux) +#if !os(Linux) && !os(Windows) class BlockNetworkCalls: URLProtocol { var initialURL: URL? = nil override class func canInit(with request: URLRequest) -> Bool { - + return true } - + override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - + override var cachedResponse: CachedURLResponse? { return nil } - + override func startLoading() { client?.urlProtocol(self, didReceive: HTTPURLResponse(url: URL(string: "http://api.segment.com")!, statusCode: 200, httpVersion: nil, headerFields: ["blocked": "true"])!, cacheStoragePolicy: .notAllowed) client?.urlProtocolDidFinishLoading(self) } - + override func stopLoading() { - + } } From 526ac3d487db01045f0137b0a6477c30a3e2506b Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Mon, 27 Nov 2023 19:59:12 -0500 Subject: [PATCH 02/19] Remove SSH agent --- .github/workflows/swift.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2657907a..fd82346c 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -55,9 +55,6 @@ jobs: branch: swift-5.8-release tag: 5.8-RELEASE - uses: actions/checkout@v2 - - uses: webfactory/ssh-agent@v0.5.3 - with: - ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }} - name: Build run: swift build - name: Run tests From 41d9f7d369af7b0f6e990657617e4610ba72f482 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Tue, 28 Nov 2023 21:36:34 -0500 Subject: [PATCH 03/19] Add windows vendor and tests --- .editorconfig | 9 +++ .../Platforms/Vendors/WindowsUtils.swift | 75 +++++++++++++++++++ .../WindowsVendorSystem_Tests.swift | 38 ++++++++++ 3 files changed, 122 insertions(+) create mode 100644 .editorconfig create mode 100644 Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift create mode 100644 Tests/Segment-Tests/WindowsVendorSystem_Tests.swift diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1b6a6dfa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.swift] +indent_style = tab +indent_size = 4 diff --git a/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift new file mode 100644 index 00000000..eb9760b8 --- /dev/null +++ b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift @@ -0,0 +1,75 @@ +import Foundation + +#if os(Windows) + +import WinSDK + +internal class WindowsVendorSystem: VendorSystem { + override var manufacturer: String { + return "unknown" + } + + override var type: String { + return "Windows" + } + + override var model: String { + return "unknown" + } + + override var name: String { + return "unknown" + } + + override var identifierForVendor: String? { + return nil + } + + override var systemName: String { + // If the name is larger than 256 characters, we might get an error. + var size: DWORD = 256 + var buffer = [CHAR](repeating: 0, count: Int(size)) + guard GetComputerNameA(&buffer, &size) else { + return "unknown" + } + + return String(cString: buffer) + } + + override var systemVersion: String { + let version = ProcessInfo.processInfo.operatingSystemVersion + return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + } + + override var screenSize: ScreenSize { + var rect: RECT = .init(left: 0, top: 0, right: 0, bottom: 0) + guard SystemParametersInfoA(UInt32(SPI_GETWORKAREA), 0, &rect, 0) else { + return ScreenSize(width: 0, height: 0) + } + + return ScreenSize(width: rect.width, height: rect.height) + } + + override var userAgent: String? { + return "unknown" + } + + override var connection: ConnectionStatus { + return .unknown + } + + override var requiredPlugins: [any PlatformPlugin] { + [] + } +} + +extension RECT { + internal var width: Double { + Double(right - left) + } + + internal var height: Double { + Double(bottom - top) + } +} +#endif diff --git a/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift new file mode 100644 index 00000000..deb2eb58 --- /dev/null +++ b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import Segment + +#if os(Windows) + +final class WindowsVendorSystem_Tests: XCTestCase { + func testScreenSizeReturnsNonEmpty() { + let system = WindowsVendorSystem() + + let screen = system.screenSize + + XCTAssertNotEqual(screen.width, 0) + XCTAssertNotEqual(screen.height, 0) + } + + func testNameReturnsNonEmpty() { + let system = WindowsVendorSystem() + + let name = system.systemName + + XCTAssertNotEqual(name, "unknown") + } + + func testVersionNumberIsWellFormatted() { + let system = WindowsVendorSystem() + + let version = system.systemVersion + + let components = version.split(separator: ".") + + XCTAssertEqual(components.count, 3) + + // Ensure that the version components are all numeric + XCTAssertTrue(components.allSatisfy({ Int($0) != nil })) + } +} + +#endif From d1498a7419bb82214f89e978f6fd74ecb02eafd2 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Tue, 28 Nov 2023 21:40:10 -0500 Subject: [PATCH 04/19] Update windows ci and leave comment --- .github/workflows/swift.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index fd82346c..2462a387 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -46,19 +46,24 @@ jobs: - name: Run tests run: swift test --enable-test-discovery + # Windows support is still very much a work in progress + # We use a different action here to be able to use a more + # up-to-date toolchain. build_and_test_spm_windows: needs: cancel_previous runs-on: windows-latest steps: - - uses: compnerd/gha-setup-swift@main - with: - branch: swift-5.8-release - tag: 5.8-RELEASE - - uses: actions/checkout@v2 - - name: Build - run: swift build - - name: Run tests - run: swift test --enable-test-discovery + - uses: compnerd/gha-setup-swift@main + with: + release-tag-name: "20231116.2" + github-repo: "thebrowsercompany/swift-build" + release-asset-name: installer-amd64.exe + github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v2 + - name: Build + run: swift build + - name: Run tests + run: swift test --enable-test-discovery build_and_test_ios: needs: cancel_previous From 00cc95ebf319f562e221e83155102e2eddce646d Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Tue, 28 Nov 2023 21:45:26 -0500 Subject: [PATCH 05/19] Disable testing and leave comment --- .github/workflows/swift.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2462a387..b26a8c27 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -62,8 +62,9 @@ jobs: - uses: actions/checkout@v2 - name: Build run: swift build - - name: Run tests - run: swift test --enable-test-discovery + # Testing disabled until https://github.com/segmentio/analytics-swift/issues/279 is fixed + # - name: Run tests + # run: swift test --enable-test-discovery build_and_test_ios: needs: cancel_previous From a136212760c2e3635f01a80492a7a93ea7cfe027 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Tue, 28 Nov 2023 21:50:40 -0500 Subject: [PATCH 06/19] Use spaces --- .editorconfig | 2 +- .../Platforms/Vendors/WindowsUtils.swift | 124 +++++++++--------- .../WindowsVendorSystem_Tests.swift | 38 +++--- 3 files changed, 82 insertions(+), 82 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1b6a6dfa..b46a8f0f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,5 +5,5 @@ end_of_line = lf insert_final_newline = true [*.swift] -indent_style = tab +indent_style = space indent_size = 4 diff --git a/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift index eb9760b8..0bcc4fcd 100644 --- a/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift +++ b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift @@ -5,71 +5,71 @@ import Foundation import WinSDK internal class WindowsVendorSystem: VendorSystem { - override var manufacturer: String { - return "unknown" - } - - override var type: String { - return "Windows" - } - - override var model: String { - return "unknown" - } - - override var name: String { - return "unknown" - } - - override var identifierForVendor: String? { - return nil - } - - override var systemName: String { - // If the name is larger than 256 characters, we might get an error. - var size: DWORD = 256 - var buffer = [CHAR](repeating: 0, count: Int(size)) - guard GetComputerNameA(&buffer, &size) else { - return "unknown" - } - - return String(cString: buffer) - } - - override var systemVersion: String { - let version = ProcessInfo.processInfo.operatingSystemVersion - return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" - } - - override var screenSize: ScreenSize { - var rect: RECT = .init(left: 0, top: 0, right: 0, bottom: 0) - guard SystemParametersInfoA(UInt32(SPI_GETWORKAREA), 0, &rect, 0) else { - return ScreenSize(width: 0, height: 0) - } - - return ScreenSize(width: rect.width, height: rect.height) - } - - override var userAgent: String? { - return "unknown" - } - - override var connection: ConnectionStatus { - return .unknown - } - - override var requiredPlugins: [any PlatformPlugin] { - [] - } + override var manufacturer: String { + return "unknown" + } + + override var type: String { + return "Windows" + } + + override var model: String { + return "unknown" + } + + override var name: String { + return "unknown" + } + + override var identifierForVendor: String? { + return nil + } + + override var systemName: String { + // If the name is larger than 256 characters, we might get an error. + var size: DWORD = 256 + var buffer = [CHAR](repeating: 0, count: Int(size)) + guard GetComputerNameA(&buffer, &size) else { + return "unknown" + } + + return String(cString: buffer) + } + + override var systemVersion: String { + let version = ProcessInfo.processInfo.operatingSystemVersion + return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + } + + override var screenSize: ScreenSize { + var rect: RECT = .init(left: 0, top: 0, right: 0, bottom: 0) + guard SystemParametersInfoA(UInt32(SPI_GETWORKAREA), 0, &rect, 0) else { + return ScreenSize(width: 0, height: 0) + } + + return ScreenSize(width: rect.width, height: rect.height) + } + + override var userAgent: String? { + return "unknown" + } + + override var connection: ConnectionStatus { + return .unknown + } + + override var requiredPlugins: [any PlatformPlugin] { + [] + } } extension RECT { - internal var width: Double { - Double(right - left) - } + internal var width: Double { + Double(right - left) + } - internal var height: Double { - Double(bottom - top) - } + internal var height: Double { + Double(bottom - top) + } } #endif diff --git a/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift index deb2eb58..ac79b995 100644 --- a/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift +++ b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift @@ -4,35 +4,35 @@ import XCTest #if os(Windows) final class WindowsVendorSystem_Tests: XCTestCase { - func testScreenSizeReturnsNonEmpty() { - let system = WindowsVendorSystem() + func testScreenSizeReturnsNonEmpty() { + let system = WindowsVendorSystem() - let screen = system.screenSize + let screen = system.screenSize - XCTAssertNotEqual(screen.width, 0) - XCTAssertNotEqual(screen.height, 0) - } + XCTAssertNotEqual(screen.width, 0) + XCTAssertNotEqual(screen.height, 0) + } - func testNameReturnsNonEmpty() { - let system = WindowsVendorSystem() + func testNameReturnsNonEmpty() { + let system = WindowsVendorSystem() - let name = system.systemName + let name = system.systemName - XCTAssertNotEqual(name, "unknown") - } + XCTAssertNotEqual(name, "unknown") + } - func testVersionNumberIsWellFormatted() { - let system = WindowsVendorSystem() + func testVersionNumberIsWellFormatted() { + let system = WindowsVendorSystem() - let version = system.systemVersion + let version = system.systemVersion - let components = version.split(separator: ".") + let components = version.split(separator: ".") - XCTAssertEqual(components.count, 3) + XCTAssertEqual(components.count, 3) - // Ensure that the version components are all numeric - XCTAssertTrue(components.allSatisfy({ Int($0) != nil })) - } + // Ensure that the version components are all numeric + XCTAssertTrue(components.allSatisfy({ Int($0) != nil })) + } } #endif From 70979035779f290aa4c31d666366b370a87ff25b Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Thu, 30 Nov 2023 20:00:21 -0500 Subject: [PATCH 07/19] Update json encoder version --- Package.resolved | 8 ++++---- Package.swift | 2 +- Package@swift-5.9.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 4908cb84..e8051818 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,14 +5,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/segmentio/jsonsafeencoder-swift.git", "state" : { - "revision" : "75ad40f07d4e0b938e3afb80811244d6b7acd4ba", - "version" : "1.0.0" + "revision" : "8b70dc8c01b7b041912e30e29d2b488a43f782ac", + "version" : "1.0.1" } }, { "identity" : "sovran-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/segmentio/Sovran-Swift.git", + "location" : "https://github.com/segmentio/sovran-swift.git", "state" : { "revision" : "64f3b5150c282a34af4578188dce2fd597e600e3", "version" : "1.1.0" @@ -20,4 +20,4 @@ } ], "version" : 2 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index eafeb95d..828a6f84 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0"), - .package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.0") + .package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index a088a180..7a56786c 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -22,7 +22,7 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0"), - .package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.0") + .package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. From 3031680431e5c9486187609d9305844d8edc1b30 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 04:44:00 -0700 Subject: [PATCH 08/19] Make `shared()` public. --- Sources/Segment/Analytics.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Segment/Analytics.swift b/Sources/Segment/Analytics.swift index 031ae6a4..feff5d50 100644 --- a/Sources/Segment/Analytics.swift +++ b/Sources/Segment/Analytics.swift @@ -39,7 +39,7 @@ public class Analytics { In the case of a dead instance, an assert will be thrown when in DEBUG builds to assist developers in knowning that `shared()` is being called too soon. */ - static func shared() -> Analytics { + public static func shared() -> Analytics { if let a = firstInstance { if a.isDead == false { return a From f11b809fecf42dd1408a53ea1044b18bbe5ff57c Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 10:08:06 -0700 Subject: [PATCH 09/19] disable windows test suite temporarily --- .github/workflows/swift.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 46b565ad..2c7cc827 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -50,19 +50,19 @@ jobs: # Windows support is still very much a work in progress # We use a different action here to be able to use a more # up-to-date toolchain. - build_and_test_spm_windows: - needs: cancel_previous - runs-on: windows-latest - steps: - - uses: compnerd/gha-setup-swift@main - with: - release-tag-name: "20231116.2" - github-repo: "thebrowsercompany/swift-build" - release-asset-name: installer-amd64.exe - github-token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v2 - - name: Build - run: swift build + # build_and_test_spm_windows: + # needs: cancel_previous + # runs-on: windows-latest + # steps: + # - uses: compnerd/gha-setup-swift@main + # with: + # release-tag-name: "20231116.2" + # github-repo: "thebrowsercompany/swift-build" + # release-asset-name: installer-amd64.exe + # github-token: ${{ secrets.GITHUB_TOKEN }} + # - uses: actions/checkout@v2 + # - name: Build + # run: swift build # Testing disabled until https://github.com/segmentio/analytics-swift/issues/279 is fixed # - name: Run tests # run: swift test --enable-test-discovery From a9e490e37840ba6dfa025b6300c39cbeb9b78abc Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 10:15:04 -0700 Subject: [PATCH 10/19] Updated tests for windows compat --- Tests/Segment-Tests/Analytics_Tests.swift | 30 ---------------------- Tests/Segment-Tests/HTTPClient_Tests.swift | 2 +- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 33d8a1c2..7793cde7 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -175,16 +175,6 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) -#if !os(watchOS) && !os(Linux) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - // prime the pump for userAgent, since it's retrieved async. - let vendorSystem = VendorSystem.current - while vendorSystem.userAgent == nil { - RunLoop.main.run(until: Date.distantPast) - } - */ -#endif - waitUntilStarted(analytics: analytics) // add a referrer @@ -207,12 +197,6 @@ final class Analytics_Tests: XCTestCase { let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") - // this key not present on watchOS (doesn't have webkit) -#if !os(watchOS) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. */ - //XCTAssertNotNil(context?["userAgent"], "userAgent missing!") -#endif - // these keys not present on linux or Windows #if !os(Linux) && !os(Windows) XCTAssertNotNil(context?["app"], "app missing!") @@ -228,16 +212,6 @@ final class Analytics_Tests: XCTestCase { let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) -#if !os(watchOS) && !os(Linux) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - // prime the pump for userAgent, since it's retrieved async. - let vendorSystem = VendorSystem.current - while vendorSystem.userAgent == nil { - RunLoop.main.run(until: Date.distantPast) - } - */ -#endif - waitUntilStarted(analytics: analytics) // add a referrer @@ -259,10 +233,6 @@ final class Analytics_Tests: XCTestCase { let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") - - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - XCTAssertEqual(context?["userAgent"] as! String, "testing user agent") - */ // these keys not present on linux #if !os(Linux) && !os(Windows) diff --git a/Tests/Segment-Tests/HTTPClient_Tests.swift b/Tests/Segment-Tests/HTTPClient_Tests.swift index f71f42dd..6fe317ba 100644 --- a/Tests/Segment-Tests/HTTPClient_Tests.swift +++ b/Tests/Segment-Tests/HTTPClient_Tests.swift @@ -5,7 +5,7 @@ // Created by Brandon Sneed on 1/21/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import XCTest @testable import Segment From 2d0ad8053b1798aeb635604de714baf266ecc700 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 10:20:37 -0700 Subject: [PATCH 11/19] another test update --- Tests/Segment-Tests/Analytics_Tests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 7793cde7..086baf8b 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -840,7 +840,7 @@ final class Analytics_Tests: XCTestCase { } // Linux doesn't know what URLProtocol is and on watchOS it somehow works differently and isn't hit. - #if !os(Linux) && !os(watchOS) + #if !os(Linux) && !os(watchOS) && !os(Windows) func testFailedSegmentResponse() throws { //register our network blocker (returns 400 response) guard URLProtocol.registerClass(FailedNetworkCalls.self) else { From 89d1ceecb3df79f38f63b2926aa4ef81aa681d74 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 10:31:10 -0700 Subject: [PATCH 12/19] turn win test runner back on --- .github/workflows/swift.yml | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2c7cc827..49308b0d 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -47,25 +47,19 @@ jobs: - name: Run tests run: swift test --enable-test-discovery - # Windows support is still very much a work in progress - # We use a different action here to be able to use a more - # up-to-date toolchain. - # build_and_test_spm_windows: - # needs: cancel_previous - # runs-on: windows-latest - # steps: - # - uses: compnerd/gha-setup-swift@main - # with: - # release-tag-name: "20231116.2" - # github-repo: "thebrowsercompany/swift-build" - # release-asset-name: installer-amd64.exe - # github-token: ${{ secrets.GITHUB_TOKEN }} - # - uses: actions/checkout@v2 - # - name: Build - # run: swift build - # Testing disabled until https://github.com/segmentio/analytics-swift/issues/279 is fixed - # - name: Run tests - # run: swift test --enable-test-discovery + build_and_test_spm_windows: + needs: cancel_previous + runs-on: windows-latest + steps: + - uses: compnerd/gha-setup-swift@main + with: + branch: swift-5.10-release + tag: 5.10-RELEASE + - uses: actions/checkout@v2 + - name: Build + run: swift build + - name: Run tests + run: swift test --enable-test-discovery build_and_test_ios: needs: cancel_previous From e7e342336476c94c072cb1426b8932ce2e5e87da Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 11:08:38 -0700 Subject: [PATCH 13/19] try dynamic lib on windows. --- Package.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Package.swift b/Package.swift index 173bdb9c..87335f6d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,13 @@ import PackageDescription +#if os(Windows) +// refer to this thread: https://forums.swift.org/t/linker-warnings-on-windows-with-swift-argument-parser/71443 +let libraryType: Product.Library.LibraryType = .dynamic +#else +let libraryType: Product.Library.LibraryType = .static +#endif + let package = Package( name: "Segment", platforms: [ @@ -16,6 +23,7 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "Segment", + type: libraryType, targets: ["Segment"]), ], dependencies: [ From 99fc6710cac3533e5239e8cb7b29b4bb9020c30a Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Wed, 15 May 2024 11:23:49 -0700 Subject: [PATCH 14/19] try a different build runner --- .github/workflows/swift.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 49308b0d..ff4c768e 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -51,10 +51,9 @@ jobs: needs: cancel_previous runs-on: windows-latest steps: - - uses: compnerd/gha-setup-swift@main + - uses: SwiftyLab/setup-swift@latest with: - branch: swift-5.10-release - tag: 5.10-RELEASE + swift-version: "5.10" - uses: actions/checkout@v2 - name: Build run: swift build From f34a37438fba0d051814b90814d35445d5cc3d9e Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 07:46:56 -0700 Subject: [PATCH 15/19] try some settings-fu --- Package.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 87335f6d..0df8c451 100644 --- a/Package.swift +++ b/Package.swift @@ -44,6 +44,13 @@ let package = Package( resources: [.process("Resources")]), .testTarget( name: "Segment-Tests", - dependencies: ["Segment"]), + dependencies: ["Segment"], + swiftSettings: [ + .unsafeFlags([ + "-I", "sdk/include", + "-Xcc", "-DDECLSPEC=__declspec(dllimport)", + ], .when(platforms: [.windows])) + ] + ), ] ) From e8c33f47c099dab8bccb9d1e720bf7df1eca8d48 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 07:48:39 -0700 Subject: [PATCH 16/19] and again ... --- Package.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 0df8c451..bb135a63 100644 --- a/Package.swift +++ b/Package.swift @@ -41,16 +41,16 @@ let package = Package( .product(name: "Sovran", package: "sovran-swift"), .product(name: "JSONSafeEncoding", package: "jsonsafeencoding-swift") ], - resources: [.process("Resources")]), - .testTarget( - name: "Segment-Tests", - dependencies: ["Segment"], + resources: [.process("Resources")], swiftSettings: [ .unsafeFlags([ - "-I", "sdk/include", "-Xcc", "-DDECLSPEC=__declspec(dllimport)", ], .when(platforms: [.windows])) ] ), + .testTarget( + name: "Segment-Tests", + dependencies: ["Segment"] + ), ] ) From 4ed04fe67c96b0b91c5f35ccf61eff44bf4e15da Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 07:54:29 -0700 Subject: [PATCH 17/19] disable tests for Windows. --- .github/workflows/swift.yml | 8 ++++++-- Package.swift | 15 +-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index ff4c768e..2239d1fb 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -57,8 +57,12 @@ jobs: - uses: actions/checkout@v2 - name: Build run: swift build - - name: Run tests - run: swift test --enable-test-discovery + # + # Disable tests right now. There's an SPM issue where link errors generate + # a bad exit code even though the tests run/work properly. + # + # - name: Run tests + # run: swift test --enable-test-discovery build_and_test_ios: needs: cancel_previous diff --git a/Package.swift b/Package.swift index bb135a63..70fc4b79 100644 --- a/Package.swift +++ b/Package.swift @@ -3,13 +3,6 @@ import PackageDescription -#if os(Windows) -// refer to this thread: https://forums.swift.org/t/linker-warnings-on-windows-with-swift-argument-parser/71443 -let libraryType: Product.Library.LibraryType = .dynamic -#else -let libraryType: Product.Library.LibraryType = .static -#endif - let package = Package( name: "Segment", platforms: [ @@ -23,7 +16,6 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "Segment", - type: libraryType, targets: ["Segment"]), ], dependencies: [ @@ -41,12 +33,7 @@ let package = Package( .product(name: "Sovran", package: "sovran-swift"), .product(name: "JSONSafeEncoding", package: "jsonsafeencoding-swift") ], - resources: [.process("Resources")], - swiftSettings: [ - .unsafeFlags([ - "-Xcc", "-DDECLSPEC=__declspec(dllimport)", - ], .when(platforms: [.windows])) - ] + resources: [.process("Resources")] ), .testTarget( name: "Segment-Tests", From 69bd82bf9b657ebca70dc231117835dd8fc231c7 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 07:55:23 -0700 Subject: [PATCH 18/19] Added link to issue. --- .github/workflows/swift.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2239d1fb..e3fed886 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -61,6 +61,8 @@ jobs: # Disable tests right now. There's an SPM issue where link errors generate # a bad exit code even though the tests run/work properly. # + # See: https://forums.swift.org/t/linker-warnings-on-windows-with-swift-argument-parser/71443/2 + # # - name: Run tests # run: swift test --enable-test-discovery From 9bb33b46dd41a572b21599403f0c4576ca233c5d Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 08:06:02 -0700 Subject: [PATCH 19/19] Update gitignore. --- .editorconfig | 9 --------- .gitignore | 6 ++++-- 2 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b46a8f0f..00000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true - -[*.swift] -indent_style = space -indent_size = 4 diff --git a/.gitignore b/.gitignore index a2da4812..87e4c200 100644 --- a/.gitignore +++ b/.gitignore @@ -92,11 +92,13 @@ iOSInjectionProject/ Package.resolved *.xcuserdatad /.swiftpm/xcode/xcshareddata - -# XCFramework Segment-Package_XCFramework *.zip Segment.xcframework.zip Segment.xcframework XCFrameworkOutput *.sha256 +.fleet +.idea +.vscode +.editorconfig