Skip to content

SDK-412 UUA Naming inconsistencies#1060

Merged
joaodordio merged 4 commits into
masterfrom
fix/SDK-412-UUA-Naming-inconsistencies
Jun 17, 2026
Merged

SDK-412 UUA Naming inconsistencies#1060
joaodordio merged 4 commits into
masterfrom
fix/SDK-412-UUA-Naming-inconsistencies

Conversation

@joaodordio

@joaodordio joaodordio commented Apr 28, 2026

Copy link
Copy Markdown
Member

🔹 Jira Ticket(s)

✏️ Description

Brings the iOS SDK in line with the cross-SDK Unknown User Activation (UUA) naming convention. All public renames ship with @available(*, deprecated, renamed:) forwarders, so source-compatible. On-disk formats migrate read-old/write-new on first access, so existing users keep their unknown sessions across the upgrade.

Public API renames (deprecated aliases kept)

  • UnknownUserManager: trackUnknownUserEvent to trackUnknownEvent, trackUnknownUserUpdateUser to trackUnknownUpdateUser, trackUnknownUserPurchaseEvent to trackUnknownPurchaseEvent, trackUnknownUserUpdateCart to trackUnknownUpdateCart, trackUnknownUserTokenRegistration to trackUnknownTokenRegistration, updateUnknownUserSession to updateUnknownSession, getUnknownUserCriteria to getUnknownCriteria. Same renames mirrored on UnknownUserManagerProtocol.
  • IterableIdentityResolution.mergeOnUnknownUserToKnown is now mergeOnUnknownToKnown. Deprecated computed alias and a deprecated convenience init keep the old call sites working.

Internal renames

  • Const.Path.trackUnknownUserSession to trackUnknownSession.
  • ApiClientProtocol.trackUnknownUserSession to trackUnknownSession.
  • RequestCreator.createTrackUnknownUserSessionRequest to createTrackUnknownSessionRequest.

Storage migrations

  • Keychain: itbl_userid_unknown_user to itbl_userid_unknown. Legacy key still readable via a one-shot migration in the userIdUnknownUser getter.
  • UserDefaults: itbl_unknown_user_sessions to itbl_unknown_sessions. Legacy blob is decoded, re-encoded under the new key, then removed.
  • IterableUnknownUserSessionsWrapper and IterableUnknownUserSessions now use CodingKeys to encode the new schema (itbl_unknown_sessions / totalUnknownSessionCount / lastUnknownSession / firstUnknownSession) while still decoding both old and new payloads. Swift property names stayed put to keep the diff small.
  • Stored event discriminator: JsonKey.eventType is now "eventType" (was "dataType"). unknownUserEvents and unknownUserUpdate getters normalize legacy entries on read and persist them back.

Tests

  • New UUANormalizationMigrationTests.swift covers the keychain migration, UserDefaults sessions blob migration, sessions wrapper round-trip (decodes legacy and modern, encodes only modern), the dataType to eventType rewrite on stored events, and the IterableIdentityResolution deprecated init alias.
  • Existing UUA tests updated to reference the canonical names.

@codecov

codecov Bot commented Apr 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.66055% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.14%. Comparing base (72b371d) to head (04d8903).

Files with missing lines Patch % Lines
swift-sdk/Internal/InternalIterableAPI.swift 72.72% 3 Missing ⚠️
swift-sdk/Internal/UnknownUserManager.swift 90.32% 3 Missing ⚠️
swift-sdk/SDK/IterableAPI.swift 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1060      +/-   ##
==========================================
+ Coverage   71.64%   72.14%   +0.49%     
==========================================
  Files         114      114              
  Lines        9467     9538      +71     
==========================================
+ Hits         6783     6881      +98     
+ Misses       2684     2657      -27     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joaodordio joaodordio marked this pull request as ready for review April 29, 2026 00:55
@joaodordio joaodordio requested a review from sumeruchat April 29, 2026 00:56

@sumeruchat sumeruchat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review notes

A few things worth addressing before merge. Ordered by severity.

🔴 1. IterableUserDefaults migration reads from the wrong store

swift-sdk/Internal/IterableUserDefaults.swift:165–175

Both the new-key and legacy-key reads use UserDefaults.standard, but writes/removals use the injected userDefaults. For any consumer on a non-default suite this means:

  • Legacy data in the configured suite is invisible to the migration → silent data loss
  • Unrelated data at the legacy key in .standard gets pulled into the configured suite → cross-store contamination

Fix: use userDefaults for the reads. Worth grepping the rest of the file — the pre-existing reads on this same path look like they have the same shape.

🔴 2. The new sessions migration test isn't exercising the migration path

tests/unit-tests/UUANormalizationMigrationTests.swifttestSessionsBlobMigratesFromLegacyUserDefaultsKey

The test writes to a suite-scoped UserDefaults, but production reads from .standard. It's passing by coincidence, not because the migration runs. Falls out of fixing #1, but please confirm the test actually fails without the fix.

🟡 3. Public UnknownUserManagerProtocol breaks external conformers

swift-sdk/Internal/UnknownUserManagerProtocol.swift:9–17

Adding the renamed methods as required members of an @objc public protocol breaks any downstream conformer (test doubles, wrappers) — they now have to implement both old and new spellings.

Suggested fix: drop the new methods from the protocol for this release. Keep only the deprecated names there; put the renamed methods on the concrete UnknownUserManager only. Internal call sites talk to the class anyway. Revisit at the next major.

🟡 4. Missing migration test for unknownUserUpdate

tests/unit-tests/UUANormalizationMigrationTests.swift

Only the array variant unknownUserEvents has a dataType → eventType test. The single-dict unknownUserUpdate getter runs through a separate path. One small mirror test.

🟢 5. Question: do we actually need to rename the keychain key?

swift-sdk/Internal/Utilities/Keychain/IterableKeychain.swift

The keychain key is internal-only — no backend, no host app, no other SDK reads it. Renaming itbl_userid_unknown_user → itbl_userid_unknown buys cosmetic alignment, but costs:

  • A permanent migration path
  • An extra keychain syscall on every set, forever
  • A downgrade footgun (revert to old build → unknown-user identity disappears)

Same argument arguably applies to the itbl_unknown_user_sessions UserDefaults key. The public API renames and the dataType → eventType discriminator change have clear justification — these two on-disk renames don't. Suggest reverting just those.


TL;DR: #1 is a real bug (silent data loss on non-default suites); #2 falls out of fixing it; #3 and #4 are strong recommends; #5 is a design question worth discussing before merge.

@joaodordio joaodordio force-pushed the fix/SDK-412-UUA-Naming-inconsistencies branch from 24b5343 to 0af342f Compare May 6, 2026 16:50
@joaodordio joaodordio requested a review from sumeruchat May 7, 2026 12:43
@joaodordio joaodordio self-assigned this May 11, 2026

@sumeruchat sumeruchat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the quick turnaround on the review notes. Re-reviewed commit 0af342f and verified each point:

  • 🔴 #1, #2 (UserDefaults.standard vs injected store + non-load-bearing test): resolved by deleting the migration entirely. Buggy code is gone, not patched.
  • 🟡 #3 (public protocol break): protocol surface reverted to pre-PR; InternalIterableAPI.unknownUserManager retyped to the concrete class so internal call sites keep the new names. Clean.
  • 🟡 #4 (missing unknownUserUpdate test): testUnknownUserUpdateRewritesLegacyDataTypeKeyOnRead added.
  • 🟢 #5 (keychain / sessions wrapper key renames): both reverted to original strings. Nice distinction between the on-disk wrapper key (reverted, internal-only) and the inner sessions field names (kept — those go to the backend in unknownSessionContext, so cross-SDK alignment matters there).

Bonus coverage I noticed beyond what was asked for:

  • testCriteriaCheckerNormalizesLegacyDataTypeOnInit — guards the criteriaDataType vs eventType invariant
  • testUnknownUserEventsLeavesModernEventsUntouched — idempotency on modern data
  • Deprecated-forwarder tests on every renamed UnknownUserManager method
  • UUANormalizationMigrationTests wired into the xcodeproj (was orphaned)

Ran ./agent_test.sh UUANormalizationMigrationTests locally — all 17 tests green.

🟢 LGTM.

@sumeruchat

sumeruchat commented May 13, 2026

Copy link
Copy Markdown
Contributor

@joaodordio one important pre-merge check I want to flag separately from the approval:

The inner IterableUnknownUserSessions field rename (totalUnknownUserSessionCounttotalUnknownSessionCount, lastUnknownUserSessionlastUnknownSession, firstUnknownUserSessionfirstUnknownSession) changes the JSON keys that get sent to the backend in the unknownSessionContext body on /unknownuser/events/session.

Before this merges, can you confirm the Iterable backend already accepts the new field names? Either:

  • The endpoint accepts both old and new keys today (preferred — safe rollout), or
  • The backend rolled out support for the new names ahead of this SDK release

If neither is true, every customer's unknown-user session tracking degrades silently the moment they pick up this SDK version — that's a quiet production regression with no client-side error.

@sumeruchat

Copy link
Copy Markdown
Contributor

One more housekeeping item @joaodordio — please add a note to the release notes (or CHANGELOG.md) calling out the upgrade-then-downgrade hazard on the sessions blob:

IterableUnknownUserSessions now encodes with the new field names (totalUnknownSessionCount, lastUnknownSession, firstUnknownSession). A user who installs an SDK version with this change and later downgrades to a pre-SDK-412 build will hit a decode failure on their stored sessions blob, since the older SDK's synthesized Codable expects the old key names. The blast radius is small — unknown-user session counter resets to zero — but it's a real behavior change worth flagging so customers who pin or roll back SDK versions aren't surprised.

Same goes for the dataType → eventType discriminator on unknownUserEvents/unknownUserUpdate — once the new SDK rewrites those entries, an older SDK reading them won't find the dataType key and will skip the events.

@joaodordio

Copy link
Copy Markdown
Member Author

@joaodordio one important pre-merge check I want to flag separately from the approval:

The inner IterableUnknownUserSessions field rename (totalUnknownUserSessionCounttotalUnknownSessionCount, lastUnknownUserSessionlastUnknownSession, firstUnknownUserSessionfirstUnknownSession) changes the JSON keys that get sent to the backend in the unknownSessionContext body on /unknownuser/events/session.

Before this merges, can you confirm the Iterable backend already accepts the new field names? Either:

  • The endpoint accepts both old and new keys today (preferred — safe rollout), or
  • The backend rolled out support for the new names ahead of this SDK release

If neither is true, every customer's unknown-user session tracking degrades silently the moment they pick up this SDK version — that's a quiet production regression with no client-side error.

Traced this end to end before merging. TL;DR: not a regression, the rename is a strict improvement, and there's a separate pre-existing cross-SDK bug worth filing as its own ticket.

Endpoint contract

Backend route POST /api/unknownuser/events/session is handled by AnonymousUserCriteriaController.trackUnknownSession, which deserializes the body via apiJsResult[UnknownSessionRequest]. Both UnknownSessionRequest and UnknownSessionContext (monolith/app/models/api/UnknownSessionRequest.scala) use plain Json.format[T], no JsonNaming, no aliases, no custom Reads. Case-class fields:

  • totalUnknownSessionCount: Option[Int]
  • lastUnknownSessionTime: Option[Long]
  • firstUnknownSessionTime: Option[Long]

I greped the whole repo to confirm there are no overriding formats for either type.

iOS wire format, before vs after this PR

Field Backend expects iOS pre-PR sends iOS post-PR sends Effect on backend
count totalUnknownSessionCount totalUnknownUserSessionCount totalUnknownSessionCount fix, was always None, now parses
last session time lastUnknownSessionTime lastUnknownUserSession lastUnknownSession unchanged, still None, controller falls back to now
first session time firstUnknownSessionTime firstUnknownUserSession firstUnknownSession unchanged, still None, controller falls back to now

Pre-PR the iOS struct used default Codable, so the property names were the wire keys verbatim. Post-PR the new CodingKeys map renames them on the wire to match Android and Web.

So the rename actually fixes a long-standing silent drop on totalUnknownSessionCount for iOS. The session time fields don't get worse, they were already being dropped under the old names too.

Cross-SDK note (out of scope, but worth flagging)

Android (IterableConstants.SHARED_PREFS_LAST_SESSION = "lastUnknownSession", fed straight into KEY_UNKNOWN_SESSION_CONTEXT) and Web both send lastUnknownSession / firstUnknownSession, which the backend doesn't read — it expects lastUnknownSessionTime / firstUnknownSessionTime and falls back to now when missing. That's a pre-existing platform-wide bug, not something this PR introduces or makes worse. I'll file a separate ticket for it.

- Drop new methods from `UnknownUserManagerProtocol` and retype
  `InternalIterableAPI.unknownUserManager` to the concrete
  `UnknownUserManager` so internal call sites keep the new names
  without breaking external conformers (review #3).
- Revert the keychain key (`itbl_userid_unknown_user`) and the
  UserDefaults sessions wrapper key (`itbl_unknown_user_sessions`)
  back to their pre-PR values; both are local-only with no backend
  contract, so the perpetual migration cost and downgrade footgun
  weren't worth the cosmetic alignment (review #5). Removes the
  associated migrations in `IterableKeychain` / `IterableUserDefaults`
  and the `UserDefaults.standard`-vs-injected-store bug they
  introduced (review #1, #2).
- Keep the inner `IterableUnknownUserSessions` field rename to align
  with Android per ticket #3 — these names are sent to the backend in
  `unknownSessionContext`, so cross-SDK alignment matters here. Uses
  `CodingKeys` for the new payload + a backward-compatible decoder
  for existing on-disk blobs.
- Keep the `dataType → eventType` discriminator rename with one-shot
  read normalization (ticket #5).
- Keep the public `UnknownUserManager` / `IterableIdentityResolution`
  renames with deprecated forwarders.
Tests: wire `UUANormalizationMigrationTests` into the Xcode project
(was orphaned), add the missing `unknownUserUpdate` migration test
(review #4), add deprecated-forwarder coverage for every renamed
`UnknownUserManager` method, and assert the encoded sessions payload
uses the Android-aligned field names while still decoding legacy
blobs.
@joaodordio joaodordio force-pushed the fix/SDK-412-UUA-Naming-inconsistencies branch from 8ff78dd to 177827d Compare June 17, 2026 00:33
Updated CHANGELOG to reflect fixes and changes in the latest release, including a crash fix for CocoaPods and changes to UUA storage encoding.
@joaodordio joaodordio merged commit 9295ccc into master Jun 17, 2026
15 checks passed
@joaodordio joaodordio deleted the fix/SDK-412-UUA-Naming-inconsistencies branch June 17, 2026 00:59
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.

2 participants