Skip to content

Release 2.15.2#740

Open
itmindbox wants to merge 61 commits into
masterfrom
release/2.15.2
Open

Release 2.15.2#740
itmindbox wants to merge 61 commits into
masterfrom
release/2.15.2

Conversation

@itmindbox

Copy link
Copy Markdown
Contributor

Updates the release version to 2.15.2. Automated PR: merge release/2.15.2 into master

AndreyEmtsov and others added 30 commits May 22, 2026 13:46
Merge 'master' into 'develop' after release
…709)

CocoaPods trunk sometimes returns 504 Gateway Timeout while the spec still lands in CocoaPods/Specs. Re-running the failed publish job
then fails again with 409 "Unable to accept duplicate entry",
keeping the release red even though it is actually done.

Wrap pod trunk push in .github/pod-trunk-push.sh and treat 409 as a successful no-op so a single re-run closes the release cleanly.
…ailure (#706)

* MOBILE-182: Pin DateFormatter to en_US_POSIX in MindboxLogger ISO-8601 primitive

* MOBILE-182: Collapse duplicate ISO-8601 formatter into the MindboxLogger primitive

* MOBILE-182: Assert dateTimeUtc literal shape on InAppShowFailure

* MOBILE-182: Replace incremental pbxproj IDs with random 24-char hex

---------

Co-authored-by: Vailence <utekeshev@mindbox.cloud>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…device UUID (#714)

* MOBILE-183 Unify date formatting on a single cached, thread-safe formatter

* MOBILE-183 Persist MBPersistenceStorage dates as fixed-pattern UTC

* MOBILE-183 Add DateFormatMigration for legacy persisted date strings

* MOBILE-183 Snapshot isInstalled after migration in CoreController

* MOBILE-183 Stabilize installed state and device UUID generation

* MOBILE-183 Add detailed logging to DateFormatMigration

* MOBILE-183 Handle 12h/24h hour cycle in DateFormatMigration

* MOBILE-183 Run DateFormatMigration through the standard migration chain

* MOBILE-183: Add deviceUUID/instanceId initialization tests

* MOBILE-183: Align mock isInstalled with production and test date round-trips

* MOBILE-183: Keep mock installationDate precision, derive isInstalled from marker

* MOBILE-183: Address PR review — formatter caching, key reuse, Swift Testing migration

- Replace NSLock+mutable cache with an immutable static formatter dictionary
- Precompute Date+Extension formatters as static let
- Derive DateFormatMigration date keys from UserDefaultsWrapper.Key; make formatters lazy
- Rewrite DateFormatMigrationTests on Swift Testing; restore global defaults in deinit
- Use an independent hour-cycle oracle (Locale.Components) instead of the production expression
- Fix configutaion -> configuration typo in CoreController

* MOBILE-183: Avoid force-unwrap in DateFormat formatter lookup

Use a CaseIterable-built formatter cache with an on-demand fallback instead of
force-unwrapping the dictionary lookup, satisfying the force_unwrapping rule.

---------

Co-authored-by: Vailence <utekeshev@mindbox.cloud>
A delay of 0 (nil / "00:00:00" / invalid string all fall back to 0) arms
the production timer for .now(), so it can present the in-app and clean up
on its own before the test inspects state. The tests asserted a
"scheduled but not yet presented" intermediate window that does not
deterministically exist on that path, so they raced the timer — CI saw
count==0, a nil presentationTime, and presentCallsCount==1.

Assert only the deterministic end state and trigger showEligibleInapp
only when the timer hasn't already; removal happens under `queue`, so
presentation stays idempotent and fires exactly once on either path. Also
add the missing isEmpty cleanup assertion to the zero/invalid-delay tests
and an explicit getDelay(nil)==0 check, for parity with the no-delay case.
Swift Testing results are invisible in the legacy reporting path: HTML comes
from xcpretty, which cannot parse them. Instead, scan now emits the junit
report (trainer builds it from the xcresult, formatter-independent) plus the
xcresult bundle itself; the workflow publishes a junit-based check/summary
and uploads the xcresult as an artifact for full inspection in Xcode.
* MOBILE-0000: Publish develop coverage to GitHub Pages

Coverage HTML is generated from the test run's xcresult by a dependency-free
script (test bundles excluded from the numbers) and deployed with the official
upload-pages-artifact/deploy-pages flow on every push to develop - no gh-pages
branch, so the repo SPM clients mirror-clone does not grow.

The trend survives full-site redeploys by carrying the published history.json
forward into each new site (~150 bytes per deploy, capped at 365 points). The
page is fully self-contained - inline CSS/JS, no external resources - with
file search, expand/collapse, source links at the measured commit, dark theme
and viewer-local timestamps.

* MOBILE-0000: Add CSV/JSON export links to coverage site

Raise the history cap from 365 to 1000 points: it bounds records, not
days, and develop gets ~190 pushes/year, so 365 only covered ~2 years.
1000 points is still a ~220 KB JSON.

coverage.csv is generated at build time next to index.html, so the
export needs no JS and the page stays self-contained.
Since Flutter 3.29 the UI and platform threads are merged on iOS, so the synchronous work in public operations - JSON validation, body encoding, the CoreData write, in-app matching - runs on the host UI thread: 1-2.5 ms per call, 24-83 ms per burst of 25 on an iPhone 12, surfacing as >33ms interaction-delay hangs. Route the post-validation body of executeAsync/SyncOperation (all 6 overloads) and pushClicked (x2) through one serial eventQueue: callers return in ~2-4 us, order of public calls = order of DB writes = order of delivery. Bodies are still encoded on the caller (they are mutable classes the host may reuse right after the call), and track() stays synchronous on purpose: its skipNextDirectTrackVisit write must happen-before the didBecomeActive-driven trackDirect dispatch.

executeSyncOperation completions now reach main on every path: the network result has hopped to main since 2021, the early validation errors (no configuration / no deviceUUID) were the inconsistent exception. observe() resolves the value under observeSemaphore but invokes the host completion after unlocking, removing a latent self-deadlock when a completion re-enters getDeviceUUID/getAPNSToken; delivery thread is unchanged. Nil-dependency paths log an .error instead of dropping events silently. Operation-name validation switches from a per-call NSRegularExpression (~3 us + allocations) to an allocation-free scalar scan (~16 ns), staying synchronous as a fail-fast guard; it is intentionally stricter for names with a trailing line terminator, which the ICU $-quirk used to accept (verified side-by-side; numbers and method in Profiling/MOBILE-208-operations.md on profiling/MOBILE-208-operations).

Device A/B (iPhone 12, Release, 9 runs per side): per-call caller cost 0.8-2.6 ms -> 2-4 us; burst of 25 on main 24-83 ms -> 0.1-0.2 ms; interaction-delay hangs 27 -> 1; process CPU and thread count unchanged.
Swift Testing: OperationNameValidatorTests - parameterized charset cases (the three legacy cases migrate with the same polarity: TEST.- valid, тест/TESт invalid), the exact ASCII range-boundary neighbors of the scalar scan, unicode lookalikes/invisibles, and the intentional trailing-line-terminator tightening. MindboxOperationsTests - burst call order equals DB write order, body snapshotted at call time (host mutation after return doesn't leak), invalid name/JSON dropped with a sentinel proving the pipeline survives, executeSyncOperation completion on main for both the network and the early-error path.

XCTest (needs the suite's init machinery): observe() re-entrancy regression - getAPNSToken called from inside the getDeviceUUID completion deadlocked while the completion was invoked under observeSemaphore. Verified against the old implementation: fails there with three timeouts (controller queue wedged), passes on the fix.
The suite resets process-wide singletons (PersistenceStorage.reset(),
DB erase(), Mindbox.shared.assembly(), canScheduleOperations), so it must
not interleave with other tests. The Mindbox scheme already runs the target
serially (parallelizable=NO), but mark the suite .serialized to match the
other stateful suites and drop the implicit dependency on that scheme flag.
…ad-operations

Mobile-208: UI thread operations
Add Mindbox.xctestplan covering MindboxTests, MindboxNotificationsTests and
MindboxLoggerTests, and point the scheme + unitTestLane at it. Now every test
target runs in one simulator boot/build - MindboxNotificationsTests was never
executed in CI before - while MindboxTests stays serial (shared singletons).
Code coverage stays enabled via the plan, now also covering
MindboxNotifications.framework.
The MindboxLoggerTests target sat empty while its tests lived in MindboxTests.
Move the 4 pure-MindboxLogger test files plus their 2 mocks into it and drop a
dead `@testable import Mindbox` from MBLoggerCoreDataManagerTests (it only
exercises MindboxLogger types). Relocate the .dateFormatting tag - used solely
by the moved DateFormatTests - into a Logger-local Tag+Extensions, and raise the
target to iOS 13 since Swift Testing requires it.

SDKLogManagerTests stays in MindboxTests: it tests SDKLogsManager, a Mindbox
type, and genuinely needs the Mindbox import.

All three targets run via the shared test plan; MindboxLoggerTests is now
non-empty and green, MindboxTests still passes.

pbxproj edits done with the xcodeproj gem, which also reflowed three unrelated
PBXFileSystemSynchronizedRootGroup entries (semantically a no-op).
Keep the default Mindbox plan Unit-Tests-only so local Cmd+U and CI stay fast, and give Thread and Address Sanitizer their own plans to run on demand — a multi-config plan runs every config on Cmd+U and the sanitizer-instrumented runner is slow and flaky to launch.

Move all three plans under TestPlans/ (also pulling AddressSanitizer back into the repo — it had been saved above the project root), and scope the Mindbox plan's coverage to the three product frameworks so test bundles are no longer instrumented. Published coverage is unchanged (~53.5%); a new product framework must be added to codeCoverageTargets to be measured.
Clear DEVELOPMENT_TEAM (was N39VVWZXXP) across all build configs so the project is team-agnostic: simulator builds and CI unit tests need no team, and nothing in CI signs for a device (only build/unit-test lanes, both on the simulator). Removes the signing warnings the hardcoded team produced for anyone not on it.
Align bundle identifiers on the cloud.<Target> scheme: the Logger targets still carried the cloud.organization.* Xcode template placeholder and MindboxTests used Mindbox.MindboxTests. These IDs are dev/CI-only — the SDK ships via CocoaPods and SPM (which set their own) and nothing reads them by string (no Bundle(identifier:)).

Also set SUPPORTS_MACCATALYST = NO on MindboxLogger to match Mindbox and MindboxNotifications; it was the only framework still offering a Mac Catalyst destination.
Add threadSanitizerLane and addressSanitizerLane that run the unit tests under the Thread/Address Sanitizer test plans, each with its own output directory. Scaffolding for a future scheduled job — deliberately kept out of the PR/develop pipeline, since ThreadSanitizer stays red until the surfaced data races are fixed.
All three unit-test targets now deploy to iOS 13 — the minimum for Swift Testing — so new tests here build instead of failing, matching MindboxTests and MindboxLoggerTests. Product frameworks stay at iOS 12.

(Xcode also re-sorted SUPPORTS_MACCATALYST alphabetically in the Logger config; no behavior change.)
MOBILE-0000: Unit tests via shared test plan + Thread/Address Sanitizer plans
Rewrite the three MindboxNotifications suites from XCTest to Swift Testing and
add suites for the previously untested push-format pipeline (strategy factory,
legacy/current strategies, formatter, validator), ImageFormat, the public
isMindboxPush/getMindboxPushData API, and MBPushNotification decoding. Target
coverage goes 64.6% -> 95.9% (6 -> 43 tests); no production code changed.

The async download test awaits a continuation instead of a 1s expectation,
removing the CI-starvation flakiness without needing a URLSession seam. Shared
payload/request builders and the UN test doubles live in NotificationTestFixtures;
suites carry subject tags (.pushParsing / .imageFormat / .notificationService /
.notificationContent) plus a .network tag so CI can exclude the networked test.

Known gap (flagged, no test): legacy defaultIosWithoutTitle pushes, where
aps.alert is a String rather than a {title, body} dict, are not recognized by
isMindboxPush/getMindboxPushData -- LegacyFormatStrategy expects an alert dict.
…ations

MindboxNotifications links and depends on MindboxLogger but never imports or
uses it, so the framework is dead weight in integrators' NSE/NCE extensions.
Remove the link, build file, container proxy and target dependency. The main
Mindbox target keeps its own MindboxLogger dependency.
…ons-coverage

MOBILE-0000: Raise `MindboxNotifications` coverage to ~96%; drop unused `logger` dep
ios-sdk was the only SDK repo without Gitleaks; aligns secret
scanning with android/flutter/react-native. Requires the org
MINDBOX_GITLEAKS_LICENSE secret (Actions + Dependabot).
* MOBILE-206: Fix infinite layout loop in modal in-app element rebuild

* MOBILE-206: Add regression tests for modal layout rebuild fix

* MOBILE-206: PR Comm Fix

---------

Co-authored-by: Vailence <utekeshev@mindbox.cloud>
…726)

Full `bundle update` to clear Dependabot alerts. concurrent-ruby 1.3.6->1.3.7
fixes GHSA-h8w8/6wx8/wv3x. faraday 1.10.5->1.10.6 backports the
GHSA-98m9-hrrm-r99r (CVE-2026-54297) NestedParamsEncoder fix to the 1.x line --
fastlane still pins faraday ~>1.0, so 2.14.3 is unreachable. fastlane
2.235.0->2.236.1; Gemfile and BUNDLED WITH unchanged.
justSmK and others added 29 commits June 26, 2026 17:46
MindboxLoggerTests was a manual PBXGroup, so each new test file required four hand-edited pbxproj entries. Convert it to a PBXFileSystemSynchronizedRootGroup (matching MindboxTests) so files added under the folder compile automatically.
Swift Testing suites covering MindboxError and the error/decodable models, the Logger/MBLogger facade, Core Data context helpers, log primitives, URL attributes and persistence internals; tags split by subsystem. Raises MindboxLogger framework line coverage from 54% to 91% via MindboxLoggerTests alone.
Convert LogStoreTrimmer/LoggerDatabaseLoader/MBLoggerCoreDataManager tests from XCTest to Swift Testing (async waits via continuations) and drop the two baseline-less measure perf tests. The manager suite is .serialized because its background-notification tests post to the global NotificationCenter that every manager observes — parallel execution would flip writesImmediately across tests. Adds a .trimming tag.
Stop FileManagerStoreURLTests deleting the shared Caches store (cross-suite flake under parallel run); make the LoggerPersistenceInternals setter round-trip assert a distinct value instead of a tautology; rename the assertion-free MBLogger tests as honest smoke tests; fix the .serialized rationale comment.
Convert both manual PBXGroups to PBXFileSystemSynchronizedRootGroups (matching MindboxTests/MindboxLoggerTests). MindboxNotificationsTests uses a generated Info.plist (GENERATE_INFOPLIST_FILE) like MindboxLoggerTests, so no membership exception is needed; TestPlans is target-less so schemes resolve plans by path.
… cycle (#728)

* MOBILE-230: Fix executeSyncOperation dropping completion on invalid JSON

Previously, passing an invalid JSON string to executeSyncOperation(json:completion:)
caused the operation to be silently dropped on eventQueue without ever invoking the
completion handler. This violated the public contract (completion always arrives on
the main thread, either as .success or .failure).

Fix: deliver .failure(.internalError(.parsing)) on the main thread when JSON validation
fails, matching the delivery pattern used by MBEventRepository.send<T> for all other
error paths.

Test: syncInvalidJSONDeliversFailureOnMain asserts the completion is called as .failure
on the main thread, and will fail (timeout ~10s) rather than hang if the regression recurs.

* MOBILE-230: Deliver .validationError on invalid name/JSON for all sync overloads

All three executeSyncOperation overloads now call completion(.failure(.validationError(...)))
on the main thread when an invalid operation name is passed, matching the DoD requirement.

The invalid-JSON branch in enqueueSyncEvent is unified through the same failSyncOperation
helper, so both client-side validation paths produce a consistent .validationError with
a ValidationMessage describing the failure location.

Added test: syncInvalidNameDeliversFailureOnMain pins the regression for the name path.

* MOBILE-230: Fix hour-cycle override in DateFormatMigration

Locale.current encodes the device 12h/24h setting as an @Hours= keyword,
so appending a second hours= produced a duplicate that ICU resolved to the
first occurrence — defeating the override and leaving every legacy formatter
on the device's own hour cycle. Strip any pre-existing hours/hc keyword
before forcing the cycle so values written under the opposite cycle parse.

* MOBILE-230: Distinguish sync operation validation error by location

Parameterize failSyncOperation location so invalid JSON reports
operationBody instead of operationSystemName, fix the misleading
.parsing doc comment, and assert the error type and location in tests.

---------

Co-authored-by: Vailence <utekeshev@mindbox.cloud>
#730)

* MOBILE-213: Add MobileSdkShouldSendInAppTags feature toggle and gatedTags helper

Introduce the MobileSdkShouldSendInAppTags feature flag (same semantics as
MobileSdkShouldSendInAppShowError: disabled only when config sends false) and a
single pure gatedTags(isTagsFeatureEnabled:) helper that collapses disabled or
empty tag dictionaries to nil. Register the new source and test files.

* MOBILE-213: Send in-app tags in Show and Click operations

Gate tags inside InAppMessagesTracker via the injected FeatureToggleManager and
add tags to the Inapp.Click body (InAppBody). Thread the message's tags into the
click-tracking call site. Tracker method signatures for trackTargeting are also
updated here; their call sites are wired in the Targeting/ShowFailure change.

* MOBILE-213: Send in-app tags in Targeting and ShowFailure operations

Add tags to the Inapp.Targeting body and to each Inapp.ShowFailure item, gated by
the tags feature flag inside InappShowFailureManager. Thread tags through the
InAppConfigurationDataFacade (trackTargeting, collectTargetingFailures via
tagsByInappId, downloadImage) and the InappMapper call sites, plus the
presentation error path in InappScheduleManager.

* MOBILE-213: Merge in-app tags into WebView JS-bridge operations

Add JSONValue.mergingInAppTags implementing the merge contract (set when absent
or null, fill only missing keys when tags is an object, leave non-object tags
untouched) and apply it to sync/async custom operation bodies in TransparentView,
gated by the tags feature flag. Thread tags from InAppFormData through the
ViewFactory into WebViewController and TransparentView.

* MOBILE-213: Apply review fixes for in-app tags

- Guard FeatureToggleManager.featureToggles with NSLock (cross-thread access)
- Centralize tags gating in FeatureToggleManager.gatedTags(_:)
- Drop hand-written encode(to:) in InAppShowBody/InAppBody
- Collapse trackClick/trackTargeting into shared track helper
- Build tagsByInappId without nested optionality, only for failed in-apps
- Parse explicit MobileSdkShouldSendInAppTags=true in config fixture
- Construct Settings.FeatureToggles directly in test helpers

* MOBILE-213: Cover JS-bridge operation tags merging with TransparentView tests

* MOBILE-213: Cover tagsByInappId building and tags propagation in InappMapper tests

* MOBILE-213: Assert in-app tags propagate into show failure on presentation error

* MOBILE-213: Use a fresh FeatureToggleManager per parsing test to avoid shared-state leakage

* MOBILE-213: Fold Optional tags gating into a static FeatureToggleManager helper

* MOBILE-213: Log tag merge key collisions and non-object tags for Android parity

* MOBILE-213: Reject empty operation names when parsing JS-bridge operations

* MOBILE-213: Make JS-bridge operation error message reflect all validation failures

* MOBILE-213: Require explicit tags argument in WebView init to avoid silently dropping them

* MOBILE-213: Assert absence of tags key (not null) in tracker omit cases

* MOBILE-213: Conform EventRepositorySpy to cancelAllRequests requirement

---------

Co-authored-by: Vailence <utekeshev@mindbox.cloud>
Standalone robustness for the existing show, independent of the cache/
prewarm work that follows in later PRs:
- WebViewReadyChecker replaces the one-shot post-load bridge probe with a
  short poll, so a page whose module scripts evaluate a beat after `load`
  is not failed; a give-up after `init` closes the show instead of
  leaving a dead bridge on screen.
- Init timeout reports webview_presentation_failed (bridge never appeared)
  vs webview_load_failed (page never loaded) via timeoutError.
- Network and WebView build the User-Agent through one SDKUserAgent (fixes
  the "unknow" fallback typo).
- MBContainer guards DI registration/resolution with a recursive lock.
- The cached in-app config is written atomically.
WebViewReadyCheckerTests covers ready-first, retry-then-ready, give-up
after the full budget, error-retries, and silent cancel. WebViewTimeout
ErrorTests pins the presentation-failed vs load-failed split.
- InAppWebViewDataStore: one shared WKWebsiteDataStore for prewarm and
  shows — iOS 17+ an isolated persistent store under a fixed UUIDv5;
  below 17 .default() (product decision); cache kill-switch →
  .nonPersistent().
- InAppWebViewHTMLFetcher: the single HTML transport — a cookie-less
  session with its own small URLCache and revalidation when caching is
  on, else a cache-less ephemeral session.
- InAppWebViewFactory: the one place a Mindbox WKWebView is configured.
- The facade builds its WKWebView via the factory and fetches via the
  fetcher.
- Feature toggles MobileSdkShouldPrewarmInAppWebView / MobileSdkShould
  CacheInAppWebView (absent key ⇒ on, kill switch); fetchDecodedConfig
  FromCache centralizes the lenient launch-time decode.
Pins the shared-store contract (one isolated persistent instance, exact
UUIDv5), the cache toggle kill-switch semantics, the cookie-less
revalidating fetch session, and the factory's store/UA wiring.
SDKUserAgentTests pins the User-Agent segment order/format and the
"unknown" fallbacks — the network layer and the WebView share this one
builder, and the backend slices traffic by the string. MBContainer
ConcurrencyTests proves a container-scoped singleton is minted exactly
once under 32 concurrent resolves (the recursive lock); verified
non-flaky over 25 iterations and red when the lock is removed.
Adds .userAgent and .dependencyInjection test tags.
A pre-warmed WKWebView can deliver navigation callbacks and JS messages
left over from a previous load. Filter callbacks by navigation identity
and gate messages until the show's own document commits. Inert until the
prewarm path arms it via expectContentNavigation (PR3b).
Cold WebView shows pay for a WKWebView spin-up plus a full resource
fetch every time. Prewarm resources into a shared, reusable instance in
two stages off the init path — a cached-config head start (stage 1) and a
fresh-config confirmation (stage 2) — then let the show borrow that warm
instance instead of building a cold one. Navigation is pinned to
SDK-issued documents, learned hosts are preconnected, and the instance is
parked (blank load) or released under memory pressure. Gated by the
prewarm feature toggle; borrowed loads arm the bridge staleness filter.
DataStore: the pre-iOS-17 product decision is already stated in full in the type
doc; the inline note at `.default()` only repeated it (and its toggle-off line
describes `shared()`, not this `instance` closure).

LearnedHostsStore: "backed by PersistenceStorage" is obvious from the initializer.
These `// Add X` / `// Serialize` labels predate this feature but sit in a file it
already reworks; each just echoes the descriptively named method below it. Kept the
localState one — it carries a why (JS-side migration), not a restatement.
The develop rebase unioned Settings.FeatureToggles to four toggles (shouldSendInAppTags
from MOBILE-213 plus the webview cache/prewarm pair). The memberwise-init call sites in
four test files predated the union and were missing the added arguments — a semantic
merge break the textual rebase couldn't catch. Compile-verified via the test target.
Refresh all gems via a lockless bundle install; fastlane 2.236.1 ->
2.237.0, cocoapods 1.16.2 -> 1.17.0. Lock now records Bundler 4.0.15
with CHECKSUMS, aligning with ios-app and CI's unpinned bundler.
Slowest unit test is ~7s and everything else is sub-second, so 60s keeps wide headroom while catching a real hang faster. 60s is also the effective floor — the allowance rounds up to the nearest minute.
The JS-bridge onError contract must match onValidationError: the
contents of data directly, without the {type, data} envelope.
createJSON() is unchanged - public API used by wrapper SDKs.
The convertToString() fallback had unquoted keys, trailing commas and
misspelled errroKey/errroName — unparseable by any JS consumer. Flagged
by Copilot on the PR; pre-existing, but it belongs to this contract.
@github-actions

Copy link
Copy Markdown
Contributor
TestsPassed ✅SkippedFailedTime ⏱
Unit tests report1329 ran1329 ✅2m 23s 899ms

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.

5 participants