From e8af8ae017bb345e40c54bbf19ecc9e6f3d0c3d8 Mon Sep 17 00:00:00 2001 From: Santi G Date: Fri, 24 Oct 2025 00:29:48 +0200 Subject: [PATCH] add deferred automatic event initialization for SwiftUI issue Support enabling automatic events after initialization to avoid UIKit conflicts during app startup. Solves accent color loading issues in SwiftUI apps that initialize Mixpanel early when enabling automaticEvents from init. --- .../MixpanelAutomaticEventsTests.swift | 146 ++++++++++++++++++ Sources/MixpanelInstance.swift | 27 +++- 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift index dc22cfb4..66ae72f4 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift @@ -158,4 +158,150 @@ class MixpanelAutomaticEventsTests: MixpanelBaseTests { removeDBfile(mp.apiToken) removeDBfile(mp2.apiToken) } + + // MARK: - Tests for Deferred Automatic Event Initialization (SwiftUI Early Init Fix) + + func testEnableAutomaticEventsAfterInitializationWithFalse() { + let testToken = randomId() + // Initialize with trackAutomaticEvents: false (simulating SwiftUI early init scenario) + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + + // At this point, no automatic events should be tracked + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, + "No automatic events should be tracked when initialized with trackAutomaticEvents: false") + + // Now enable automatic events (simulating the app UI being ready) + testMixpanel.trackAutomaticEventsEnabled = true + waitForTrackingQueue(testMixpanel) + + // After enabling, first open event should be tracked + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count >= 1, + "First open event should be tracked after enabling automatic events") + let firstOpenEvent = eventQueue(token: testMixpanel.apiToken).first + XCTAssertEqual( + firstOpenEvent?["event"] as? String, "$ae_first_open", + "Should have first open event after re-initialization") + + removeDBfile(testMixpanel.apiToken) + } + + func testEnableAutomaticEventsMethodAfterInitializationWithFalse() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, + "No automatic events should be tracked initially") + + // Use the explicit enableAutomaticEvents() method + testMixpanel.enableAutomaticEvents() + waitForTrackingQueue(testMixpanel) + + // Verify automatic events are now enabled + XCTAssertTrue( + testMixpanel.trackAutomaticEventsEnabled, + "trackAutomaticEventsEnabled should be true after calling enableAutomaticEvents()") + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count >= 1, + "First open event should be tracked after calling enableAutomaticEvents()") + + removeDBfile(testMixpanel.apiToken) + } + + func testSessionTrackingAfterDeferredAutomaticEventsEnable() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + + // Enable automatic events after initial setup + testMixpanel.enableAutomaticEvents() + waitForTrackingQueue(testMixpanel) + + // Clear previous events (first open) + let countBefore = eventQueue(token: testMixpanel.apiToken).count + + // Simulate app resignation + testMixpanel.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) + + // Session event should be tracked + let countAfter = eventQueue(token: testMixpanel.apiToken).count + XCTAssertTrue( + countAfter > countBefore, + "Session event should be tracked after enabling deferred automatic events") + + let sessionEvent = eventQueue(token: testMixpanel.apiToken).last + XCTAssertEqual( + sessionEvent?["event"] as? String, "$ae_session", + "Should track session event after deferred enable") + + removeDBfile(testMixpanel.apiToken) + } + + func testMultipleDeferredEnablesAreIdempotent() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + + // Enable multiple times + testMixpanel.trackAutomaticEventsEnabled = true + waitForTrackingQueue(testMixpanel) + let countAfterFirstEnable = eventQueue(token: testMixpanel.apiToken).count + + testMixpanel.trackAutomaticEventsEnabled = true + waitForTrackingQueue(testMixpanel) + let countAfterSecondEnable = eventQueue(token: testMixpanel.apiToken).count + + testMixpanel.enableAutomaticEvents() + waitForTrackingQueue(testMixpanel) + let countAfterThirdEnable = eventQueue(token: testMixpanel.apiToken).count + + // The first event count should remain the same despite multiple enables + XCTAssertEqual( + countAfterFirstEnable, countAfterSecondEnable, + "Second enable should not duplicate first open event") + XCTAssertEqual( + countAfterSecondEnable, countAfterThirdEnable, + "Third enable should not duplicate first open event") + + removeDBfile(testMixpanel.apiToken) + } + + func testDisablingAndReEnablingAutomaticEvents() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + + waitForTrackingQueue(testMixpanel) + let initialEventCount = eventQueue(token: testMixpanel.apiToken).count + XCTAssertTrue( + initialEventCount > 0, "Should have first open event initially") + + // Disable automatic events + testMixpanel.trackAutomaticEventsEnabled = false + XCTAssertFalse( + testMixpanel.trackAutomaticEventsEnabled, + "Should be disabled after setting to false") + + // Re-enable automatic events + testMixpanel.trackAutomaticEventsEnabled = true + XCTAssertTrue( + testMixpanel.trackAutomaticEventsEnabled, + "Should be re-enabled after setting to true") + + removeDBfile(testMixpanel.apiToken) + } } diff --git a/Sources/MixpanelInstance.swift b/Sources/MixpanelInstance.swift index 2f1b993e..cb823df5 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -114,9 +114,30 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele /// Controls whether to show spinning network activity indicator when flushing /// data to the Mixpanel servers. Defaults to true. open var showNetworkActivityIndicator = true - + private var _trackAutomaticEventsEnabled: Bool /// This allows enabling or disabling collecting common mobile events, - open var trackAutomaticEventsEnabled: Bool + open var trackAutomaticEventsEnabled: Bool { + get { + return _trackAutomaticEventsEnabled + } + set { + _trackAutomaticEventsEnabled = newValue + #if os(iOS) || os(tvOS) || os(visionOS) + if newValue && !MixpanelInstance.isiOSAppExtension() { + // Re-initialize automatic event tracking if being enabled + automaticEvents.delegate = self + automaticEvents.initializeEvents(instanceName: self.name) + } + #endif + } + } + + #if os(iOS) || os(tvOS) || os(visionOS) + /// Enables automatic event tracking for the Mixpanel instance. + public func enableAutomaticEvents() { + self.trackAutomaticEventsEnabled = true + } + #endif /// Flush timer's interval. /// Setting a flush interval of 0 will turn off the flush timer and you need to call the flush() API manually @@ -366,7 +387,7 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele if let apiToken = apiToken, !apiToken.isEmpty { self.apiToken = apiToken } - trackAutomaticEventsEnabled = trackAutomaticEvents + _trackAutomaticEventsEnabled = trackAutomaticEvents if let serverURL = serverURL { self.serverURL = serverURL }