Skip to content

refactor: move webview JSON field reading to Swift - #929

Merged
BrandonStalnaker merged 3 commits into
workstation/swift-migrationfrom
refactor/delete-migrated-objc-wrappers
Sep 4, 2026
Merged

BrandonStalnaker merged 3 commits into
workstation/swift-migrationfrom
refactor/delete-migrated-objc-wrappers

Conversation

@nickolas-dimitrakas

@nickolas-dimitrakas nickolas-dimitrakas commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Background

Continues the wrapper-deletion audit from #918: no further dead files or privatization candidates remain, so the series continues via extraction. MPConvertJS.m (webview→SDK-type JSON converter) was the best-shaped untouched candidate.

What Has Changed

  • Added MPConvertJSFields.swift reading commerceEventActionForJSValue:, promotionFieldsFromJSON:, transactionAttributesFieldsFromJSON:. SDK-type construction (MPCommerceEvent, MPPromotion, etc.) stays in the .m. 307 → 262 SLOC.
  • Preserved the JS-action↔SDK-action remap (orders differ), including returning nil for unknown/undefined values so the .m keeps its MPILogError + AddToCart fallback.
  • action parameter typed Any? (not NSNumber *) since the original read a string-encoded value via -intValue with no enforced type.
  • .m now assigns every field unconditionally — safe because every target setter already removes its key on nil.

Validation

  • abi-guard PASSED
  • trunk check clean (caught 6 trailing-comma issues in new tests, fixed)
  • ObjC 1004 / Swift 640 tests green; new Swift coverage 11/11
  • tvOS: CI only

Stack

Depends on #927.

Checklist

  • Self-review completed
  • Tests added or updated
  • Tested locally

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🐦 Swift Migration Progress

Production implementation code at 8c3eea8da050 compared with 7982b6e1ad8c.

Area Goal Progress Base This PR Swift SLOC Objective-C remaining Change
Core SDK Short term — in scope █████▏░░░░ 51.43% 51.87% 7,984 7,407 🚀 +0.44 pp
Core SDK Long term — all Objective-C ███▊░░░░░░ 37.32% 37.67% 7,984 13,209 🚀 +0.35 pp
SDK kit infrastructure Short term — in scope ██████▊░░░ 66.96% 66.96% 2,021 997 ➖ 0.00 pp
SDK kit infrastructure Long term — all Objective-C ████░░░░░░ 39.95% 39.95% 2,021 3,038 ➖ 0.00 pp
Standalone kits Short term — in scope ▏░░░░░░░░░ 1.06% 1.06% 152 14,218 ➖ 0.00 pp
Standalone kits Long term — all Objective-C ▏░░░░░░░░░ 1.06% 1.06% 152 14,218 ➖ 0.00 pp

Objective-C retained by design: Core SDK 5,802 · SDK kit infrastructure 2,041 · Standalone kits 0.

This PR's code movement

Area Swift lines added Objective-C lines removed
Core SDK 119 75
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.

@nickolas-dimitrakas nickolas-dimitrakas self-assigned this Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 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.47 MB 2.48 MB +4 KB
Executable Impact 848 bytes 848 bytes +N/A
XCFramework Size 6.82 MB 6.82 MB +4 KB

➡️ 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":2616,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2532,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6984}

This PR:

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2620,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2536,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6988}

@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches webview→commerce event conversion on a hot path; the guarded-setter fix changes when empty promotions/transactions are omitted, which could affect forwarded analytics if clients relied on the accidental empty-dict behavior.

Overview
Moves webview-bridge JSON parsing for commerce event actions, promotion fields, and transaction attributes into new Swift MPConvertJSFields, while MPConvertJS.m still builds MPCommerceEvent, MPPromotion, and related SDK types.

MPConvertJS.m now calls Swift for the JS→SDK commerce action remap (including nil for unknown values so logging + AddToCart fallback stay the same) and for typed field extraction with strict NSString/NSNumber guards matching the old ObjC checks.

Behavior fix: promotion and transaction attribute setters are only invoked when a value is actually present, so invalid/all-null payloads do not touch lazy attribute dictionaries and still yield nil dictionaryRepresentation instead of an empty dict that would get forwarded as real data.

Adds Swift unit tests for MPConvertJSFields and ObjC regression tests for empty promotion/transaction conversion; Xcode project includes the new test target file.

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

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b1e001a. Configure here.

Comment thread mParticle-Apple-SDK/Utils/MPConvertJS.m Outdated
@nickolas-dimitrakas
nickolas-dimitrakas force-pushed the refactor/delete-migrated-objc-wrappers branch from b1e001a to 49ef53a Compare September 3, 2026 19:28
Base automatically changed from refactor/attribute-projection-parsing-to-swift to workstation/swift-migration September 3, 2026 19:30
Continues the "move migrated types over" work after #918. MPConvertJS is the
webview bridge's JSON-to-SDK-type converter; every one of its public methods
returns an SDK type, so construction stays in the .m per
docs/swift-migration/CONVERSION-RECIPE.md and only the reading moves.
MPConvertJS.m goes from 307 to 262 sloc.

Moved to Swift:

- +commerceEventActionForJSValue:. MPJSCommerceEventAction and
  MPCommerceEventAction do not share an order, so this is a genuine remap, not
  a cast: AddToWishList is 9 on the way in and 2 on the way out, and Checkout
  is 3 in and 4 out. Every pair is asserted individually rather than trusted.
  Swift returns nil for a value the bridge does not define — including its own
  Unknown (0) — so the .m keeps the MPILogError and the AddToCart fallback at
  the same call site the switch's default: had.
- promotionFieldsFromJSON: and transactionAttributesFieldsFromJSON:, which
  replace twelve -isKindOfClass: guarded reads.

The parameter of the action mapping is Any? rather than NSNumber* on purpose:
the ObjC declared NSNumber* but read the value with -intValue and nothing
enforced the type, so a string-encoded action worked. That is preserved through
MPJSONCoercion and pinned by a test.

The .m now assigns every field unconditionally. That is safe because each
target setter removes its key when handed nil (MPPromotion.m:176,
MPTransactionAttributes.m:186 and friends) and the keys are absent on a
freshly initialised instance, so assigning nil and not assigning are the same
thing. Checked rather than assumed — all twelve properties have custom setters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nickolas-dimitrakas
nickolas-dimitrakas force-pushed the refactor/delete-migrated-objc-wrappers branch from 49ef53a to 4b44d2d Compare September 3, 2026 19:30

@BrandonStalnaker BrandonStalnaker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MPPromotion’s setters always go through the lazy attributes / beautifiedAttributes getters. Those allocate empty dictionaries, then remove a key that was never there. dictionaryRepresentation returns _attributes as-is (it does not treat empty as nil):

Before this PR, a promotion with no valid string fields left _attributes nil, so [promotion dictionaryRepresentation] was nil and MPPromotionContainer skipped it. After, it is @{}. The container then ships that empty object (and the same for beautified output):

MPPromotion and MPTransactionAttributes both lazily allocate their
attributes/beautifiedAttributes dictionaries on first touch, including
from a setter's nil-removal branch. Assigning every field unconditionally
still triggers that allocation when the value is nil, so a promotion or
transaction with no valid fields ended up with an empty (non-nil)
dictionaryRepresentation instead of nil -- which MPPromotionContainer no
longer skips, shipping an empty promotion where it previously shipped
nothing.

Restores the per-field nil guard removed when this method moved to
Swift-backed field reading, and adds a regression test for both types.

Addresses Brandon's review on this PR.
@nickolas-dimitrakas

Copy link
Copy Markdown
Contributor Author

Good catch, @BrandonStalnaker — fixed. Restored the per-field if guard in promotion: so a nil value never touches the lazy attributes/beautifiedAttributes dictionary, keeping dictionaryRepresentation nil (not @{}) when nothing valid came through.

transactionAttributes: had the identical unconditional-assignment pattern and the identical lazy-dictionary setter, so it had the same bug — fixed that one too, same guard.

Added a regression test for each asserting dictionaryRepresentation is nil with no valid fields. Pushed as b8f17ca.

if (fields.shipping) on an NSNumber* pointer trips the static analyzer's
osx.NumberObjectConversion check, same as the string-field guards fixed
in b8f17ca. Use an explicit != nil check for the three NSNumber fields.
@BrandonStalnaker
BrandonStalnaker merged commit ae9a889 into workstation/swift-migration Sep 4, 2026
81 checks passed
@BrandonStalnaker
BrandonStalnaker deleted the refactor/delete-migrated-objc-wrappers branch September 4, 2026 14:48
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