-
Notifications
You must be signed in to change notification settings - Fork 245
Adding deferred Automatic event initialization for SwiftUI issue #682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| } | ||
|
Comment on lines
+282
to
+306
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,9 +114,36 @@ 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 { | ||||||||||||||||||||||||||||||||||
| var value = false | ||||||||||||||||||||||||||||||||||
| readWriteLock.read { | ||||||||||||||||||||||||||||||||||
| value = _trackAutomaticEventsEnabled | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| set { | ||||||||||||||||||||||||||||||||||
| readWriteLock.write { | ||||||||||||||||||||||||||||||||||
| _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) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| automaticEvents.initializeEvents(instanceName: self.name) | |
| automaticEvents.initializeEvents(instanceName: self.name) | |
| } else if !newValue { | |
| // Disable automatic event tracking and remove observers | |
| automaticEvents.stopEvents() | |
| automaticEvents.delegate = nil |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling initializeEvents() multiple times will register duplicate NotificationCenter observers and add multiple SKPaymentQueue observers. The AutomaticEvents.initializeEvents() method (lines 84-96 in AutomaticEvents.swift) calls addObserver without first removing existing observers, which will cause notification handlers to fire multiple times.
Consider adding a guard to prevent re-initialization, or removing observers before re-adding them. For example:
if newValue && !MixpanelInstance.isiOSAppExtension() {
if !automaticEvents.hasAddedObserver {
automaticEvents.delegate = self
automaticEvents.initializeEvents(instanceName: self.name)
}
}| // Re-initialize automatic event tracking if being enabled | |
| automaticEvents.delegate = self | |
| automaticEvents.initializeEvents(instanceName: self.name) | |
| } | |
| if !automaticEvents.hasAddedObserver { | |
| automaticEvents.delegate = self | |
| automaticEvents.initializeEvents(instanceName: self.name) | |
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The re-initialization logic in the setter has a race condition. The readWriteLock.write block only protects the assignment to _trackAutomaticEventsEnabled, but the subsequent calls to automaticEvents.delegate = self and automaticEvents.initializeEvents(instanceName: self.name) happen outside the lock. This means multiple threads could concurrently execute the re-initialization code if they both read the new value as true before the write lock is released.
Consider moving the re-initialization logic inside the write lock:
readWriteLock.write {
_trackAutomaticEventsEnabled = newValue
#if os(iOS) || os(tvOS) || os(visionOS)
if newValue && !MixpanelInstance.isiOSAppExtension() {
automaticEvents.delegate = self
automaticEvents.initializeEvents(instanceName: self.name)
}
#endif
}| } | |
| #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) | |
| if newValue && !MixpanelInstance.isiOSAppExtension() { | |
| // Re-initialize automatic event tracking if being enabled | |
| automaticEvents.delegate = self | |
| automaticEvents.initializeEvents(instanceName: self.name) | |
| } | |
| #endif | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test claims to verify idempotent behavior when enabling automatic events multiple times, but it only checks that the event count stays the same. It doesn't verify that NotificationCenter observers or SKPaymentQueue observers aren't duplicated.
Consider adding assertions to verify that notification handlers don't fire multiple times after multiple enables. For example, trigger a notification and verify the session event is only tracked once, not multiple times.