Skip to content

Release 1.0.16#46

Open
motasem-userpilot wants to merge 19 commits into
mainfrom
Release-1.0.16
Open

Release 1.0.16#46
motasem-userpilot wants to merge 19 commits into
mainfrom
Release-1.0.16

Conversation

@motasem-userpilot

Copy link
Copy Markdown
Collaborator

🚀 Description

📄 Motivation and Context

🧪 How Has This Been Tested?

  • Unit tests (XCTest)
  • Manual testing on real devices
  • Manual testing on simulators
  • Tested on different iOS versions (please list)
  • Tested with different device types (iPhone, iPad)

📷 Screenshots / Screen Recordings (if appropriate)

📦 Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Refactor (code change that neither fixes a bug nor adds a feature)
  • Optimization (improves performance without changing behavior)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

🛠 Migration Notes (optional)

⚠️ Deprecations (optional)

🧹 Cleanup Tasks (optional)

✅ Checklist

  • My code follows the project's coding standards and guidelines.
  • I have added unit and/or UI tests where applicable.
  • I have tested the changes on multiple iOS versions (e.g., iOS 14, 15, 16, 17).
  • I have tested the changes on multiple device types (e.g., iPhone SE, iPhone 14 Pro, iPad Air).
  • I have updated documentation if needed.
  • My changes are backwards compatible (unless otherwise noted).
  • I have verified that there are no new warnings or errors in Xcode.
  • I have considered performance and memory usage implications.

🩹 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
@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR releases a broad SDK update across analytics, networking, links, and samples. The main changes are:

  • New queued analytics and offline event storage.
  • New remote settings source and network monitor.
  • Updated socket and session state handling.
  • New deep-link and in-app browser support.
  • Updated push notification configuration behavior.
  • Sample app, README, and test updates.

Confidence Score: 4/5

The analytics queue and remote URL-opening paths need fixes before merging.

  • Socket close and ack callbacks can consume different queued analytics events.
  • Offline replay can leave event processing stuck when the socket is closing.
  • Pre-ready network events can remain only in memory and disappear on app termination.
  • Remote-controlled links can open arbitrary schemes without a default-deny policy.

AnalyticsPublisher.swift, OfflineEventsHandler.swift, LinkOpener.swift, NetworkMonitor.swift

Security Review

Remote push payloads and experience content can now open URLs through a permissive link path. Add a default-deny scheme or host allowlist before opening remote-controlled links.

Important Files Changed

Filename Overview
Sources/Userpilot/Analytics/AnalyticsPublisher.swift Reworks analytics delivery around queues, socket acks, offline replay, and session state.
Sources/Userpilot/Analytics/OfflineEventsHandler.swift Adds local offline event save and restore through a batch_events socket event.
Sources/Userpilot/Network/NetworkMonitor.swift Adds network readiness and availability gating for analytics delivery.
Sources/Userpilot/Utilities/Link/LinkOpener.swift Adds URL routing through delegates, in-app Safari, external browser, and custom schemes.
Sources/Userpilot/PushNotification/PushNotificationMonitor.swift Updates push permission refresh and notification deep-link handling.
Sources/Userpilot/Remote/UserpilotRemoteSource.swift Adds URLSession-based SDK settings fetch, caching, and error mapping.
Sources/Userpilot/Userpilot+Config.swift Adds configuration flags for in-app browser usage and push permission requests.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant App
  participant AnalyticsPublisher
  participant NetworkMonitor
  participant OfflineEventsHandler
  participant SocketManager
  App->>AnalyticsPublisher: publish(event)
  AnalyticsPublisher->>NetworkMonitor: read readiness
  alt not ready
    AnalyticsPublisher->>AnalyticsPublisher: enqueue initialQueue
  else offline
    AnalyticsPublisher->>OfflineEventsHandler: save offline event
  else online
    AnalyticsPublisher->>AnalyticsPublisher: enqueue eventsQueue
    AnalyticsPublisher->>SocketManager: publish next event
    SocketManager-->>AnalyticsPublisher: ack
    AnalyticsPublisher->>AnalyticsPublisher: dequeue and continue
  end
  alt cached offline events exist
    AnalyticsPublisher->>OfflineEventsHandler: restore cached batch
    OfflineEventsHandler->>SocketManager: publish batch_events
    SocketManager-->>OfflineEventsHandler: batch ack
    OfflineEventsHandler-->>AnalyticsPublisher: restore completion
  end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant App
  participant AnalyticsPublisher
  participant NetworkMonitor
  participant OfflineEventsHandler
  participant SocketManager
  App->>AnalyticsPublisher: publish(event)
  AnalyticsPublisher->>NetworkMonitor: read readiness
  alt not ready
    AnalyticsPublisher->>AnalyticsPublisher: enqueue initialQueue
  else offline
    AnalyticsPublisher->>OfflineEventsHandler: save offline event
  else online
    AnalyticsPublisher->>AnalyticsPublisher: enqueue eventsQueue
    AnalyticsPublisher->>SocketManager: publish next event
    SocketManager-->>AnalyticsPublisher: ack
    AnalyticsPublisher->>AnalyticsPublisher: dequeue and continue
  end
  alt cached offline events exist
    AnalyticsPublisher->>OfflineEventsHandler: restore cached batch
    OfflineEventsHandler->>SocketManager: publish batch_events
    SocketManager-->>OfflineEventsHandler: batch ack
    OfflineEventsHandler-->>AnalyticsPublisher: restore completion
  end
Loading

Comments Outside Diff (1)

  1. Sources/Userpilot/PushNotification/PushNotificationMonitor.swift, line 176-200 (link)

    P2 Permission Prompt Fires On Connect

    The new push flow requests notification authorization from the SDK status refresh path unless the host explicitly opts out. Apps that enable automatic push config can show the iOS permission prompt as soon as the socket opens, before the host app has a chance to present its own explanation.

Reviews (1): Last reviewed commit: "💫 Update content transition steps anima..." | Re-trigger Greptile

Comment on lines +343 to +349
if eventName == Constants.Event.batchEventsEvent {
logger.info("✅ Offline batch events sent successfully")
if let completion = offlineRestoreCompletion {
offlineRestoreCompletion = nil
completion()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Comment on lines 599 to 612
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)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Close Callback Reorders Queue

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.

Comment on lines +628 to +630
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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Ack Removes Wrong Event

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.

Comment on lines +40 to +71
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)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Remote URLs Open Permissively

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.

Comment on lines +91 to +99
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 URL Open Lacks Scene Context

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 @@
//

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

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