fix: emit presentation_id, close_reason, cache_key, build_id on paywall events - #448
Open
chroxify wants to merge 2 commits into
Open
fix: emit presentation_id, close_reason, cache_key, build_id on paywall events#448chroxify wants to merge 2 commits into
chroxify wants to merge 2 commits into
Conversation
…ll events Superwall-Android never wrote presentation_id into outgoing paywall event payloads (paywall_page_view, paywall_open, paywall_close, etc.), which breaks any dashboard funnel that correlates a set of page views into one paywall session. Confirmed on live ClickHouse data: the field is 100% empty on Android across every SDK version, vs 0% empty on iOS. - PaywallCloseReason: add a `description` extension mirroring iOS's camelCase close-reason strings (systemLogic, forNextPaywall, webViewFailedToLoad, manualClose, none). - PaywallInfo: add `presentationId`, and serialize it alongside the already-modeled-but-never-emitted close_reason/cache_key/build_id in eventParams(). - Paywall: add a transient `presentationId` field, threaded through getInfo(). - PaywallRequestManager.updatePaywall: mint a fresh UUID presentationId on every getPaywall() call that results in a presentation (fresh fetch, in-flight-task reuse, and content-cache hit), so repeat presentations of a cached paywall get distinct, correlatable IDs. Trade-off: PaywallLoad.Complete/PaywallProductsLoad.* events track before updatePaywall runs, so they won't carry presentation_id — same existing timing gap as experiment_id/variant_id/presentation_source_type. paywall_open/paywall_page_view/paywall_close all fire after updatePaywall and reliably get a stable ID.
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.
Changes in this pull request
presentation_idto every paywall analytics event (paywall_open,paywall_page_view,paywall_close, etc.) — a unique ID minted once per paywall presentation, used by the dashboard to correlate a set of page views into one paywall session.close_reason,cache_key, andbuild_idfields to the same event payload. These already exist onPaywall/PaywallInfobut were never written intoPaywallInfo.eventParams().Root cause
PaywallInfo.eventParams()on Android never includedpresentation_idat all — the field doesn't exist on the model, let alone get serialized. Confirmed on live ClickHouse data:presentation_idis 100% empty on Android across every SDK version, vs 0% empty on iOS. Full comparison, including exact iOS source references, inoutputs/android-ios-page-tracking-comparison-2026-08-06.mdfrom the investigation that led to this PR.Without
presentation_id, the dashboard can't group an Android paywall's page views into a single session — any Android-heavy campaign's page-view funnel silently breaks. This is what surfaced the bug: campaign 98723's page-view data looked broken.close_reason,cache_key,build_idhave the same shape of bug: the data exists on the Android models (Paywall.closeReason,.cacheKey,.buildId) but was never serialized into the outgoing event params, unlike iOS'sPaywallInfo.swift.iOS parity
presentationId: minted asUUID().uuidStringonce per paywall fetch (RawPaywallResponse.swift:19), stored onPaywall, copied intoPaywallInfo, emitted as"presentation_id". Android now mints it inPaywallRequestManager.updatePaywall, which — like iOS re-minting onPaywall.update(from:)— runs on everygetPaywall()call that results in an actual presentation (fresh fetch, in-flight-task reuse, and Android's ownpaywallsByHashcontent-cache hit), so repeat presentations of the same cached paywall get distinct, correlatable IDs.close_reason: emitted ascloseReason.descriptionon both platforms now, using the same camelCase strings (systemLogic,forNextPaywall,webViewFailedToLoad,manualClose,none).cache_key/build_id: emitted directly, matching iOS.Documented trade-off
PaywallLoad.Complete/PaywallProductsLoad.*events track beforeupdatePaywallruns in the call sequence, so those response/product-loading-lifecycle events won't carrypresentation_id. This is not a new inconsistency —experiment_id,variant_id, andpresentation_source_typehave the exact same timing gap today. The events that matter for the reported dashboard bug (paywall_open,paywall_page_view,paywall_close, and everything else tracked once a paywall is actually presented) all fire afterupdatePaywalland reliably get a stable, correct ID. Preload-only calls skipupdatePaywallentirely via existing early-return branches, so preloading correctly doesn't mint a wasted ID.Explicitly out of scope (flagged only, not bundled here)
trigger_session_idfield (// TODO remove in next major update).state/customer_infoto iOS, orintro_offer_eligibility/app_transaction_id/shimmer-load timestamps to Android.These need a product/eng call, not a bundled change in a bug-fix PR.
Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.ktlintin the main directory and fixed any issues.Note on unchecked boxes: this sandbox has no JDK/Android SDK available (
apt-get install openjdk-17-jdk-headlesstimed out), so I could not run./gradlew test,connectedCheck, orktlintFormat/ktlintChecklocally. All changes were reviewed manually against existing, proven patterns in the codebase (named-argument construction everywherePaywall/PaywallInfoare built, so the new trailing fields with defaults are non-breaking;kotlinx.serialization.Transientfields are unaffected by JSON deserialization). New/updated tests are included and should be validated by CI:PaywallInfoTest:eventParams()includespresentation_id/close_reason/cache_key/build_id, and omitspresentation_idwhen null.PaywallRequestManagerTest:getPaywall()sets a non-blankpresentationId, and mints a new one on a second call that hits the request-hash cache (the direct regression test for the reported bug).InternalSuperwallEventTest:PaywallPageView.getSuperwallParameters()carriespresentation_idsourced frompaywallInfo.presentationId.Please run CI (
./gradlew test, ktlint) to confirm before merging.