Skip to content

Conversation

@santigracia
Copy link
Collaborator

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.

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.
Wrapped access to _trackAutomaticEventsEnabled in read/write locks to ensure thread safety when getting or setting the property.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for deferred automatic event initialization to solve SwiftUI early initialization issues. The main purpose is to allow apps to initialize Mixpanel with automatic events disabled and enable them later once the UI is ready, avoiding accent color loading conflicts during app startup.

Key changes:

  • Converted trackAutomaticEventsEnabled from a stored property to a computed property with getter/setter that can trigger re-initialization
  • Added enableAutomaticEvents() convenience method for explicit enabling
  • Implemented comprehensive test coverage for deferred initialization scenarios

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
Sources/MixpanelInstance.swift Converted trackAutomaticEventsEnabled to computed property with re-initialization logic in setter; added enableAutomaticEvents() method
MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift Added 5 new tests covering deferred initialization, idempotency, session tracking, and enable/disable scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +252 to +280
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)
}
Copy link

Copilot AI Nov 26, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +282 to +306
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)
}
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only verifies that the property value changes when disabling/re-enabling automatic events, but doesn't test that automatic events actually stop being tracked when disabled. The implementation doesn't remove notification observers when trackAutomaticEventsEnabled is set to false, so events would continue to be tracked.

Consider adding assertions to verify that after disabling, no session events are tracked when notifications fire. For example:

// After disabling
testMixpanel.automaticEvents.perform(
  #selector(AutomaticEvents.appWillResignActive(_:)),
  with: Notification(name: Notification.Name(rawValue: "test")))
waitForTrackingQueue(testMixpanel)
let eventCountAfterDisable = eventQueue(token: testMixpanel.apiToken).count
XCTAssertEqual(eventCountAfterDisable, initialEventCount, "No new events should be tracked when disabled")

Copilot uses AI. Check for mistakes.
open var showNetworkActivityIndicator = true

private var _trackAutomaticEventsEnabled: Bool
/// This allows enabling or disabling collecting common mobile events,
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 118 is incomplete. It should describe what the property does.

Consider completing the documentation:

/// This allows enabling or disabling collecting common mobile events.
/// When set to true, automatically tracks events like first open, app updated, and session events.
/// Setting this to true after initialization will initialize automatic event tracking.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +137
}
#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
Copy link

Copilot AI Nov 26, 2025

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
}
Suggested change
}
#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
}

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +136
// Re-initialize automatic event tracking if being enabled
automaticEvents.delegate = self
automaticEvents.initializeEvents(instanceName: self.name)
}
Copy link

Copilot AI Nov 26, 2025

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)
  }
}
Suggested change
// 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 uses AI. Check for mistakes.
if newValue && !MixpanelInstance.isiOSAppExtension() {
// Re-initialize automatic event tracking if being enabled
automaticEvents.delegate = self
automaticEvents.initializeEvents(instanceName: self.name)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setter does not handle the case when trackAutomaticEventsEnabled is set to false. There is no logic to stop tracking automatic events, remove notification observers, or remove the SKPaymentQueue observer. This means once automatic events are enabled, they cannot be properly disabled.

Suggested change
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 uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant