* Defer automatic events initialization
Delay automaticEvents.initializeEvents by dispatching to DispatchQueue.main.async to run on the next run loop iteration. This avoids interfering with SwiftUI's accent color setup when Mixpanel.initialize() is called from a SwiftUI App's init (see #522). Uses a weak self capture to avoid retain cycles.
* Drain main run loop before tracking queue
Call waitForAsyncTasks() at the start of waitForTrackingQueue to drain the main run loop so any deferred initialization (e.g. automatic events) is dispatched to the tracking queue before synchronizing. Adds a clarifying comment and prevents races in tests that rely on tracking/ network queue state.
- Fix for copilot review comment on pull/712
* Fix for tests in PR
drains the main run loop only once per test right after init, ensuring initializeEvents() has dispatched before the test proceeds
* Add FlagOptions with loadOnFirstForeground to fix initialize/identify race
When calling Mixpanel.initialize immediately followed by identify, both
can trigger concurrent flag fetches causing inconsistent flag evaluation.
This adds a FlagOptions struct with a loadOnFirstForeground option that
lets customers disable the automatic flag load at init time, call
identify first, then manually trigger loadFlags(). The new flagsOptions
parameter on MixpanelOptions groups flag config together while keeping
full backward compatibility with existing featureFlagsEnabled and
featureFlagsContext parameters.
Usage:
let options = MixpanelOptions(
token: "TOKEN",
flagsOptions: FlagOptions(enabled: true, loadOnFirstForeground: false)
)
let mp = Mixpanel.initialize(options: options)
mp.identify(distinctId: "user123")
mp.flags.loadFlags()
https://claude.ai/code/session_01234twgpsuvC2SP67zKDNru
* Clarify FlagOptions docs: identify() already triggers loadFlags()
identify() calls loadFlags() internally when the distinctId changes,
so an explicit loadFlags() call is only needed when re-identifying
with the same persisted distinctId.
https://claude.ai/code/session_01234twgpsuvC2SP67zKDNru
* deprecate old options
* test update
* Fix CI test failures: replace timer waits with predicate expectations
Timer-based waits (0.2-0.3s) expire before .utility QoS dispatches
complete on resource-constrained CI runners. Replace with
XCTNSPredicateExpectation that polls observable state and fulfills
immediately when ready, with 10s timeout for CI headroom.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* rename fields
* rename to featureFlagOptions
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add deviceIdProvider closure for custom device ID generation
Adds a `deviceIdProvider: (() -> String)?` property to MixpanelOptions
that allows customers to supply their own device ID generation logic.
Key behaviors:
- Provider called on init (if no persisted identity) and after reset()/optOutTracking()
- Return same value = persistent device ID (never resets)
- Return different value = ephemeral device ID (resets each time)
- SDK logs warning when provider would replace existing persisted anonymousId
- Empty string from provider falls back to default UUID/IDFV behavior
This gives customers full control over device ID persistence and reset
semantics without needing separate parameters on reset() and optOutTracking().
Includes:
- 13 unit tests covering all behaviors
- Comprehensive documentation in docs/device-id-provider.md
* Address PR review feedback for deviceIdProvider
- Change log level from warn to error for identity mismatch
- Use defaultDeviceId() in unarchive to avoid calling provider twice
- Add whitespace trimming to provider result
- Update documentation table with "Value Used?" column
- Add testIdentifyWithDeviceIdProvider test case
* Add deviceIdProvider QA helpers to MixpanelDemo
- Add persistent and ephemeral device ID provider examples in AppDelegate
- Enhance Reset button with before/after console logging
- Default to no provider (standard SDK behavior) for production
* Make deviceIdProvider return optional String for graceful error handling
Change type from `(() -> String)?` to `(() -> String?)?` so customers can
return nil to signal "use SDK default" when their device ID source fails.
- Update MixpanelOptions type signature and documentation
- Handle optional return in defaultDeviceId() with nil-coalescing
- Update warning message to mention nil or empty string
- Add failingDeviceIdProvider test option in demo app
- Add testProviderReturningNilFallsBackToDefault test
* Add Original ID Merge compatibility note to deviceIdProvider docs
Document the footgun with persistent device IDs + Original ID Merge
on shared devices, where createAlias() can incorrectly merge
identities of different users.
* Add thread-safety warning to deviceIdProvider documentation
The provider closure is called while holding internal locks, so blocking
operations (Keychain, network, UserDefaults) can cause deadlocks. Update
docs and examples to recommend caching the device ID at app launch and
returning the cached value from the provider.
- Add Thread Safety note to MixpanelOptions.swift doc comment
- Update docs/device-id-provider.md with caching pattern
- Revise Best Practices to emphasize non-blocking providers
- Update MixpanelDemo to demonstrate caching pattern
Since the UserDefaults calls made by saveIdentity cause blocking NSNotifications to be emitted to the main thread, calling saveIdentity under the write lock can cause deadlocks.
Since the saveIdentity call only reads state it’s now moved to only take a read lock instead of being under the write lock.