diff --git a/Sources/Segment/Analytics.swift b/Sources/Segment/Analytics.swift index 4ea2b92b..d8500b46 100644 --- a/Sources/Segment/Analytics.swift +++ b/Sources/Segment/Analytics.swift @@ -28,6 +28,7 @@ public class Analytics { static internal let deadInstance = "DEADINSTANCE" static internal weak var firstInstance: Analytics? = nil + @Atomic static internal var activeWriteKeys = [String]() /** This method isn't a traditional singleton implementation. It's provided here @@ -58,6 +59,12 @@ public class Analytics { /// - Parameters: /// - configuration: The configuration to use public init(configuration: Configuration) { + if Self.isActiveWriteKey(configuration.values.writeKey) { + fatalError("Cannot initialize multiple instances of Analytics with the same write key") + } else { + Self.addActiveWriteKey(configuration.values.writeKey) + } + store = Store() storage = Storage(store: self.store, writeKey: configuration.values.writeKey) timeline = Timeline() @@ -74,6 +81,10 @@ public class Analytics { platformStartup() } + deinit { + Self.removeActiveWriteKey(configuration.values.writeKey) + } + internal func process(incomingEvent: E) { guard enabled == true else { return } let event = incomingEvent.applyRawEventData(store: store) @@ -428,11 +439,26 @@ extension Analytics { Self.firstInstance = self } } - + /// Determines if an instance is dead. internal var isDead: Bool { return configuration.values.writeKey == Self.deadInstance } + + /// Manage active writekeys. It's wrapped in @atomic + internal static func isActiveWriteKey(_ writeKey: String) -> Bool { + Self.activeWriteKeys.contains(writeKey) + } + + internal static func addActiveWriteKey(_ writeKey: String) { + Self.activeWriteKeys.append(writeKey) + } + + internal static func removeActiveWriteKey(_ writeKey: String) { + Self.activeWriteKeys.removeAll { key in + writeKey == key + } + } } // MARK: Operating mode based scheduling diff --git a/Sources/Segment/Configuration.swift b/Sources/Segment/Configuration.swift index e77591cf..aa7db9f7 100644 --- a/Sources/Segment/Configuration.swift +++ b/Sources/Segment/Configuration.swift @@ -47,7 +47,7 @@ public class Configuration { 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) @@ -127,7 +127,7 @@ public extension Configuration { /// let config = Configuration(writeKey: "1234").defaultSettings(defaults) /// ``` /// - /// - Parameter settings: + /// - Parameter settings: /// - Returns: The current Configuration. @discardableResult func defaultSettings(_ settings: Settings?) -> Configuration { diff --git a/Sources/Segment/ObjC/ObjCPlugin.swift b/Sources/Segment/ObjC/ObjCPlugin.swift index 9504b824..1396d670 100644 --- a/Sources/Segment/ObjC/ObjCPlugin.swift +++ b/Sources/Segment/ObjC/ObjCPlugin.swift @@ -30,7 +30,7 @@ public class ObjCSegmentMixpanel: NSObject, ObjCPlugin, ObjCPluginShim { @objc(SEGEventPlugin) public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { public var type: PluginType = .enrichment - public var analytics: Analytics? = nil + public weak var analytics: Analytics? = nil @objc(executeEvent:) public func execute(event: ObjCRawEvent?) -> ObjCRawEvent? { diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 7fc373ad..45460984 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -13,6 +13,9 @@ final class Analytics_Tests: XCTestCase { let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) + + waitUntilStarted(analytics: analytics) + checkIfLeaked(analytics) } func testPluginConfigure() { @@ -28,6 +31,8 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotNil(ziggy.analytics) XCTAssertNotNil(myDestination.analytics) XCTAssertNotNil(goober.analytics) + + waitUntilStarted(analytics: analytics) } func testPluginRemove() { @@ -91,6 +96,7 @@ final class Analytics_Tests: XCTestCase { XCTAssertEqual(ziggy1.receivedInitialUpdate, 1) XCTAssertEqual(ziggy2.receivedInitialUpdate, 1) + checkIfLeaked(analytics) } @@ -160,6 +166,7 @@ final class Analytics_Tests: XCTestCase { XCTAssertTrue(anonId != "") XCTAssertTrue(anonId.count == 36) // it's a UUID y0. + waitUntilStarted(analytics: analytics) } func testContext() { @@ -560,7 +567,7 @@ final class Analytics_Tests: XCTestCase { var timeline: Timeline let type: PluginType let key: String - var analytics: Analytics? + weak var analytics: Analytics? init(key: String) { self.key = key diff --git a/Tests/Segment-Tests/Storage_Tests.swift b/Tests/Segment-Tests/Storage_Tests.swift index a9690782..130edf52 100644 --- a/Tests/Segment-Tests/Storage_Tests.swift +++ b/Tests/Segment-Tests/Storage_Tests.swift @@ -56,7 +56,7 @@ class StorageTests: XCTestCase { let analytics = Analytics(configuration: Configuration(writeKey: "test")) analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.waitUntilStarted() // this will crash if it fails. let j = try! JSON(jsonSettings) analytics.storage.write(.settings, value: j) @@ -70,7 +70,7 @@ class StorageTests: XCTestCase { func testBasicWriting() throws { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - + analytics.waitUntilStarted() analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let userInfo: UserInfo? = analytics.store.currentState() @@ -91,6 +91,8 @@ class StorageTests: XCTestCase { let analytics = Analytics(configuration: Configuration(writeKey: "test")) analytics.storage.hardReset(doYouKnowHowToUseThis: true) + analytics.waitUntilStarted() + var event = IdentifyEvent(userId: "brandon1", traits: try! JSON(with: MyTraits(email: "blah@blah.com"))) analytics.storage.write(.events, value: event) @@ -134,6 +136,8 @@ class StorageTests: XCTestCase { let analytics = Analytics(configuration: Configuration(writeKey: "test")) analytics.storage.hardReset(doYouKnowHowToUseThis: true) + analytics.waitUntilStarted() + var event = IdentifyEvent(userId: "brandon1", traits: try! JSON(with: MyTraits(email: "blah@blah.com"))) analytics.storage.write(.events, value: event) diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 8d9ce2d2..060e0186 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -26,7 +26,7 @@ struct MyTraits: Codable { class GooberPlugin: EventPlugin { let type: PluginType - var analytics: Analytics? + weak var analytics: Analytics? init() { self.type = .enrichment @@ -41,7 +41,7 @@ class GooberPlugin: EventPlugin { class ZiggyPlugin: EventPlugin { let type: PluginType - var analytics: Analytics? + weak var analytics: Analytics? var receivedInitialUpdate: Int = 0 var completion: (() -> Void)? @@ -79,7 +79,7 @@ class MyDestination: DestinationPlugin { var timeline: Timeline let type: PluginType let key: String - var analytics: Analytics? + weak var analytics: Analytics? let trackCompletion: (() -> Bool)? let disabled: Bool @@ -114,7 +114,7 @@ class MyDestination: DestinationPlugin { class OutputReaderPlugin: Plugin { let type: PluginType - var analytics: Analytics? + weak var analytics: Analytics? var events = [RawEvent]() var lastEvent: RawEvent? = nil