Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.

## Unreleased

## Fixes
- Paywall analytics events (`paywall_open`, `paywall_page_view`, `paywall_close`, etc.) now include a `presentation_id`, a unique identifier minted for each paywall presentation. Previously this field was always empty on Android, which broke dashboard funnels that correlate a paywall's page views into a single session. Also adds the previously-missing `close_reason`, `cache_key`, and `build_id` fields to these events, matching the data already sent by the iOS SDK.

## 2.8.0

## Enhancements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ data class Paywall(
*/
@kotlinx.serialization.Transient()
var state: Map<String, Any> = emptyMap(),
/**
* A unique identifier minted for each distinct presentation of this paywall, used to
* correlate the events tracked during that presentation (e.g. page views).
*/
@kotlinx.serialization.Transient()
var presentationId: String? = null,
@SerialName("url_config")
val urlConfig: PaywallWebviewUrl.Config? = null,
@Serializable
Expand Down Expand Up @@ -275,6 +281,7 @@ data class Paywall(
buildId = buildId,
isScrollEnabled = isScrollEnabled ?: true,
state = state,
presentationId = presentationId,
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ sealed class PaywallCloseReason {
else -> true
}
}

val PaywallCloseReason.description: String
get() =
when (this) {
is PaywallCloseReason.SystemLogic -> "systemLogic"
is PaywallCloseReason.ForNextPaywall -> "forNextPaywall"
is PaywallCloseReason.WebViewFailedToLoad -> "webViewFailedToLoad"
is PaywallCloseReason.ManualClose -> "manualClose"
is PaywallCloseReason.None -> "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ data class PaywallInfo(
@Serializable(with = AnyMapSerializer::class)
val state: Map<String, Any> = emptyMap(),
val customerInfo: CustomerInfo = CustomerInfo.empty(),
val presentationId: String? = null,
) {
constructor(
databaseId: String,
Expand Down Expand Up @@ -99,6 +100,7 @@ data class PaywallInfo(
isScrollEnabled: Boolean,
state: Map<String, Any> = emptyMap(),
customerInfo: CustomerInfo = CustomerInfo.empty(),
presentationId: String? = null,
) : this(
databaseId = databaseId,
identifier = identifier,
Expand Down Expand Up @@ -187,6 +189,7 @@ data class PaywallInfo(
isScrollEnabled = isScrollEnabled,
state = state,
customerInfo = customerInfo,
presentationId = presentationId,
)

fun eventParams(
Expand Down Expand Up @@ -220,6 +223,10 @@ data class PaywallInfo(
"variant_id" to experiment?.variant?.id,
"is_scroll_enabled" to isScrollEnabled,
"state" to state,
"presentation_id" to presentationId,
"close_reason" to closeReason.description,
"cache_key" to cacheKey,
"build_id" to buildId,
)
val customerParams = customerInfo.toParams()
if (customerParams.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class PaywallRequestManager(
return@withContext paywall.copy(
experiment = request.responseIdentifiers.experiment,
presentationSourceType = request.presentationSourceType,
presentationId = java.util.UUID.randomUUID().toString(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ class InternalSuperwallEventTest {
presentation = PaywallPresentationInfo(PaywallPresentationStyle.Modal, 0),
buildId = "build_1",
cacheKey = "cache_1",
presentationId = "presentation_1",
)

private fun stubStoreProduct(
Expand Down Expand Up @@ -1033,6 +1034,7 @@ class InternalSuperwallEventTest {

And("paywall info params are also included") {
assertEquals(paywallInfo.identifier, params["paywall_identifier"])
assertEquals(paywallInfo.presentationId, params["presentation_id"])
}

And("the superwall placement is paywall_page_view") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ class PaywallInfoTest {
assertNotNull(params["paywall_response_load_start_time"])
}

@Test
fun eventParams_includesPresentationIdCloseReasonCacheKeyAndBuildId() {
val info =
PaywallInfo.empty().copy(
presentationId = "presentation-123",
closeReason = PaywallCloseReason.ManualClose,
cacheKey = "cache-456",
buildId = "build-789",
)

val params = info.eventParams()

assertEquals("presentation-123", params["presentation_id"])
assertEquals("manualClose", params["close_reason"])
assertEquals("cache-456", params["cache_key"])
assertEquals("build-789", params["build_id"])
}

@Test
fun eventParams_omitsPresentationId_whenNull() {
val info = PaywallInfo.empty().copy(presentationId = null)

val params = info.eventParams()

assertFalse(params.containsKey("presentation_id"))
}

private fun createProductItem(
name: String,
productIdentifier: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,72 @@ class PaywallRequestManagerTest {
assertEquals(sourceType, (result as Either.Success).value.presentationSourceType)
}

@Test
fun test_getPaywall_setsPresentationId() =
runTest {
val paywall = Paywall.stub().copy(identifier = "test_paywall")
val request =
mockk<PaywallRequest> {
every { responseIdentifiers } returns ResponseIdentifiers(paywallId = "test_paywall")
every { eventData } returns null
every { overrides } returns PaywallRequest.Overrides(products = null, isFreeTrial = null)
every { isDebuggerLaunched } returns false
every { presentationSourceType } returns null
}

coEvery { network.getPaywall(any(), any()) } returns Either.Success(paywall)
coEvery { storeManager.getProducts(any(), any(), any()) } returns
mockk {
every { productItems } returns emptyList()
every { productsByFullId } returns emptyMap()
every { this@mockk.paywall } returns null
}

val result = requestManager.getPaywall(request)

assertTrue(result is Either.Success)
val presentationId = (result as Either.Success).value.presentationId
assertNotNull(presentationId)
assertTrue(presentationId!!.isNotBlank())
}

@Test
fun test_getPaywall_generatesNewPresentationId_onEachCall() =
runTest {
val paywall = Paywall.stub().copy(identifier = "test_paywall")
val request =
mockk<PaywallRequest> {
every { responseIdentifiers } returns ResponseIdentifiers(paywallId = "test_paywall")
every { eventData } returns null
every { overrides } returns PaywallRequest.Overrides(products = null, isFreeTrial = null)
every { isDebuggerLaunched } returns false
every { presentationSourceType } returns null
}

coEvery { network.getPaywall(any(), any()) } returns Either.Success(paywall)
coEvery { storeManager.getProducts(any(), any(), any()) } returns
mockk {
every { productItems } returns emptyList()
every { productsByFullId } returns emptyMap()
every { this@mockk.paywall } returns null
}

// First call
val result1 = requestManager.getPaywall(request)
// Second call hits the request-hash cache, but should still mint a fresh presentation ID
val result2 = requestManager.getPaywall(request)

assertTrue(result1 is Either.Success)
assertTrue(result2 is Either.Success)
val presentationId1 = (result1 as Either.Success).value.presentationId
val presentationId2 = (result2 as Either.Success).value.presentationId
assertNotNull(presentationId1)
assertNotNull(presentationId2)
assertTrue(presentationId1 != presentationId2)
// Network should only be called once due to caching
coVerify(exactly = 1) { network.getPaywall(any(), any()) }
}

@Test
fun test_resetCache_clearsPaywallCache() =
runTest {
Expand Down