Skip to content

feat: add android install attribution matching - #389

Open
yusuftor wants to merge 32 commits into
developfrom
feature/mmp-android
Open

yusuftor wants to merge 32 commits into
developfrom
feature/mmp-android

Conversation

@yusuftor

@yusuftor yusuftor commented Mar 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add Android MMP install matching and attribution_match event support
  • send Android fingerprint fields on /api/match and merge acquisition attributes back into user attributes
  • add Play Install Referrer click-id support for deterministic Android matching

Notes

  • depends on the backend changes in superwall/paywall-next on branch
  • keeps the dashboard setup flow unchanged; Android uses the same Meta integration page per dashboard application

Verification

  • ./gradlew :superwall:compileDebugKotlin

Greptile Summary

This PR adds Android MMP install attribution matching by wiring a new MmpService into the SDK startup flow: on first install (within a 7-day window) the Play Store Install Referrer is queried for a sw_mmp_click_id, a fingerprint payload is posted to /api/match, and the returned acquisitionAttributes are merged into user attributes before an attribution_match event is tracked.

Previous review concerns are well-addressed — clickId is now Long?, JSON parsing uses the safe as? JsonPrimitive cast, the withTimeoutOrNull spin-loop has a delay(50) yield point, double URL-decoding is removed, and endConnection() is called on the backing field rather than the null-guarded computed property.

Key remaining observations:

  • Startup latency on retries — checkForMmpClickId() (up to 5 s) is awaited synchronously before fetchConfiguration(). Because DidCompleteMMPInstallAttributionRequest is only written on a successful match, any launch within the 7-day window where the previous request failed will incur the full Play Store referrer wait again, delaying the configure() completion callback and paywall availability on every such launch.
  • Repeated failure events — when the backend is unreachable the SDK emits attribution_match with reason: "request_failed" on every retry launch, which could generate noisy analytics data for the full attribution window.
  • Layer coupling — mergeMMPAcquisitionAttributesIfNeeded calls Superwall.instance.setUserAttributes() from inside the Network class, bypassing the ApiFactory interface already available in the constructor and making the method untestable via NetworkMock.

Confidence Score: 4/5

Safe to merge with awareness of the startup latency regression on retry launches within the attribution window.

All previously flagged P1 issues are resolved. The three remaining findings are P2 design/quality concerns: startup latency on repeated retries, repeated failure-event noise, and a layer-coupling smell. None block correctness on first install or break existing flows, but the latency issue could noticeably degrade UX on retry launches for up to 7 days post-install.

Superwall.kt (startup critical path), LocalStorage.kt (retry/completion logic), Network.kt (Superwall.instance coupling)

Important Files Changed

Filename Overview
superwall/src/main/java/com/superwall/sdk/Superwall.kt Adds MMP attribution matching to the configure flow; checkForMmpClickId() is awaited synchronously before fetchConfiguration(), adding up to 5 s of startup latency on every retry launch within the 7-day window.
superwall/src/main/java/com/superwall/sdk/network/Network.kt Implements matchMMPInstall with safe JSON helpers and acquisition-attribute merging; reaches up to Superwall.instance from the network layer, breaking layer boundaries and testability.
superwall/src/main/java/com/superwall/sdk/network/MmpService.kt New NetworkService subclass for /api/match; clickId is correctly typed as Long?, serialization config is sensible, and the two-retry policy is appropriate.
superwall/src/main/java/com/superwall/sdk/web/DeepLinkReferrer.kt Previous issues resolved: delay(50) added to spin-loop, double URL-decode removed, endConnection() now called on the backing field, getInstallReferrerParams shared helper extracts common logic cleanly.
superwall/src/main/java/com/superwall/sdk/storage/LocalStorage.kt Adds eligibility/completion gates for MMP attribution; DidCompleteMMPInstallAttributionRequest is only written on success, causing repeated retries (and repeated failure events) across launches within the 7-day window.
superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt Two new Storable<Boolean> cache keys added for MMP attribution state; straightforward and consistent with existing key definitions.
superwall/src/main/java/com/superwall/sdk/analytics/superwall/AttributionMatchInfo.kt New public data class with Provider and Confidence enums; clean, serializable, and well-documented.
superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt Wires MmpService into Network; uses api.subscription.host with version = "/" which correctly resolves to /api/match at the subscription host.
superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt Adds timezoneOffsetSeconds, screenWidth, screenHeight, devicePixelRatio, and appInstalledAtMillis helpers; timezoneOffsetSeconds reuses the existing rawOffset computation already used by secondsFromGMT.
superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt Adds subscriptionHost and enrichmentHost to the Custom environment with baseHost defaults; also surfaces both in toMap() for debugging.
superwall/src/main/java/com/superwall/sdk/network/SuperwallAPI.kt Adds matchMMPInstall to the SuperwallAPI interface with a nullable default parameter; NetworkMock stub correctly returns false.
superwall/src/androidTest/java/com/superwall/sdk/network/NetworkMock.kt Stub implementation added for matchMMPInstall, returning false — consistent with existing mock behaviour.

Sequence Diagram

sequenceDiagram
    participant App
    participant Superwall
    participant LocalStorage
    participant DeepLinkReferrer
    participant PlayStore as Play Store Referrer
    participant Network
    participant MmpService as MmpService (/api/match)

    App->>Superwall: configure()
    Superwall->>LocalStorage: recordAppInstall()
    Superwall->>Superwall: identityManager.configure() [await]
    Superwall->>LocalStorage: shouldAttemptInitialMMPInstallAttributionMatch()
    LocalStorage-->>Superwall: true (first install, within 7-day window)

    Superwall->>DeepLinkReferrer: checkForMmpClickId() [await, ≤5 s]
    DeepLinkReferrer->>PlayStore: startConnection()
    PlayStore-->>DeepLinkReferrer: OK / timeout
    DeepLinkReferrer-->>Superwall: Result<Long> (clickId or null)

    Superwall->>LocalStorage: recordMMPInstallAttributionRequest { ... } [fire-and-forget]
    Superwall->>Superwall: configManager.fetchConfiguration() [await]
    Superwall-->>App: completion(.success)

    Note over LocalStorage,MmpService: Fire-and-forget coroutine
    LocalStorage->>Network: matchMMPInstall(clickId)
    Network->>MmpService: POST /api/match
    MmpService-->>Network: MmpMatchResponse
    alt matched == true
        Network->>Superwall: setUserAttributes(acquisitionAttributes)
        Network->>Superwall: track(AttributionMatch(matched=true))
        Network-->>LocalStorage: true → write DidCompleteMMPInstallAttributionRequest
    else network error
        Network->>Superwall: track(AttributionMatch(reason=request_failed))
        Network-->>LocalStorage: false → flag NOT written (retry next launch)
    end
Loading

Comments Outside Diff (1)

  1. superwall/src/main/java/com/superwall/sdk/analytics/superwall/AttributionMatchInfo.kt, line 121-123 (link)

    P2 APPLE_SEARCH_ADS is dead code in the Android SDK

    Apple Search Ads is an iOS-only advertising platform and has no equivalent on Android. Shipping this variant in the Android SDK is misleading — consumer code could construct an AttributionMatchInfo with Provider.APPLE_SEARCH_ADS, but it will never be produced by the SDK itself. Consider removing the variant or annotating it as reserved for future cross-platform use to avoid confusion.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: superwall/src/main/java/com/superwall/sdk/analytics/superwall/AttributionMatchInfo.kt
    Line: 121-123
    
    Comment:
    **`APPLE_SEARCH_ADS` is dead code in the Android SDK**
    
    Apple Search Ads is an iOS-only advertising platform and has no equivalent on Android. Shipping this variant in the Android SDK is misleading — consumer code could construct an `AttributionMatchInfo` with `Provider.APPLE_SEARCH_ADS`, but it will never be produced by the SDK itself. Consider removing the variant or annotating it as reserved for future cross-platform use to avoid confusion.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/Superwall.kt
Line: 698-705

Comment:
**`checkForMmpClickId()` blocks `fetchConfiguration()` on every retry launch**

`checkForMmpClickId()` is `await`ed in-line (up to 5 seconds) before `fetchConfiguration()` is called. Because `DidCompleteMMPInstallAttributionRequest` is only written on a successful match, a device that encounters a transient network failure on first install will retry the Play Store referrer lookup on _every_ subsequent launch within the 7-day window — each time adding up to 5 seconds of delay before `fetchConfiguration()` completes and the `configure()` completion callback fires.

On devices where the Play Store referrer service is unavailable the full 5-second timeout will always elapse, making this a recurring startup regression for the entire attribution window.

Consider launching the referrer lookup concurrently with `identityManager.configure()` and passing the result to the deferred attribution request:

```kotlin
// Option: run referrer lookup in parallel with identityManager.configure()
val clickIdDeferred = async {
    DeepLinkReferrer({ context }, ioScope).checkForMmpClickId().getOrNull()
}
dependencyContainer.identityManager.configure()

if (dependencyContainer.storage.shouldAttemptInitialMMPInstallAttributionMatch(...)) {
    dependencyContainer.storage.recordMMPInstallAttributionRequest {
        dependencyContainer.network.matchMMPInstall(clickIdDeferred.await())
    }
}
dependencyContainer.configManager.fetchConfiguration()
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/network/Network.kt
Line: 396-423

Comment:
**`Network` layer reaches up to the `Superwall` singleton**

`mergeMMPAcquisitionAttributesIfNeeded` calls `Superwall.instance.setUserAttributes(attributes)` directly from inside the `Network` class. This:

1. Creates a circular dependency: `Network` → `Superwall` → `DependencyContainer` → `Network`.
2. Breaks testability — the `NetworkMock` stub in `NetworkMock.kt` can't exercise this side-effect in isolation.
3. Could theoretically cause issues if `Superwall.instance` is accessed before `Superwall.configure()` completes (e.g., in tests or unusual host app setups).

The `Network` constructor already receives `factory: ApiFactory` which exposes `factory.identityManager`. Merging attributes through that interface (or surfacing the merged map as a return value from `matchMMPInstall` so the caller in `Superwall.kt` can apply them) would stay within the established layer boundaries.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/storage/LocalStorage.kt
Line: 213-226

Comment:
**Repeated `attribution_match` failures within the attribution window**

`DidCompleteMMPInstallAttributionRequest` is only written when `matchRequest()` returns `true` (i.e., a successful server match). When the backend is unreachable or returns an error, the flag stays `false` and `shouldAttemptInitialMMPInstallAttributionMatch` will return `true` again on the next launch. Combined with the startup delay noted above, this means:

- An `attribution_match` event with `reason: "request_failed"` can be emitted on every app launch for up to 7 days.
- Analysts looking at attribution funnels will see multiple events per install on unstable connections.

If retry-on-failure is intentional, consider capping the total retry count (persisted in storage) to reduce noise, or writing the `DidCompleteMMPInstallAttributionRequest` flag after a configurable number of failures.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix android referrer and test network mo..." | Re-trigger Greptile

Comment thread superwall/src/main/java/com/superwall/sdk/network/MmpService.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/network/Network.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/web/DeepLinkReferrer.kt
Comment thread superwall/src/main/java/com/superwall/sdk/web/DeepLinkReferrer.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/storage/LocalStorage.kt
Comment thread superwall/src/main/java/com/superwall/sdk/web/DeepLinkReferrer.kt Outdated
@ianrumac
ianrumac force-pushed the feature/mmp-android branch from 100d7e4 to 7668d57 Compare August 27, 2026 13:16
yusuftor and others added 9 commits October 8, 2026 17:56
…, keep acquisition attributes across identify

- Only fire the install match once config has attribution_options.mmp.enabled,
  like iOS. Eligibility is still recorded at launch, and a match skipped while
  tracking is off starts when the app opts back in.
- Redeem the install-referrer code once per install. Play keeps the referrer
  for the life of the install, so it was redeemed on every launch.
- Merge the cached acquisition_* attributes inside the identity reset itself,
  so identifying as a different user no longer drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yusuftor
yusuftor force-pushed the feature/mmp-android branch from 7668d57 to fb2d95b Compare October 8, 2026 16:07
yusuftor and others added 2 commits October 8, 2026 18:21
Adds `SuperwallOptions.adConsent` and `Superwall.instance.adConsent`
(`AdConsent(adUserData, adPersonalization)` of `ConsentStatus`), reported
as the `adUserDataConsent` / `adPersonalizationConsent` device attributes
("granted" / "denied") and as `ad_consent` in config attributes. Defaults
to granted; both report denied when eventTrackingBehavior is NONE.
Changing it at runtime re-sends device and config attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…curately

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yusuftor yusuftor added the docs-required Ships a customer-facing change that needs a superwall/docs update label Oct 8, 2026
@yusuftor

yusuftor commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

📚 Docs required

What changed for customers: Android apps now get install attribution matching (an attribution_match event plus acquisition_* user attributes) and a new SuperwallOptions.adConsent / Superwall.instance.adConsent option for reporting ad consent, which defaults to granted and is reported as denied under eventTrackingBehavior = NONE.

Why this needs docs: adds a new SuperwallOptions field (adConsent) to the option list the SDK reference enumerates, with a compliance prerequisite (EEA/UK/CH apps must set it from their consent flow); it also brings install attribution to a new platform, while docs still say attribution is iOS only. This is one feature across superwall/Superwall-iOS#544 and #389; one docs PR should cover both.

Coverage today: absent for adConsent / AdConsent / ConsentStatus / adUserDataConsent (zero hits in superwall/docs); stale at content/docs/integrations/meta-ads.mdx#L25 ("Meta Ads attribution is iOS only for now"), and content/docs/android/sdk-reference/SuperwallEvent.mdx has no attribution_match — content/docs/ios/sdk-reference/SuperwallOptions.mdx (https://superwall.com/docs/ios/sdk-reference/SuperwallOptions), content/docs/android/sdk-reference/SuperwallOptions.mdx (https://superwall.com/docs/android/sdk-reference/SuperwallOptions), content/docs/integrations/meta-ads.mdx (https://superwall.com/docs/integrations/meta-ads)

Without a docs page this change also gets no changelog entry: superwall/docs publishes a daily GitHub Release that superwall.com renders at /changelog, and every entry must link to a live docs page. Undocumented means invisible to customers.

Prompt for the docs agent — run in superwall/docs
Document a feature that shipped across two SDK PRs, treated as one change:
superwall/Superwall-iOS#544 (Add ad consent option for Google Ads conversion uploads) and
superwall/Superwall-Android#389 (feat: add android install attribution matching).

What shipped, in customer terms:
1. Ad consent option (iOS and Android). New `SuperwallOptions.adConsent`, also settable at runtime via
   `Superwall.shared.adConsent` (iOS) / `Superwall.instance.adConsent` (Android). Type is
   `AdConsent(adUserData:adPersonalization:)`, each a `ConsentStatus` (iOS `.granted` / `.denied`,
   Android `GRANTED` / `DENIED`). Both default to granted. AdConsent is immutable; change it by assigning a
   new value, which re-sends device attributes immediately. Superwall forwards it with the conversions it
   uploads to ad networks such as Google Ads. Reported as the device attributes `adUserDataConsent` and
   `adPersonalizationConsent` ("granted" / "denied"). Rules: `eventTrackingBehavior` = none reports both as
   denied; on iOS, when App Tracking Transparency is denied or restricted, personalization is reported as
   denied (notDetermined follows the option; the SDK never prompts for ATT).
   Apps with users in the EEA, UK or Switzerland must set it from their consent flow.
2. Android install attribution matching. Android now matches an install to the ad click that led to it,
   like iOS already does: once per install, only within 7 days of install, only when the dashboard has turned
   matching on for the app, never blocking startup, using the Play install referrer click id when present.
   It tracks an `attribution_match` SuperwallEvent (AttributionMatchInfo: provider, matched, source,
   confidence high/medium/low, matchScore, reason) and merges `acquisition_*` attributes into user
   attributes, usable in chart breakdowns/filters and audiences, carried over after identify/reset.
   Skipped while eventTrackingBehavior is NONE; runs if tracking is turned back on.
   Also a fix: web checkout codes passed through the Play install referrer are now redeemed (once, on first
   launch after install).

Setup or prerequisites a customer must complete:
- EEA/UK/CH apps: set `adConsent` from their consent management flow at configure time and update it when
  the user changes consent.
- Android attribution: update to the Android SDK version that ships #389 (check its CHANGELOG/release tag)
  and ship a new build; new installs only.

Where it belongs:
- content/docs/ios/sdk-reference/SuperwallOptions.mdx — add `adConsent` to the signature (~line 31) and
  TypeTable (near `eventTrackingBehavior`, ~line 78); explain defaults, the ATT and `.none` rules.
- content/docs/android/sdk-reference/SuperwallOptions.mdx — same for Kotlin/Java signatures (~lines 23, 42)
  and TypeTable (~line 74).
- content/shared/configuring/using-superwalloptions.mdx — short "Ad consent" section with the EEA/UK/CH
  requirement and an example of setting it from a consent flow (iOS + Android tabs).
- content/docs/ios/sdk-reference/Superwall.mdx and content/docs/android/sdk-reference/Superwall.mdx — add
  the runtime `adConsent` property if those pages enumerate runtime properties.
- content/docs/integrations/meta-ads.mdx line 25 — "Meta Ads attribution is iOS only for now" is now stale;
  add the Android SDK minimum version and the Play install referrer note. Adjust the Warning at ~line 30
  (mentions only SDK 4.16.0).
- content/docs/integrations/mmp.mdx — mention Android support and the ad consent option (Google Ads is listed
  as "coming soon" at line 20; add consent guidance where Google Ads is or will be documented).
- content/docs/android/sdk-reference/SuperwallEvent.mdx — add `AttributionMatch` / `attribution_match`
  (iOS SuperwallEvent.mdx also lacks it; add there too). tracking-analytics.mdx already lists the event.
- content/docs/ios/changelog.mdx and content/docs/android/changelog.mdx — entries once the SDK versions ship.

Source of truth — read these before writing:
- gh pr diff 544 --repo superwall/Superwall-iOS --patch
- gh pr diff 389 --repo superwall/Superwall-Android --patch
- iOS: Sources/SuperwallKit/Config/Options/AdConsent.swift, Sources/SuperwallKit/Config/Options/SuperwallOptions.swift,
  Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift, CHANGELOG.md
- Android: superwall/src/main/java/com/superwall/sdk/config/options/AdConsent.kt,
  .../analytics/superwall/AttributionMatchInfo.kt, .../analytics/attribution/MMPAttributionManager.kt, CHANGELOG.md

Constraints:
- Match the voice and structure of neighbouring pages under content/docs/**.
- Use the Fumadocs TypeTable for parameters and types; never <ParamTable>.
- If you add a page, add it to the folder's meta.json.
- Run `bun run build:cf` and `bun test` before opening the PR.
- Open a PR against superwall/docs referencing superwall/Superwall-iOS#544 and superwall/Superwall-Android#389. Do not deploy.

Flagged by the docs-required skill. If this is wrong, remove the label and say why in a reply so the skill's calibration set can be corrected.

@yusuftor

yusuftor commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@pullfrog

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

Two paths can permanently lose data the PR is meant to deliver: a web-checkout code from the install referrer is marked redeemed even when the redemption fails, and a fresh install killed before config resolves on first launch is never marked eligible for an attribution match.

Reviewed changes

Reviewed the full PR: Android MMP install-attribution matching, the AdConsent option, and the install-referrer fixes.

  • Install attribution matching — new MMPAttributionManager, MmpService (/api/match on a new mmpHost), a config gate via attribution_options.mmp.enabled, and an attribution_match event.
  • Install-scoped acquisition_* attributes — the match payload is cached in MMPAcquisitionData and merged back into user attributes on Reset/FullReset, so it survives identify and reset.
  • Eligibility gating — IsEligibleForMMPInstallAttributionMatch / DidCompleteMMPInstallAttributionRequest limit matching to fresh installs within 7 days. Upgraders are excluded.
  • Install referrer — DeepLinkReferrer now parses the real query string (the old parser only ever produced an item key, so code was never found), memoizes the params behind a mutex, and exposes checkForMmpClickId(). The redeemer now redeems a referrer code once per install.
  • Ad consent — SuperwallOptions.adConsent / Superwall.instance.adConsent are reported as adUserDataConsent / adPersonalizationConsent device attributes and forced to denied under EventTrackingBehavior.NONE.
  • Network environment — Custom gains subscriptionHost, enrichmentHost and mmpHost parameters.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread superwall/src/main/java/com/superwall/sdk/web/WebPaywallRedeemer.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/Superwall.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/web/DeepLinkReferrer.kt
Comment thread superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt Outdated
Comment thread superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt Outdated
…sent re-sends

ConsentStatus clashes with FirebaseAnalytics.ConsentStatus in apps that use
both. Each adConsent assignment launched its own coroutine, so an older
snapshot could be tracked after a newer one; re-sends now run one at a time
and drop superseded assignments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No new issues in this delta, just one CHANGELOG nit inline. The five threads from the previous review are still open because none of the files they point at changed.

Reviewed changes

Reviewed the one commit added since the last Pullfrog review (08901e0 → c665202).

  • Renamed ConsentStatus to AdConsentStatus across AdConsent.kt, the tests and the CHANGELOG. Nothing in the repo still uses the old name.
  • Serialized adConsent re-sends: the Superwall.adConsent setter now bumps an AtomicLong generation and runs the re-send under a Mutex. Any run whose generation is no longer current is dropped, and the snapshot is taken inside the lock, so an older consent can't be tracked after a newer one.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread CHANGELOG.md Outdated
github-actions Bot and others added 2 commits October 8, 2026 16:44
A consent change made while eventTrackingBehavior was NONE was never sent,
and turning tracking back on only re-sent config attributes, so Superwall
kept the old value until the next session. The re-send now lives in
AdConsentPublisher, used by both setters, with tests for the events it
sends and for dropping superseded sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues in this delta. The six threads from earlier Pullfrog reviews are still open, including the CHANGELOG "a AdConsentStatus" nit. None of the files they point at changed.

Reviewed changes

Reviewed the commit added since the last Pullfrog review (c665202 → 688a8af).

  • Extracted AdConsentPublisher: the generation counter and mutex that serialize ad consent re-sends moved out of Superwall into an internal class. It keeps the same guarantee: a send that was superseded while waiting for the lock is dropped, and the snapshot is taken inside the lock. This also gets rid of the out-of-order imports at the top of Superwall.kt.
  • Re-sent ad consent when tracking comes back on: the eventTrackingBehavior setter now remembers whether the previous value was NONE. If it was, it calls adConsentPublisher.publish(), which sends DeviceAttributes plus config attributes, so a consent change made while tracking was off still reaches Superwall. Every other transition sends only config attributes, as before.
  • Added AdConsentPublisherTest: covers the send order and dropping a superseded publish.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

github-actions Bot and others added 2 commits October 8, 2026 16:56
- Record install-match eligibility before config is fetched, so a first
  launch killed while offline is still matched on the next launch.
- Mark an install-referrer code redeemed only once the server answered,
  so a failed request is retried on the next launch.
- Stop waiting for the install referrer once the Play connection failed
  for good, instead of holding the lock for the full timeout.
- Keep Custom environments on their previous *.superwall.dev hosts and
  restore the 4-argument constructor with @jvmoverloads.
- Fix the MMPAcquisitionData KDoc placement and a CHANGELOG article.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. All six earlier Pullfrog threads are fixed in 9eda6ad, and the author has already resolved them.

Reviewed changes

Reviewed the one commit added since the last Pullfrog review (688a8af → 9eda6ad). The 5d8047d coverage badge commit between them changes no code.

  • Moved the MMP eligibility check to the start of startup: shouldAttemptInitialMMPInstallAttributionMatch now runs right after DidTrackAppInstall is read and before recordAppInstall. There is no suspension point between the two writes anymore, so if the first launch is killed before config arrives, the install still counts as eligible. The request is still gated on the stored shouldMatchInstall result and still waits for config and identity.
  • Stopped marking failed referrer redemptions as done: RedeemedInstallReferrerCode is written only when the code is in LatestRedemptionResponse.allCodes. That list is @Transient and built from the server's codes, and it is only stored on a successful /redeem. A transport failure is therefore retried on the next launch. Added a test for the success path and a check on the failure path that nothing is written.
  • Stopped the referrer wait early when the connection fails: the wait in getInstallReferrerParams returns as soon as referrerClient is null. A failed Play connection no longer keeps referrerMutex locked for the full 30s or 5s timeout.
  • Restored the old Custom hosts and added @JvmOverloads: subscriptionHost, enrichmentHost and mmpHost now default to the *.superwall.dev hosts that Custom used before. The 4-argument constructor still works from Java.
  • Smaller fixes: moved the MMPAcquisitionData KDoc back onto its own object, and changed "a AdConsentStatus" to "an" in the CHANGELOG.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

github-actions Bot and others added 3 commits October 8, 2026 17:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… queue

Any behavior other than ALL clears the events queue, which can hold a
just-queued consent update. Re-send device and config attributes after such
a change, once the queue has applied it, as iOS does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. There are no open Pullfrog threads.

Reviewed changes

Reviewed the one code commit added since the last Pullfrog review (9eda6ad → c313eaf). c4773b9 only changed docs and 828b3ba only updated the coverage badge.

  • Returned a Job from EventsQueue.setTrackingBehavior: the method now returns the job it launches on the queue's single-threaded dispatcher. That job updates trackingBehavior and, for anything other than ALL, clears the queued events. Nothing else calls the method or mocks it, and the repo has no .api dump that would need updating.
  • Re-sent ad consent whenever a tracking change can drop it: the eventTrackingBehavior setter now calls adConsentPublisher.publish(after = queueUpdated) when the value changes or stays SUPERWALL_ONLY. Turning tracking off (NONE) still sends nothing. A setter call that leaves the value at ALL still sends only config attributes. So an ALL → SUPERWALL_ONLY change that clears a queued consent update now sends that consent again.
  • Made AdConsentPublisher.publish wait on after: the send joins after before it takes the mutex, so the re-sent consent can't be enqueued before the queue is cleared. The join also means it reads the updated trackingBehavior. If a later adConsent publish supersedes this one, it is still enqueued after the clear, because the queue's dispatcher runs jobs in the order they were submitted.
  • Added a test for after: checks that nothing is tracked until the job passed as after completes, and that both attribute events are sent afterwards.

Tests were not run here because a Gradle run doesn't finish in this sandbox, so these findings come from reading the code.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

github-actions Bot and others added 4 commits October 9, 2026 09:34
When the developer never sets adConsent, report the purpose consents an
IAB TCF banner stored in the app's default SharedPreferences when GDPR
applies (ad user data = purposes 1 and 7, ad personalization = 3 and 4).
The developer's value still wins once assigned, even to the default, and
NONE still denies everything. A new adConsentSource device attribute says
which source supplied the values, and a banner change that alters the
reported consent re-sends it through AdConsentPublisher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ranted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. There are no open Pullfrog threads.

Reviewed changes

Reviewed the two commits added since the last Pullfrog review (c313eaf → 478a3a9). bae64bc and 2d0e1b8 only updated the coverage badge.

  • Read ad consent from an IAB TCF banner: the new TcfConsentReader reads IABTCF_gdprApplies and IABTCF_PurposeConsents from the app's default SharedPreferences. Ad user data needs purposes 1 and 7, and ad personalization needs purposes 3 and 4. That matches Google's TCF to consent mode mapping. It returns nothing when GDPR doesn't apply or no purposes are stored.
  • Picked a consent source: reportedAdConsent uses the developer's value if adConsent was ever assigned (tracked by the new isAdConsentSet, which the custom setter flips and which is also sent as ad_consent_set), then the banner, then granted. NONE still denies everything. The source goes out as the new adConsentSource device attribute and is part of the DeviceHelper template fingerprint, so a change in the source alone rebuilds the cached template.
  • Re-sent consent when the banner changes: configure starts watching the banner on ioScope. The listener is held strongly and registered only once. It publishes only if the reported consent actually changes, so nothing is sent while the developer's value takes precedence or while tracking is NONE. The publish then reads the banner again when it builds the device attributes.
  • Updated docs and tests: the CHANGELOG and KDoc now describe the TCF fallback. Added TcfConsentReaderTest (the mapping, source precedence, and Robolectric checks of the reader and listener) and banner cases in IdentityActorIntegrationTest.

I didn't run the tests because Gradle doesn't finish in this sandbox. These findings come from reading the code.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

yusuftor and others added 3 commits October 9, 2026 14:50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange

A device-attributes event reads consent when it is built and is tracked
later; a banner or setter change in between was published first and then
overtaken by the stale send. After tracking device attributes, compare the
consent they carried with the current one and re-send when it differs, as
iOS does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new reconcile step can loop forever. If building the device template fails, every re-send reports no consent, so it immediately triggers another re-send.

Reviewed changes

Reviewed the one code commit added since the last Pullfrog review (478a3a9 → 48915ef). 300d6c3 only changed docs and 942b157 only updated the coverage badge.

  • Re-checked consent after each DeviceAttributes track: Superwall.track now calls reconcileAdConsentAfterPublish after enqueueing any InternalSuperwallEvent.DeviceAttributes. Unless tracking is NONE, it compares the sent map with deviceHelper.currentAdConsent and calls adConsentPublisher.publish() if they differ. This catches a send that read consent before a banner or setter change and finished after it. It also covers sends that don't go through the publisher: ConfigState, AppSessionManager, setPlatformWrapper and setInterfaceStyle.
  • Added ReportedAdConsent.toAttributes() / differsFrom(sent) in AdConsent.kt, and made DeviceHelper.currentAdConsent internal. Added an AdConsentTest case for the comparison.

The re-entry from inside AdConsentPublisher doesn't deadlock. The nested publish() launches a new coroutine, and that coroutine waits for the mutex. It also doesn't break the ordering against the events-queue clear, because the queue's serial dispatcher still runs the clear job first.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread superwall/src/main/java/com/superwall/sdk/config/options/AdConsent.kt Outdated
yusuftor and others added 2 commits October 9, 2026 15:34
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hout it

An empty device template carries no consent keys, so reconciling against it
re-sent on every send for as long as the template failed to build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The empty-template reconcile loop from the last Pullfrog review is fixed, and that thread is already resolved.

Reviewed changes

Reviewed the one code commit added since the last Pullfrog review (48915ef → 348bf68). d76a49b only changed docs.

  • Stopped re-sending consent after a send with no consent keys: ReportedAdConsent.differsFrom now returns false when sent has none of adUserDataConsent, adPersonalizationConsent or adConsentSource. A failed getTemplateDevice() returns emptyMap(), so it no longer triggers another publish(), and the loop is gone. A template that does carry the keys is still compared on all three, so a stale send is still caught.
  • Updated AdConsentTest: a map with only deviceModel and an empty map now both count as not differing. The stale-value and different-source cases still count as differing.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

github-actions Bot and others added 3 commits October 9, 2026 13:54
2.8.5 shipped from develop without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-required Ships a customer-facing change that needs a superwall/docs update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants