Release 1.0.16#46
Conversation
🩹 Implement caching SDK events to send when socket connection available and manually triggering. 🩹 Implement caching manually trigger experience in addition to caching track event experience 💡 Update code comments 🔨 Add Gemfile 💄 Support transparent background flow experience
🐛 Fix caching run time events when user not identified 👔 Add more validation and cover new cases for handling sessions and auto sdk events 🐛 Fixed case where sending screen event before offline data when back from background with available network connection
🥅 Fix UI constraints warnings
| if eventName == Constants.Event.batchEventsEvent { | ||
| logger.info("✅ Offline batch events sent successfully") | ||
| if let completion = offlineRestoreCompletion { | ||
| offlineRestoreCompletion = nil | ||
| completion() | ||
| } | ||
| } |
There was a problem hiding this comment.
Offline Restore Stays Processing
When cached offline events are restored while the socket is closing for background/flush, the publish path can skip the batch_events callback that is the only place this completion runs. AnalyticsPublisher leaves isProcessingEvent set, so later analytics calls only enqueue events and the SDK can stop sending until another timeout-driven retry happens.
| func onSocketClosed() { | ||
| tryCatch { | ||
| if socketManager.isShutdownState || socketManager.didErrorOccurred { | ||
| clearAllCachedProperties() | ||
| return | ||
| } | ||
| if let eventToPublish = cachedIdentifyEvent { | ||
| publish(eventToPublish) | ||
| } else { | ||
| clearAllCachedProperties() | ||
| resetProcessingEventStatus() | ||
| // Socket closed from error state, don't reopen, clear events | ||
| if socketManager.didCloseFromError { return } | ||
| // Have events in queue, then close come from switch user, reopen socket | ||
| // and keep the identify event first event to process | ||
| if let event = eventsQueue.dequeue() { | ||
| // Mark user switch state for proper handling of new user's initial screen | ||
| userSessionStateManager.markUserSwitch() | ||
| publish(event, isInternalEvent: true) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
When the socket closes around the same time an event ack arrives, this callback and onSocketEventSent both remove the first queued event. The close path can reinsert a different event at the front as internal, which can reorder analytics events or resend an event the server already accepted.
| guard eventName.isAnalyticsEvent() else { return } | ||
| // Get first event, and remove it from queue, keep it here as we need to remove first one | ||
| let event = eventsQueue.dequeue() |
There was a problem hiding this comment.
This removes the queue head before checking whether it matches the acknowledged eventName. With internal front-insertion and close-time republishing, an ack for one analytics event can consume a different queued identify/screen/track event, leaving that event unsent and advancing session state from the wrong item.
| func handleURL(_ url: URL) { | ||
| tryCatch { | ||
| guard let userpilot = userpilot else { | ||
| logger.error("❌ Cannot open URL - Userpilot instance is nil") | ||
| return | ||
| } | ||
|
|
||
| // If a delegate is provided from the host application, preference is to use it for | ||
| // handling navigation and invoking the completion handler. | ||
| if let delegate = userpilot.navigationDelegate { | ||
| logger.info("🔗 UserpilotNavigationDelegate opening %{private}@", url.absoluteString) | ||
| delegate.navigate(to: url) | ||
| return | ||
| } | ||
|
|
||
| // If no delegate provided, fall back to automatic handling behavior provided by the | ||
| // UIApplication - caveat, the completion callback may execute before the app has | ||
| // fully navigated to the destination. | ||
|
|
||
| // SFSafariViewController only supports HTTP and HTTPS URLs and crashes otherwise, | ||
| // and scheme links crash the universal link opener, so check here to be sure we route safely. | ||
| if url.isWebLink { | ||
| if config.useInAppBrowser { | ||
| openInAppBrowser(url) | ||
| } else { | ||
| openExternalBrowser(url) | ||
| } | ||
| } else { | ||
| openSchemeLink(url) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Push payloads and experience content can pass remote deep_link values into this method, and every non-http(s) scheme falls through to UIApplication.open with no allowlist. A bad campaign payload can open tel:, sms:, App Store, or phishing links from the host app context instead of being blocked by default.
| private func openExternalBrowser(_ url: URL) { | ||
| logger.info("🌍 External browser opening %{private}@", url.absoluteString) | ||
| urlOpener.open(url) | ||
| } | ||
|
|
||
| /// Opens a URL with a custom scheme (e.g., mailto:, tel:, app-specific schemes). | ||
| private func openSchemeLink(_ url: URL) { | ||
| logger.info("🔗 Scheme link opening %{private}@", url.absoluteString) | ||
| urlOpener.open(url) |
There was a problem hiding this comment.
External and custom-scheme links are opened through UIApplication.open with empty options, while the SDK otherwise resolves an active top controller for UI presentation. In a multi-window iPadOS app, a link triggered from one scene can open against the wrong foreground scene or fail to route where the user interacted.
| @@ -0,0 +1,397 @@ | |||
| // | |||
There was a problem hiding this comment.
Pre-Ready Events Stay In Memory
The new analytics path saves offline events only after the monitor reports ready, but events published before that are kept in initialQueue. If the app starts without a usable network path and no ready-offline transition is emitted before termination, those events never reach offline storage and are lost.
🚀 Description
📄 Motivation and Context
🧪 How Has This Been Tested?
📷 Screenshots / Screen Recordings (if appropriate)
📦 Types of Changes
🛠 Migration Notes (optional)
🧹 Cleanup Tasks (optional)
✅ Checklist