Skip to content

fix: restore cookie fields across an archive round trip - #979

Open
thomson-t wants to merge 1 commit into
refactor/swift-cookiefrom
fix/swift-cookie-archive-decode
Open

fix: restore cookie fields across an archive round trip#979
thomson-t wants to merge 1 commit into
refactor/swift-cookiefrom
fix/swift-cookie-archive-decode

Conversation

@thomson-t

@thomson-t thomson-t commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #978. This PR's diff is only the third commit.

MPCookie's initWithCoder: passed NSDictionary as the expected class for content, domain and expiration — all three of which are encoded as strings:

[coder encodeObject:self.content forKey:@"content"];                        // NSString in
...
value = [coder decodeObjectOfClass:[NSDictionary class] forKey:@"content"]; // never matches

The class check never matched, so each decode returned nil and every restored cookie carried only its name.

Why it was never caught

No production path archives a cookie — cookies are written as raw sqlite columns, and the only production archives in the SDK are MPUploadSettings and a configuration dictionary.

Cookies are archivable in principle: MPConsumerInfo conforms to NSSecureCoding and encodes its cookies array, and MPConsumerInfoTests.testInstance and testConsumerInfoEncoding exercise that path. Neither asserts the cookie fields, though, and isEqual(toCookie:) compares names only — so testCookie's round-trip assertion passed straight over the loss:

XCTAssertEqualObjects(cookie, deserializedCookie, @"Should have been equal.");

The fix

The three decodes now name NSString. Restoring the values is safe:

  • They pass back through the same setters that escaped them on the way in, and percentEscape() escapes only ; and space while leaving % untouched — so it is idempotent and the values do not double-escape.
  • A restored cookie with a future expiration now reports expired == false instead of true, which is the point of storing it.

Why this is separate from #978

#978 ported the coder verbatim precisely because repairing it changes what a decoded cookie contains — a behaviour change, not a migration. Keeping it here leaves #978 a pure refactor: and gives the fix its own changelog entry.

(Note the sibling defect in #977 was the opposite shape: there, unchecked decodeObjectForKey: under supportsSecureCoding == YES made the decode path throw rather than lose data, so fixing it could not regress any working behaviour and it stayed in that PR.)

Tests

testCookieArchiveRoundTripKeepsOnlyTheName, added by #978 to pin the defect, becomes testCookieArchiveRoundTripPreservesEveryField. Added a name-only case for the cookie that never had the optional fields, which must still restore and still read as expired.

testCookie's Objective-C assertions are unchanged and still pass.

Verification

Rebased with #978 onto workstation/swift-migration after #975 and #977 landed, and re-run end to end:

  • bash Tools/abi-guard.sh check → exit 0
  • trunk check → clean (re-checked after committing)
  • run-analyzer's exact filter chain → 0 surviving warnings
  • Objective-C suite → green; Swift suite → green

🤖 Generated with Claude Code

@thomson-t
thomson-t requested a review from a team as a code owner September 8, 2026 19:43
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes decoded cookie payload for any archived data (e.g. via MPConsumerInfo), though production persistence uses SQLite columns rather than archiving cookies.

Overview
Fixes MPCookie secure decoding so archive/unarchive round trips keep content, domain, and expiration, not just the cookie name.

init(coder:) now decodes those three keys with NSString, matching what encode(with:) writes. The previous decode expected NSDictionary, so the class check always failed and optional fields were dropped on restore—while name-only isEqual still made tests look green.

Tests replace the defect-pinning case with testCookieArchiveRoundTripPreservesEveryField (including future expiration → not expired) and add a name-only round-trip case that still restores and stays expired when expiration is absent. Comments are updated to describe the fix rather than leaving the bug intentional.

Reviewed by Cursor Bugbot for commit f5407ad. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🐦 Swift Migration Progress

Production implementation code at f5407ad308c9 compared with f5048d4b6106.

Area Goal Progress Base This PR Swift SLOC Objective-C remaining Change
Core SDK Short term — in scope ███████▏░░ 70.91% 70.91% 11,513 4,723 ➖ 0.00 pp
Core SDK Long term — all Objective-C █████▏░░░░ 51.31% 51.31% 11,513 10,923 ➖ 0.00 pp
SDK kit infrastructure Short term — in scope ███████▍░░ 73.45% 73.45% 2,031 734 ➖ 0.00 pp
SDK kit infrastructure Long term — all Objective-C ████░░░░░░ 39.81% 39.81% 2,031 3,071 ➖ 0.00 pp
Standalone kits Short term — in scope ▋░░░░░░░░░ 5.84% 5.84% 864 13,921 ➖ 0.00 pp
Standalone kits Long term — all Objective-C ▋░░░░░░░░░ 5.84% 5.84% 864 13,921 ➖ 0.00 pp

Objective-C retained by design: Core SDK 6,200 · SDK kit infrastructure 2,337 · Standalone kits 0.

This PR's code movement

Area Swift lines added Objective-C lines removed
Core SDK 12 0
SDK kit infrastructure 0 0
Standalone kits 0 0
How this is measured
  • Current composition uses production source lines of code (SLOC) from cloc; comments and blank lines are excluded.
  • Short term — in scope excludes the Objective-C the migration will not delete, so 100% is the end of this project: every in-scope implementation gone.
  • Long term — all Objective-C keeps the full denominator. Reaching 100% there means the public API itself becomes Swift, which is a breaking change reserved for a future major release.
  • The gap between the two rows is the retained public/kit contract, runtime-identity, and boundary-glue surface listed in Tools/swift-migration-retained-objc.txt.
  • Retained wrappers keep their Objective-C interface but still shed logic to Swift. That thinning moves the long-term row and the retained figure, not the short-term row.
  • Both revisions are measured with the manifest from the head revision, so a manifest edit does not by itself move the reported change. A retained file this pull request renamed or deleted still counts as retained at the base.
  • Pull request movement uses physical additions/deletions from git diff base...head --numstat; it counts retained files too and is intentionally separate from SLOC totals.
  • Core excludes SDK kit infrastructure and vendored libraries. Standalone kits include only files below Kits/**/Sources.
  • Tests, examples, headers, build outputs, vendored libraries, and the MParticle/Sources Swift overlay are excluded.
  • Objective-C++ (.mm) is included in the Objective-C figures and removed counts.

Generated with cloc 2.10. This report is informational and does not gate migration direction.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📦 SDK Size Impact Report

Measures how much the SDK adds to an app's size (with-SDK minus without-SDK).

Metric Target Branch This PR Change
App Bundle Impact 2.66 MB 2.66 MB +N/A
Executable Impact 848 bytes 848 bytes +N/A
XCFramework Size 6.80 MB 6.80 MB +N/A

➡️ SDK size impact change is minimal.

Raw measurements

Target branch (workstation/swift-migration):

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2808,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2724,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6968}

This PR:

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2808,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2724,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6968}

@thomson-t
thomson-t force-pushed the fix/swift-cookie-archive-decode branch from c9a1add to bda3c27 Compare September 8, 2026 19:55
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

The cookie secure decoder now restores content, domain, and expiration as strings. Archive round-trip tests verify complete cookies and name-only cookies.

Changes

Cookie archive restoration

Layer / File(s) Summary
Cookie decoder field types
mParticle-Apple-SDK-Swift/Sources/DataModel/MPCookie.swift
MPCookiePRIVATE decodes content, domain, and expiration as NSString values.
Archive round-trip validation
mParticle-Apple-SDK-Swift/Test/Utils/MPDataModelTests.swift
Tests verify that complete cookies preserve all fields and that name-only cookies restore with nil optional fields and an expired status.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: brandonstalnaker

Merge Risk: ⚪ Minimal · up to d2a6a

The decoder now restores archived cookie fields, with coverage for complete and name-only cookies. No material merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: restoring cookie fields during archive round trips.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swift-cookie-archive-decode

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: QUIET

Plan: Advanced

Run ID: c77cbcfb-608d-4993-9426-23d576841320

📥 Commits

Reviewing files that changed from the base of the PR and between a93c6ed and 75ec848.

📒 Files selected for processing (2)
  • mParticle-Apple-SDK-Swift/Sources/DataModel/MPCookie.swift
  • mParticle-Apple-SDK-Swift/Test/Utils/MPDataModelTests.swift

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment on lines +101 to +105
/// The deleted Objective-C wrapper passed `NSDictionary` as the expected class for all three,
/// which never matches an `NSString` — so every restored cookie carried only its name. It went
/// unnoticed because nothing in the SDK archives a cookie (persistence writes raw sqlite
/// columns) and `isEqual(toCookie:)` compares names only, so the round-trip assertion in
/// `testCookie` passed over the loss.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove storage-implementation details from this documentation comment.

Keep the encoded-type mismatch and the correction. Remove the storage and prior-test details because they are not needed to document init(coder:).

As per path instructions, avoid internal architecture descriptions.

Source: Path instructions

@thomson-t
thomson-t force-pushed the fix/swift-cookie-archive-decode branch from 75ec848 to d2a6aa2 Compare September 10, 2026 20:10
`MPCookie`'s `initWithCoder:` passed `NSDictionary` as the expected class for `content`, `domain`
and `expiration`, all three of which are encoded as strings. The class check never matched, so the
decode returned nil for each and every restored cookie carried only its name.

It went unnoticed because nothing in the SDK archives a cookie — persistence writes raw sqlite
columns — and `isEqual(toCookie:)` compares names only, so the round-trip assertion in
`testCookie` passed straight over the loss.

The three decodes now name `NSString`. Restoring the values is safe: they pass back through the
same setters that escaped them on the way in, and `percentEscape()` only escapes `;` and space
while leaving `%` untouched, so it is idempotent and the values do not double-escape. A restored
cookie with a future `expiration` now also reports `expired == false` instead of `true`, which is
the point of storing it.

Split out of the migration PR deliberately: the migration ported this verbatim because repairing it
changes what a decoded cookie contains, which is a behaviour change and belongs in its own entry.

`testCookieArchiveRoundTripKeepsOnlyTheName`, which pinned the defect, becomes
`testCookieArchiveRoundTripPreservesEveryField`, plus a name-only case covering the cookie that
never had the optional fields.

Verified: `abi-guard.sh check` exits 0, `trunk check` clean, `run-analyzer` filter yields 0
warnings, both test suites green — including `testCookie`, whose Objective-C assertions are
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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