Repository navigation
chore: add an Apple test app for iOS, Mac Catalyst, macOS, tvOS and watchOS - #545
abelonogov-ld wants to merge 14 commits into
Conversation
TestApp/ is an Xcode project linking the SDK as a local Swift package, with a target per platform: iOS (also built for Mac Catalyst), native macOS, tvOS and watchOS. It evaluates flags, tracks and identifies, switches event persistence and offline mode, kills the process with events unsent, and shows the event directory as it changes. On a Mac it launches more instances of itself, to exercise persistence shared between processes. CI builds every target. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… test app The button sits under the status text and runs the Android test app's benchmark, section for section, on stores of its own in a temporary directory, then shows the tables in a sheet with Copy. It is in Debug builds only, since it reaches the SDK's internals through @testable import. Co-authored-by: Cursor <cursoragent@cursor.com>
Posts a 20 MB payload of valid custom events straight to the events service on a session of its own, with the headers the SDK's sender uses, and shows the status, headers and body that come back. Neither the event store, the batch size limit nor the retry state is involved, so the answer to a body that size is seen directly. Next to Benchmark, and like it built only for Debug. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| } | ||
| } | ||
| store.commit() | ||
| } |
There was a problem hiding this comment.
Benchmark hits store byte cap
Medium Severity
recordingCostPerCall constructs EventStore with capacity: .max but leaves the default byteCapacity of 20 MiB. Staged and committed events still count against that limit until they are delivered, so after about 16 MiB stage starts returning false. measure then keeps the fastest of five rounds, which are the rounds that mostly drop. The evaluation, full event and evaluation + track (commit point) rows therefore report the cost of a rejected write rather than staging or commit.
Reviewed by Cursor Bugbot for commit fed655e. Configure here.
| let start = DispatchTime.now().uptimeNanoseconds | ||
| body() | ||
| samples[index] = Double(DispatchTime.now().uptimeNanoseconds - start) | ||
| } |
There was a problem hiding this comment.
Watch skips outlier scaling
Low Severity
Watch scaling in measure divides iterations by ten, but measureDistribution always runs the full 100 / 1,000 / 10,000 calls. That is the commit-point outliers section, which times every commit on the caller’s thread. The class comment and README say a watch run uses a tenth of the iterations; on watchOS this section still does the full 10,000 disk commits and can run far longer than the rest of the harness.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit fed655e. Configure here.
…rsistence' into andrey/apple-test-app
Each environment's DarklyService built its own URLSession, and each session keeps its own connection pool, so seven environments posting events and diagnostics to the same host opened at least seven TCP+TLS connections. The session configuration does not depend on the environment, so every service now uses one shared session and their requests reuse connections. Streams are unaffected: they use LDSwiftEventSource's own sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 15eae25. Configure here.
| client?.setOnline(!offline) { [weak self] in | ||
| Task { @MainActor in self?.refreshConnectionStatus() } | ||
| } | ||
| } |
There was a problem hiding this comment.
Offline toggle ignored during restart
Medium Severity
Toggling Offline during start or restart updates isOffline in the UI, but client is nil so setOnline never runs, and makeConfig has already captured the previous value. The replacement client comes up in the opposite online state from the toggle. The persistence picker is disabled while isRestarting is true; this toggle is not. First start waits up to 10 seconds for flags, so the mismatch is easy to hit and can send events during an intended offline persistence check.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 15eae25. Configure here.
| func restart(reason: String = "Restarted") { | ||
| guard !configuration.isMissingMobileKey, !isRestarting | ||
| else { return } | ||
| isRestarting = true |
There was a problem hiding this comment.
Restart never shows Restarting status
Low Severity
refreshConnectionStatus can set connectionStatus to Restarting when isRestarting is true, but nothing calls it when a restart begins. The status line keeps the previous client's mode (or Not started on first launch) for the whole wait, so the Restarting branch is never used.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 15eae25. Configure here.
Co-authored-by: Cursor <cursoragent@cursor.com>


Requirements
Related issues
Stacked on #538 (event persistence, tier 3). Counterpart of the Android SDK's
test-app.Describe the solution you've provided
TestApp/is an Xcode project inside the SDK repo that links the SDK as a local Swift package, so it always runs the code in the checkout. It has one target and shared scheme per platform:TestApp_iOSTestApp_macOSTestApp_tvOSTestApp_watchOSAll four targets share
TestApp/Sources. The app:flushAndWait;disabled/deferred/immediate, restarting the client) and offline mode;fatalErrorat once;DedupingHooks;open -n), so several processes share one event directory.TestApp/scripts/launch-instances.shdoes the same from a terminal.The mobile key comes from
TestApp/Config/Secrets.xcconfig, which is git-ignored (seeSecrets.xcconfig.example), or from a-LDMobileKeylaunch argument.LAUNCHDARKLY_ENVIRONMENT = stagingpoints the app at ld-stg.CI: a new
build-test-appjob builds every scheme and destination on macos-15 with Xcode 16.4.Screenshots
Production environment,
immediatepersistence, just after start. The Mac app (native and Catalyst) shows the same form, plus a New instance button in the Event storage header.Describe alternatives you've considered
Additional context
andrey/durability-test-app, never merged) can retire once this lands.Note
Overview
Apple multi-platform test harness
The repo now includes an Xcode 16
TestAppproject that consumes the local Swift package and shares one SwiftUI codebase across iOS/Mac Catalyst/macOS/tvOS/watchOS targets. The UI drives the SDK through client restart toggles, offline controls, flag evaluation, multi-instance event storage inspection, exposure dedupe hooks, stress/loss scenarios, benchmark tooling, and oversized POST probes. Secrets are managed via optionalSecrets.xcconfig, ignored by Git, with docs/scripts (launch-instances.sh) covering local workflows.CI + tooling updates
A new composite
build-test-appaction primes simulator services thenxcodebuilds every scheme without signing.ci.ymlgains a macOS 15 job pinned to Xcode 16.4 that runs this action so the harness is built continuously..gitignorenow excludes the real secrets file alongside the documentation additions already shown in README.Reviewed by Cursor Bugbot for commit c14e919. Bugbot is set up for automated code reviews on this repo. Configure here.