-
Notifications
You must be signed in to change notification settings - Fork 93
Comparing changes
Open a pull request
base repository: launchdarkly/ios-client-sdk
base: 11.1.1
head repository: launchdarkly/ios-client-sdk
compare: 11.1.2
- 8 commits
- 14 files changed
- 9 contributors
Commits on Mar 18, 2026
-
fix: Fix flaky FlagSynchronizerSpec tests (SDK-2042) (#483)
Fixes two flaky tests in FlagSynchronizerSpec.swift, each in a separate commit: change_isOnline__online_to_offline__stops_polling — Moved isOnline = false inside the onSyncComplete callback, before signaling the semaphore. This prevents a second timer tick from firing between the callback and the assertion, which caused getFeatureFlagsCallCount to be 2 instead of 1 on slow CI runners. streaming_events__event_reported_while_polling__reports_an_event_error — Added an explicit 5-second timeout to the first waitUntil block. The default 1-second timeout was insufficient for the multi-hop async callback chain (main RunLoop → syncQueue → getFeatureFlags → reportSyncComplete → DispatchQueue.main.async) on loaded CI runners.
Configuration menu - View commit details
-
Copy full SHA for cd38c0f - Browse repository at this point
Copy the full SHA cd38c0fView commit details
Commits on Mar 19, 2026
-
fix: Ensure done() is called only once in LDTimerSpec (#485)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/v11/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** Fixes the `build-ios (15.4.0, platform=iOS Simulator,name=iPhone 15, macos-14)` CI failure: ``` timerFired__calls_execute_on_the_fireQueue_multiple_times, failed - waitUntil(..) expects its completion closure to be only called once ``` **Describe the solution you've provided** The `timerFiredSpec` test creates a repeating timer with a 0.01s interval inside a `waitUntil` block. After `fireCount` reaches 2, the `else` branch calls `done()`. Because the timer continues to fire every 10ms, `done()` was being called on every subsequent fire before Nimble could tear down the `waitUntil`, violating the exactly-once contract. This PR adds a `didCallDone` guard so that `done()` is invoked only on the first eligible timer fire and skipped thereafter. **Describe alternatives you've considered** Cancelling the timer inside the callback before calling `done()` would also work, but that would change what the test is asserting (the test expects the timer to still be valid and not cancelled after `waitUntil` completes — see lines 58–60). **Additional context** **For reviewer** — the callback executes on `testContext.fireQueue`, which is a serial `DispatchQueue`, so concurrent access to `didCallDone` is not a concern. No production code is changed. Link to Devin session: https://app.devin.ai/sessions/f07904a28c9048778d5b15d1d2c8eba5 Requested by: @kinyoklion <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: test-only change that prevents `waitUntil`'s `done()` callback from being invoked multiple times by a fast repeating timer. > > **Overview** > Stabilizes the `LDTimerSpec.timerFired` test by guarding the `waitUntil` completion so `done()` is only invoked once even if the repeating timer continues to fire. > > This fixes a flaky/CI-failing condition where the test could call `done()` multiple times without changing any production behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 498ad90. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 1d1bc02 - Browse repository at this point
Copy the full SHA 1d1bc02View commit details
Commits on Mar 20, 2026
-
chore: Update sdk_metadata features (#482)
Adds a features map to the swift-client-sdk entry in .sdk_metadata.json, documenting which SDK features are supported and the version each was introduced. 18 features included: allFlags, appMetadata, autoEnvAttrs, bigSegments, contexts, eventCompression, experimentation, flagChanges, hooks, inlineContextCustomEvents, multiEnv, offlineMode, perContextSummaryEvents, pluginSupport, privateAttrs, relayProxyProxy, track, variationDetail.
Configuration menu - View commit details
-
Copy full SHA for 5918885 - Browse repository at this point
Copy the full SHA 5918885View commit details
Commits on Mar 23, 2026
-
chore: Fix flaky FlagSynchronizerSpec polling tests (#486)
**Requirements** - [ ] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/v11/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** - Follows up on #483 and #480 which addressed other flaky tests in this file - Fixes the `build-ios (15.4.0, macos-14)` CI failure: `polling_timer_fires__one_second_interval__stops_polling, failed - expected to equal <2>, got <6>` - Fixes the `build-ios (16.4.0, macos-15)` CI failure: `streaming_events__event_reported_while_polling__reports_an_event_error, failed - waitUntil(..) expects its completion closure to be only called once` **Describe the solution you've provided** Adds a `didSignal` guard to prevent `onSyncComplete` callbacks from re-entering and double-signaling the semaphore. For tests that verify polling stopped, captures the call count after stopping and uses `Thread.sleep` to confirm no further requests occur. For tests that only need to confirm polling started, relaxes exact-count assertions to `>= 1`. Five tests are fixed: 1. **`changeIsOnlineSpec` ("stops polling")** — Added `didSignal` guard. The `isOnline = false` + `semaphore.signal()` inside the callback was already present in v11. Changed `== 1` to `>= 1`, then captures `countAfterStop`, sleeps 1.5s, and asserts count unchanged — proving polling actually stopped. 2. **`changeIsOnlineSpec` ("starts polling")** — Added `didSignal` guard to prevent double-signaling. No other changes to callback body; original test structure preserved (`isOnline` stays `true`, cleanup at the end). Changed `== 1` to `>= 1`. 3. **`changeIsOnlineSpec` ("does not stop polling")** — Added `didSignal` guard. No `isOnline = false` in callback; original test structure preserved. Changed `== 1` to `>= 1`. 4. **`streamingProcessingSpec` ("event reported while polling")** — Replaced the first `waitUntil` block with the semaphore + `didSignal` guard pattern. The original `waitUntil { done in ... { _ in done() } ... isOnline = true }` failed when polling timer ticks called `done()` more than once. 5. **`pollingTimerFiresSpec` ("stops polling")** — After stopping polling inside the callback at `requestCount == 2`, the test now captures the call count, waits 1.5 seconds, and asserts the count hasn't changed — directly proving polling actually stopped. Uses `>= 2` instead of `== 2` to tolerate in-flight callbacks. All changes are test-code-only; no application code is modified. **Describe alternatives you've considered** An earlier revision stopped polling (`isOnline = false`) inside the callbacks for tests #2 and #3 as well, but this introduced a timing dependency: the count staying at exactly 1 relied on the main queue processing the callback before the next 1-second timer tick. The current approach avoids that dependency entirely — the guard prevents re-signaling and `>= 1` tolerates any number of extra ticks. **Additional context** > **For reviewers — human review checklist:** > - **`>= 1` in "does not stop polling"**: This is weaker than the original `== 1`. The original assertion caught a restart regression (second `isOnline = true` triggering another immediate flag request). With `>= 1`, both normal polling and a restart regression satisfy the assertion. The `startPolling()` guard (`flagRequestTimer == nil`) in production code is what prevents restarts; this test no longer independently verifies that. If this is a concern, an alternative would be exposing timer state for direct inspection. > - **`didSignal` is not explicitly synchronized**, matching the existing `didCallDone` pattern in `LDTimerSpec`. The guard only needs to prevent the callback *body* from running a second time — it does not need to be atomic to accomplish this. > - **`Thread.sleep(forTimeInterval: 1.5)`** is used in both "stops polling" tests. This is generous relative to the 1-second polling interval but could theoretically be tight on very slow CI runners. Let me know if you'd prefer a longer wait or a different verification approach. > - **`pollingTimerFiresSpec` and `changeIsOnlineSpec` "stops polling" both use `>= N` then verify count stability**: There is a small window where an in-flight `getFeatureFlags` call could land between `isOnline = false` and the `countAfterStop` read. The `>= N` tolerates this, and the 1.5-second sleep assertion proves no *further* polling occurs regardless of the exact count at stop time. Link to Devin session: https://app.devin.ai/sessions/120fb9abea7743249e11b5bcbbcd1d8a Requested by: @kinyoklion --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 648921c - Browse repository at this point
Copy the full SHA 648921cView commit details -
chore: pin third-party GitHub Actions to commit SHAs (#490)
## Summary Pin all third-party GitHub Actions to full-length commit SHAs to prevent supply chain attacks. Addresses findings from the [`third-party-action-not-pinned-to-commit-sha`](https://github.com/launchdarkly/semgrep-rules/blob/main/github-actions/third-party-action-not-pinned-to-commit-sha.yml) Semgrep rule. ## Test plan - [ ] Verify CI passes with pinned action SHAs <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes only update GitHub Actions references to fixed commit SHAs; behavior should remain the same aside from potential breakage if the pinned SHAs are incompatible. > > **Overview** > Pins third-party GitHub Actions used by the release workflow to specific commit SHAs (notably `googleapis/release-please-action` and `maxim-lobanov/setup-xcode`) to reduce supply-chain risk. > > No functional release logic changes beyond updating how those actions are versioned/referenced. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 730c950. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Matthew M. Keeler <mkeeler@launchdarkly.com>
Configuration menu - View commit details
-
Copy full SHA for 0c03d39 - Browse repository at this point
Copy the full SHA 0c03d39View commit details
Commits on Mar 24, 2026
-
fix: app hang in didEnterBackground by making ConnectionInformationSt…
…ore writes async (#489) ## Summary - Move `ConnectionInformationStore.storeConnectionInformation` from a synchronous `UserDefaults.set` call to an **async dispatch** on a dedicated serial queue, preventing the main thread from blocking during background transitions - Reads go directly to `UserDefaults` without queue serialization — `UserDefaults` reads are thread-safe and this avoids blocking the main thread if a slow write is in-flight Fixes #488 ## Problem When the app transitions to background, `LDClient.didEnterBackground` synchronously writes connection information to `NSUserDefaults` on the main thread: ``` LDClient.didEnterBackground → Thread.performOnMain (DispatchQueue.main.sync) → runMode.didSet → connectionInformation.didSet → ConnectionInformationStore.storeConnectionInformation → UserDefaults.set → -[NSOperation waitUntilFinished] → BLOCKED (5000+ ms) ``` `NSUserDefaults.set` can trigger cross-process synchronization that blocks the main thread, especially on MDM-managed devices with concurrent `NSUserDefaults` access. This causes a watchdog hang of 5000+ ms. In our production app this has caused **7,016 hang events across 653 users**. ## Change **File:** `LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift` ```diff final class ConnectionInformationStore { private static let connectionInformationKey = "..." + private static let storeQueue = DispatchQueue(label: "com.launchDarkly.ConnectionInformationStore.storeQueue") static func retrieveStoredConnectionInformation() -> ConnectionInformation? { - UserDefaults.standard.retrieve(...) + UserDefaults.standard.retrieve(...) // no queue — UserDefaults reads are thread-safe } static func storeConnectionInformation(connectionInformation: ConnectionInformation) { - UserDefaults.standard.save(...) + storeQueue.async { + UserDefaults.standard.save(...) + } } } ``` ## Design decisions - **Writes are async**: The `UserDefaults.set` call that causes the 5s+ hang is dispatched to a background serial queue, unblocking the main thread - **Reads bypass the queue**: `UserDefaults` reads are inherently thread-safe (Apple documentation). `retrieveStoredConnectionInformation` is only called once during `LDClient.init`, when no writes are in-flight. Wrapping reads in `storeQueue.sync` would reintroduce the main-thread blocking if a slow write were queued - **Connection information is advisory state** (diagnostic/logging) — eventual consistency is acceptable ## Test plan - All 596 existing tests pass with 0 failures - Verified `swift build` compiles cleanly - Verified `swift test` passes all test suites <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes persistence semantics by making `ConnectionInformationStore` writes asynchronous, which could introduce timing/race issues (e.g., last-write not yet flushed) though scope is limited to cached connection diagnostics. > > **Overview** > Prevents main-thread stalls when persisting connection diagnostics by dispatching `ConnectionInformationStore.storeConnectionInformation` UserDefaults writes onto a dedicated serial `DispatchQueue`. > > Keeps `retrieveStoredConnectionInformation` reading directly from `UserDefaults` (no queue serialization) and simplifies key access to use the local `connectionInformationKey` constant. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c2e2abf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 72b0ab4 - Browse repository at this point
Copy the full SHA 72b0ab4View commit details -
fix: Call identify hooks during init. (#487)
**Requirements** - [X] I have added test coverage for new or changed functionality - [X] I have followed the repository's [pull request submission guidelines](../blob/v11/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** N/A **Describe the solution you've provided** Before this change, `beforeIdentify` and `afterIdentify` were only called when `identify` was called. After this change, they are also called as part of `init`. **Describe alternatives you've considered** None. **Additional context** I don't think this would be considered a breaking change, but I wouldn't mind a second opinion. The change in behavior is observable by the user, but the new behavior is what was originally intended. So I would consider this a bugfix, and would not expect customers to need to make any changes to their code to deal with the change. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes observable hook execution timing by invoking `beforeIdentify`/`afterIdentify` during `LDClient` init and by including plugin-provided hooks in that lifecycle, which could affect apps with side-effecting hooks. Scope is limited to hook plumbing and tests, with no auth or data model changes. > > **Overview** > `LDClient` now executes identify hooks as part of initialization: it runs `beforeIdentify` with method name `"init"` and defers `afterIdentify` until the initial `setOnline` completes. > > Plugin hooks are collected earlier (during `LDClient` init rather than `LDClient.start`) so plugin-provided hooks participate in the init identify lifecycle, and the hook helper APIs in `LDClientIdentifyHook.swift` are widened to `internal` to support this. > > Tests are updated/expanded to assert the additional init-time hook calls, preserve hook ordering, and verify plugin hooks fire during init. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ab0372a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 844d5d4 - Browse repository at this point
Copy the full SHA 844d5d4View commit details -
chore(v11): release 11.1.2 (#491)
🤖 I have created a release *beep* *boop* --- ## [11.1.2](11.1.1...11.1.2) (2026-03-24) ### Bug Fixes * app hang in didEnterBackground by making ConnectionInformationStore writes async ([#489](#489)) ([72b0ab4](72b0ab4)) * Call identify hooks during init. ([#487](#487)) ([844d5d4](844d5d4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk release bookkeeping only: version strings and documentation/changelog updates with no functional code changes in this diff. > > **Overview** > Bumps the SDK version from `11.1.1` to `11.1.2` across release metadata (`.release-please-manifest.json`), build settings (`DYLIB_CURRENT_VERSION`/`MARKETING_VERSION` in `project.pbxproj`), CocoaPods (`LaunchDarkly.podspec`), and runtime reporting (`ReportingConsts.sdkVersion`). > > Updates `CHANGELOG.md` with the `11.1.2` release notes and refreshes the SPM install snippet in `README.md` to reference `11.1.2`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit feb3392. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LaunchDarklyReleaseBot <LaunchDarklyReleaseBot@launchdarkly.com>
Configuration menu - View commit details
-
Copy full SHA for 34fcbc0 - Browse repository at this point
Copy the full SHA 34fcbc0View commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff 11.1.1...11.1.2