feat: preserve trait key casing in API responses - #416
Conversation
Trait keys returned by the API were stored lowercased, then re-sent to the identities endpoint on subsequent calls (setTrait, identify without traits), creating duplicate traits server-side for any camelCase key. Store keys verbatim (spaces are still replaced with underscores) and keep getTrait case-insensitive via an exact-then-fallback lookup.
|
@themis-blindfold review |
⚖️ Themis review: 🟠 Fix before mergeThe completed test check passed, but this casing fix does not account for lowercased traits already persisted in client caches, and its fallback lookup changes legacy reads when case-distinct traits coexist. Those paths can still re-send or return the wrong trait value after upgrade.
🟠 Majors
📝 Walkthrough
🧪 How to verify
Product take: This is a solid compatibility repair for camel-case traits, but it needs to cover already-installed clients before it stops duplicate records reliably. 🧭 Assumptions & unverified claims
Camel case needs to survive the trip home, including through the cache · reviewed at bcdc220 |
Lowercased trait keys persisted by pre-9.0.1 SDK versions are dropped on cache load and never re-sent, and a lookup matching no exact key resolves to the first case-insensitive match.
|
@themis-blindfold review |
| }); | ||
| traits.forEach(trait => { | ||
| userTraits[trait.trait_key.toLowerCase().replace(/ /g, '_')] = { | ||
| userTraits[trait.trait_key.replace(/ /g, '_')] = { |
There was a problem hiding this comment.
🟠 Major · ⚡ Quick win
(unverified) Case-insensitive reads are not paired with case-insensitive writes.
Observed: This stores appVersion as a key, while setTrait('appversion', ...) and setTraits({ appversion: ... }) merge the supplied spelling and the identity POST serialises every key. The resulting payload contains both appVersion and appversion.
Predicted: If the identities API treats casing variants as separate traits—the duplicate scenario this change addresses—an application that keeps its previous lowercase write spelling would recreate the duplicate when updating the trait. Resolve an incoming update to an existing case-insensitive key (exact match first) before merging it, and cover both update methods.
There was a problem hiding this comment.
@themis-blindfold working as intended: trait keys are case-sensitive, so a write with a different casing is a different trait, the SDK stores and sends exactly what the caller provides, matching the API and every other SDK.
Case-folding writes would silently rewrite user-chosen keys; the case-insensitive read fallback remains as a backwards-compatibility
There was a problem hiding this comment.
Confirmed. setTrait and setTraits preserve caller-provided keys (flagsmith-core.ts:773, flagsmith-core.ts:794), while identity POST serialises those keys unchanged (flagsmith-core.ts:213). Case variants are intentionally distinct writes; the finding is withdrawn.
⚖️ Themis review: 🟠 Fix before mergeTL;DR: Preserving API casing fixes the re-send path, but callers that continue using the lowercased key with
🟠 Majors
⚖️ Acknowledged
📝 Walkthrough
🧪 How to verify
Product take: This fixes a real trait-integrity problem for camelCase keys, but existing applications commonly retain the lowercased write spelling produced by earlier SDK versions. The remaining write path produces a payload containing both spellings. 🧭 Assumptions & unverified claimsThe identities API examples document A casing fix that still speaks lowercase on writes needs one more translation layer · reviewed at f7d98fc |
|
Confirmed. Case-sensitive trait writes are intentional; no change is needed. The case-insensitive lookup is a read-only compatibility fallback. 🧠 Learning recorded — lands in |
talissoncosta
left a comment
There was a problem hiding this comment.
Looks good to me!!!
Thanks for submitting a PR! Please check the boxes below:
docs/if required so people know about the feature.Changes
duplicate traits (
appVersion+appversion). Keys are stored verbatim;spaces still become underscores.
getTraitmatches exactly, then case-insensitively, so existing reads keep working.How did you test this code?
test/trait-key-casing.test.ts; full suite andtscpass.