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
1 change: 1 addition & 0 deletions app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ dependencies {
implementation(projects.tierComparison)
implementation(projects.trackingCore)
implementation(projects.trackingDatadog)
implementation(projects.trackingFirebase)
implementation(projects.uiForceUpgrade)

// OkHttp for ProGuard rules only - not available at compile time
Expand Down
2 changes: 1 addition & 1 deletion app/app/src/debug/google-services.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"project_info": {
"project_number": "190369814613",
"project_id": "hedvig-dev",
"storage_bucket": "hedvig-dev.appspot.com"
"storage_bucket": "hedvig-dev.firebasestorage.app"
},
"client": [
{
Expand Down
10 changes: 10 additions & 0 deletions app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.hedvig.android.app.crosssell.GetMemberAuthorizationCodeUseCase
import com.hedvig.android.app.externalnavigator.ExternalNavigatorImpl
import com.hedvig.android.app.navigation.CurrentDestinationHolder
import com.hedvig.android.app.navigation.NavRetainedViewModel
import com.hedvig.android.app.navigation.ScreenParameterExtractor
import com.hedvig.android.app.ui.HedvigApp
import com.hedvig.android.app.urihandler.ExternalDeepLinkHandler
import com.hedvig.android.auth.AuthTokenService
Expand All @@ -39,6 +40,7 @@ import com.hedvig.android.core.appreview.WaitUntilAppReviewDialogShouldBeOpenedU
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.core.rive.RiveInitializer
import com.hedvig.android.core.tracking.EventTrackingClient
import com.hedvig.android.data.settings.datastore.SettingsDataStore
import com.hedvig.android.featureflags.FeatureManager
import com.hedvig.android.language.LanguageLaunchCheckUseCase
Expand Down Expand Up @@ -103,6 +105,12 @@ class MainActivity : AppCompatActivity() {
@Inject
private lateinit var currentDestinationHolder: CurrentDestinationHolder

@Inject
private lateinit var eventTrackingClient: EventTrackingClient

@Inject
private lateinit var screenParameterExtractor: ScreenParameterExtractor

@Inject
private lateinit var serializersModules: Set<SerializersModule>

Expand Down Expand Up @@ -245,6 +253,8 @@ class MainActivity : AppCompatActivity() {
getMemberAuthorizationCodeUseCase = getMemberAuthorizationCodeUseCase,
missedPaymentNotificationService = missedPaymentNotificationService,
currentDestinationHolder = currentDestinationHolder,
eventTrackingClient = eventTrackingClient,
screenParameterExtractor = screenParameterExtractor,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.hedvig.android.app.navigation

import com.hedvig.android.core.common.di.AppScope
import com.hedvig.android.navigation.common.HedvigNavKey
import com.hedvig.android.navigation.common.TrackedScreen
import com.hedvig.android.navigation.compose.merge
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.modules.SerializersModule

/**
* Derives the analytics parameters attached to a destination's `screen_view`. By default it reflects
* the key's own [kotlinx.serialization]-serialized properties (every `@Serializable` `val` becomes a
* parameter), reusing the same merged [SerializersModule]s that persist the back stack — so any key
* that survives process death automatically has reportable parameters with no per-screen wiring. A key
* may instead implement [TrackedScreen] to take over entirely.
*
* Parameters ride along the single `screen_view` event keyed by screen name, so they act as breakdown
* dimensions rather than fragmenting a screen into separate entries.
*/
@SingleIn(AppScope::class)
@Inject
internal class ScreenParameterExtractor(
serializersModules: Set<SerializersModule>,
) {
private val json = Json {
serializersModule = serializersModules.merge()
encodeDefaults = true
}

fun parametersFor(destination: HedvigNavKey): Map<String, Any?> {
if (destination is TrackedScreen) {
return destination.screenParameters
}
val element = runCatching {
json.encodeToJsonElement(PolymorphicSerializer(HedvigNavKey::class), destination)
}.getOrNull() as? JsonObject ?: return emptyMap()
return element
.filterKeys { it != CLASS_DISCRIMINATOR }
.mapValues { (_, value) -> value.toPrimitiveOrNull() }
}

// The default polymorphic discriminator kotlinx.serialization writes to identify the concrete key
// type; it duplicates the screen name/class, so it is dropped from the reported parameters.
private companion object {
const val CLASS_DISCRIMINATOR = "type"
}
}

private fun JsonElement.toPrimitiveOrNull(): Any? = when (this) {
is JsonNull -> null
is JsonPrimitive -> if (isString) content else (booleanOrNull ?: longOrNull ?: doubleOrNull ?: content)
else -> toString()
}
33 changes: 33 additions & 0 deletions app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.hedvig.android.app.GlobalHedvigSnackBar
import com.hedvig.android.app.crosssell.GetMemberAuthorizationCodeUseCase
import com.hedvig.android.app.navigation.BackstackController
import com.hedvig.android.app.navigation.CurrentDestinationHolder
import com.hedvig.android.app.navigation.ScreenParameterExtractor
import com.hedvig.android.app.navigation.hedvigEntryProvider
import com.hedvig.android.app.navigation.shouldFadeThrough
import com.hedvig.android.app.urihandler.AuthorizationCodeUriHandler
Expand All @@ -60,6 +61,7 @@ import com.hedvig.android.compose.ui.LocalSharedTransitionScope
import com.hedvig.android.core.appreview.WaitUntilAppReviewDialogShouldBeOpenedUseCase
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.core.tracking.EventTrackingClient
import com.hedvig.android.data.settings.datastore.SettingsDataStore
import com.hedvig.android.design.system.hedvig.DemoModeLabel
import com.hedvig.android.design.system.hedvig.Surface
Expand All @@ -83,6 +85,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import org.jetbrains.compose.resources.stringResource
Expand Down Expand Up @@ -110,8 +113,11 @@ internal fun HedvigApp(
getMemberAuthorizationCodeUseCase: GetMemberAuthorizationCodeUseCase,
missedPaymentNotificationService: MissedPaymentNotificationService,
currentDestinationHolder: CurrentDestinationHolder,
eventTrackingClient: EventTrackingClient,
screenParameterExtractor: ScreenParameterExtractor,
) {
ReportCurrentDestinationEffect(backstackController, currentDestinationHolder)
TrackScreenViewEffect(backstackController, eventTrackingClient, screenParameterExtractor)
val hedvigAppState = rememberHedvigAppState(
backstackController = backstackController,
windowSizeClass = windowSizeClass,
Expand Down Expand Up @@ -287,6 +293,33 @@ private fun ReportCurrentDestinationEffect(
}
}

/**
* Sends a Firebase `screen_view` whenever the destination on top of the rendered stack changes, deriving the screen
* name from the key type (the `{Feature}Key` suffix is dropped) and the parameters from
* [ScreenParameterExtractor]. Parameters ride along the single `screen_view` event keyed by screen name, acting as
* breakdown dimensions rather than fragmenting a screen into separate entries.
*/
@Composable
private fun TrackScreenViewEffect(
backstackController: BackstackController,
eventTrackingClient: EventTrackingClient,
screenParameterExtractor: ScreenParameterExtractor,
) {
LaunchedEffect(backstackController, eventTrackingClient, screenParameterExtractor) {
snapshotFlow { backstackController.currentDestination }
.filterNotNull()
.collect { destination ->
val screenClass = destination::class.simpleName ?: destination.toString()
val screenName = screenClass.removeSuffix("Key")
eventTrackingClient.trackScreen(

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.

🔥

name = screenName,
screenClass = screenClass,
parameters = screenParameterExtractor.parametersFor(destination),
)
}
}
}

/**
* Temporary measure as both design systems need to live side-by-side.
* When everything can come from com.hedvig.android.design.system.hedvig, then this can potentially be removed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.hedvig.android.app.navigation

import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import com.hedvig.android.navigation.common.HedvigNavKey
import com.hedvig.android.navigation.common.TrackedScreen
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.junit.Test

internal class ScreenParameterExtractorTest {
private val extractor = ScreenParameterExtractor(
setOf(
SerializersModule {
polymorphic(HedvigNavKey::class) {
subclass(SimpleKey::class)
subclass(EmptyKey::class)
subclass(NullableKey::class)
subclass(NestedKey::class)
subclass(OverridingKey::class)
}
},
),
)

@Test
fun `primitive properties are flattened and coerced to Firebase-compatible types`() {
val params = extractor.parametersFor(SimpleKey(id = "abc", count = 42, enabled = true))

assertThat(params).isEqualTo(
mapOf<String, Any?>(
"id" to "abc",
"count" to 42L, // Int coerces to Long
"enabled" to true,
),
)
}

@Test
fun `the polymorphic type discriminator is dropped`() {
val params = extractor.parametersFor(EmptyKey)

assertThat(params).isEmpty()
}

@Test
fun `null-valued properties are preserved as null`() {
val params = extractor.parametersFor(NullableKey(maybe = null))

assertThat(params).isEqualTo(mapOf<String, Any?>("maybe" to null))
}

@Test
fun `nested objects fall back to their JSON string`() {
val params = extractor.parametersFor(NestedKey(inner = Inner(a = "x")))

assertThat(params).isEqualTo(mapOf<String, Any?>("inner" to """{"a":"x"}"""))
}

@Test
fun `a TrackedScreen takes over with its own parameters, ignoring serialized shape`() {
val params = extractor.parametersFor(OverridingKey(secret = "should-not-leak"))

assertThat(params).isEqualTo(mapOf<String, Any?>("custom" to "value"))
}
}

@Serializable
private data class SimpleKey(val id: String, val count: Int, val enabled: Boolean) : HedvigNavKey

@Serializable
private data object EmptyKey : HedvigNavKey

@Serializable
private data class NullableKey(val maybe: String?) : HedvigNavKey

@Serializable
private data class NestedKey(val inner: Inner) : HedvigNavKey

@Serializable
private data class Inner(val a: String)

@Serializable
private data class OverridingKey(val secret: String) : HedvigNavKey, TrackedScreen {
override val screenParameters: Map<String, Any?>
get() = mapOf("custom" to "value")
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fun ContractType.isTrialContract() = when (this) {
SE_DOG_PREMIUM,
SE_DOG_STANDARD,
UNKNOWN,
ContractType.SE_QASA_LANDLORD
ContractType.SE_QASA_LANDLORD,
-> false

SE_CAR_TRIAL_FULL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ private fun ConversationCard(
HedvigText(
text = formattedLastMessageSent,
style = HedvigTheme.typography.label,
color = HedvigTheme.colorScheme.textSecondary
color = HedvigTheme.colorScheme.textSecondary,
)
}
}
Expand Down Expand Up @@ -574,7 +574,7 @@ private val mockInboxConversation2 = InboxConversation(
private val mockInboxConversation3 = InboxConversation(
conversationId = "3",
header = Header.ServiceConversation,
latestMessage = Text ("Thank you! Happy to hear that!",Sender.MEMBER, Clock.System.now()),
latestMessage = Text("Thank you! Happy to hear that!", Sender.MEMBER, Clock.System.now()),
hasNewMessages = false,
createdAt = Clock.System.now(),
isClosed = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ internal fun String.markdownToPlainText(): String {
.replace("&amp;", "&") // decode last so we don't re-interpret a decoded entity
.replace(Regex("\\s+"), " ")
.trim()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale):
)
}

is DeflectionMessageFragment -> StepContent.DeflectMessage(
message = message
)
is DeflectionMessageFragment -> {
StepContent.DeflectMessage(
message = message,
)
}

else -> {
logcat { "ClaimIntentStepContentFragment: Unknown step" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ private fun ClaimChatScreenContent(
} else {
navigateUp()
}
}
},
)
}
if (showScrollArrow) {
Expand Down Expand Up @@ -505,7 +505,7 @@ private fun ClaimChatScrollableContent(
} else {
Modifier
},
closeFlow = closeFlow
closeFlow = closeFlow,
)
}
}
Expand Down Expand Up @@ -767,7 +767,7 @@ private fun StepBottomContent(
appPackageId: String,
imageLoader: ImageLoader,
openAppSettings: () -> Unit,
closeFlow: ()-> Unit,
closeFlow: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier) {
Expand Down Expand Up @@ -906,7 +906,7 @@ private fun StepBottomContent(
closeFlow()
},
enabled = true,
buttonStyle = ButtonDefaults.ButtonStyle.Secondary
buttonStyle = ButtonDefaults.ButtonStyle.Secondary,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import com.apollographql.apollo.api.Optional
import com.hedvig.android.apollo.ErrorMessage
import com.hedvig.android.apollo.safeFlow
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.core.common.di.AppScope
import com.hedvig.android.core.common.di.ActivityRetainedScope
import com.hedvig.android.core.common.di.AppScope
import com.hedvig.android.core.common.di.HedvigViewModel
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.core.demomode.DemoSwitcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ internal val paymentOverViewPreviewData: PaymentOverview
ongoingCharges = listOf(OngoingCharge("id", LocalDate.fromEpochDays(401), UiMoney(200.0, UiCurrencyCode.SEK))),
paymentConnection = PaymentConnection.Active,
isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)),
memberType = MemberType.STANDARD_MEMBER
memberType = MemberType.STANDARD_MEMBER,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sealed interface PaymentConnection {
val terminationDateIfNotConnected: LocalDate?,
) : PaymentConnection

data object NeedsPayoutSetup: PaymentConnection
data object NeedsPayoutSetup : PaymentConnection

data object Unknown : PaymentConnection
}
Loading
Loading