Add install attribution matching support#456
Open
yusuftor wants to merge 15 commits into
Open
Conversation
Resolves conflicts across CHANGELOG, Constants/podspec (4.16.0), Package.resolved, and the attribution stack: - AttributionFetcher: keep develop's zero-IDFA UUID comparison - Network.sendToken: adopt develop's throwing AdServicesResponse API - Superwall: rely on develop's config-gated AdServices auto-fire; keep MMP install-match path (hadTrackedAppInstallBeforeConfigure) - AttributionPoster: take develop's refactored token flow and re-graft the .appleSearchAds AttributionMatch tracking onto it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oder - DeviceHelper: guard screenWidth/screenHeight/devicePixelRatio behind #if os(visionOS) (UIScreen.main is meaningless there), matching the existing interfaceStyle pattern. - SK2StoreProduct / ProductPurchaserSK2: add visionOS 26.4 to the billing-plan availability checks so the 26.4-only StoreKit APIs aren't used under a guard that only covered iOS, fixing the visionOS build. - Storage: document that the post-ATT MMP match deliberately re-runs to upgrade the pre-ATT (no-IDFA) match with the real IDFA, not a bug. - EndpointKind: add a per-kind jsonEncoder mirroring jsonDecoder; route all Endpoint bodies through Kind.jsonEncoder so casing follows the backend (core=snake_case, SubscriptionsAPI=camelCase) instead of being hand-picked per call site. Behaviour-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
screenWidth/screenHeight/devicePixelRatio were computed via UIScreen.main (deprecated since iOS 16, main-thread-only) but read from the background async matchMMPInstall, a data race under strict concurrency. Cache them once at init on the main thread, preferring the connected UIWindowScene's screen and falling back to UIScreen.main only when no scene is attached. Safe to cache: the values feed only the MMP install payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
attribution_matchevent and write sharedacquisition_*user attributesChecklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.Greptile Summary
This PR adds install attribution matching support to the iOS SDK. When MMP integrations are configured on the Superwall dashboard, the SDK attempts to match each install against a performance marketing campaign by posting device fingerprint data to a new
/api/matchendpoint, and optionally re-runs the match with a real IDFA once ATT permission is granted.AttributionPoster): fires a typedattribution_matchevent after the existing AdServices token flow completes, and setsacquisition_*user attributes on a successful match.Network.matchMMPInstall): fires on configure, gates duplicate calls with persistent flags and a 7-day attribution window, and retries with IDFA once ATT is granted using a newTrackingPermissionMMPRetryGateSwift actor to prevent concurrent duplicate requests.AttributionMatchInfostruct andSuperwallEvent.attributionMatch(info:)case exposed to SDK consumers via the delegate.Confidence Score: 4/5
The new attribution matching feature is reasonably safe to merge; the main orchestration paths are well-gated by persistent flags and an actor-based concurrency guard, and all previously-raised thread concerns appear resolved in the current code.
The feature touches a non-trivial coordination path: two attribution providers, two independent storage flags, an actor retry gate, and lifecycle notifications for ATT. The implementation correctly handles idempotency, the 7-day attribution window, and race conditions. No new functional defects were found beyond what was already addressed in previous review threads. The gap between the initial match flag and the ATT-retry flag means a failed initial attempt followed by a successful ATT retry still re-issues the initial request on the next launch — a harmless extra network call given server idempotency, but worth keeping in mind if request volume becomes a concern.
Sources/SuperwallKit/Storage/Storage.swift and Sources/SuperwallKit/Superwall.swift warrant the closest attention — they carry the flag-gating logic and the ATT notification coordination that together determine whether attribution fires zero, one, or two times per device lifecycle.
Important Files Changed
Sequence Diagram
sequenceDiagram participant App participant Superwall participant Storage participant Network participant ATT as ATT/PermissionHandler participant Server as MMP Server App->>Superwall: configure() Superwall->>Storage: hasTrackedAppInstall() Superwall->>Storage: recordAppInstall() Superwall->>Storage: shouldAttemptInitialMMPInstallAttributionMatch() alt Eligible (window open, not completed) Storage-->>Superwall: true Superwall->>ATT: checkTrackingPermission() ATT-->>Superwall: status Superwall->>Storage: recordMMPInstallAttributionMatch Storage->>Network: matchMMPInstall(idfa: nil or real) Network->>Server: POST /api/match Server-->>Network: MMPMatchResponse Network->>Superwall: track(AttributionMatch) Network-->>Storage: true Storage->>Storage: save(DidCompleteMMPInstallAttributionRequest) end Note over App,Superwall: User grants ATT permission App->>Superwall: ATT granted Superwall->>Superwall: TrackingPermissionMMPRetryGate.tryBegin() Superwall->>Storage: shouldAttemptTrackingPermissionMMPInstallAttributionMatch() alt Eligible Superwall->>Network: matchMMPInstall(idfa: realIDFA) Network->>Server: POST /api/match (with IDFA) Server-->>Network: MMPMatchResponse Network->>Superwall: track(AttributionMatch) Superwall->>Storage: save(DidCompleteMMPInstallAttributionRequestAfterTrackingPermission) Superwall->>Superwall: TrackingPermissionMMPRetryGate.finish(completed) endComments Outside Diff (3)
Sources/SuperwallKit/Network/Network.swift, line 592-596 (link)info: ["payload": request]passes the fullMMPMatchRequeststruct, which includesidfa,idfv, anddeviceId, into the structured log on every failed/api/matchcall. Once the user grants ATT — exactly when the retry fires — a real IDFA will appear in crash/debug logs captured by any connected logging tool.The existing
sendTokenfailure already follows this same pattern (info: ["payload": token]), so this isn't unique to the new code, but it's worth flagging as the IDFA is a more sensitive identifier than an AdServices token. Consider redacting or omitting the identifier fields from the error payload:Prompt To Fix With AI
Sources/SuperwallKit/Network/Network.swift, line 587-588 (link)matchMMPInstallreturnstrueeven on server-side "no match"matchMMPInstallreturnstruewhenever the HTTP request succeeds (regardless of whetherresponse.matchedistrueorfalse), andfalseonly on a network error. This causesDidCompleteMMPInstallAttributionMatchto be saved even when the server returnsmatched: falsewith no attribution, which permanently prevents the initial match path from retrying on future launches.The tracking-permission retry path (
shouldAttemptTrackingPermissionMMPInstallAttributionMatch) uses a different gate (IsEligibleForMMPInstallAttributionMatch) and will still fire correctly after ATT is granted — so the retry does work. The namingdidCompleteMatchand the stored flagDidCompleteMMPInstallAttributionMatchare ambiguous because they conflate "HTTP request completed" with "attribution was found."Consider renaming the return value and the flag to
DidCompleteMMPInstallAttributionRequestto make the intent explicit, or leaving a comment at thereturn truesite clarifying that success here means "request processed; no need to retry the initial path."Prompt To Fix With AI
Sources/SuperwallKit/Storage/Storage.swift, line 716-731 (link)shouldAttemptInitialMMPInstallAttributionMatchskips only when BOTH conditions are trueThe early-exit guard is:
This means a fresh install (
hadTrackedAppInstallBeforeConfigure == false) will always fall through to check the attribution window — even ifDidCompleteMMPInstallAttributionMatchwas somehow already persisted (e.g. from a previous install that wasn't fully cleaned up). In that edge case, an extra MMP request is fired unnecessarily.A more defensive guard would check
didCompleteMatchindependently ofhadTrackedAppInstallBeforeConfigure:The
hadTrackedAppInstallBeforeConfigure == falsepath always hasdidCompleteMatch == falsein practice, so removing the conjunction doesn't change real-world behavior while making the intent clearer.Prompt To Fix With AI
Reviews (11): Last reviewed commit: "Cache screen metrics at init instead of ..." | Re-trigger Greptile