Files
Cable/android/app/build.gradle.kts
Stefan Lange-Hegermann 5476209c50 Analytics: make retention measurable
Aptabase hashes its user_id from IP + user agent with a salt that rotates
daily, so events can never be linked across days and no retention or MAU
figure can come out of the export. In 19 days of data not one of 82 user_ids
appears on two dates, which is the artefact, not the behaviour.

Instead of an identity, every event now carries this install's own counters,
kept in UserDefaults/DataStore: tenure_days, launch_no, active_days and
dormant_days. Only derived day counts leave the device, so the privacy
position is unchanged.

They make the curve countable in the export: launch_no == 1 marks exactly one
launch per install, dormant_days >= 1 exactly one launch per calendar day, so
D_k is the share of installs seen again with tenure_days == k. Event date
minus tenure_days is the install date, which gives full cohort tables.

Installs predating the counters have no install date and report
tenure_days == -1 forever, so they can be excluded instead of inflating the
new-install cohort.

The Kotlin counter arithmetic sits in a pure advance() so it can be tested
without a Context; this adds the app module's first JVM test source set.
2026-08-19 17:43:45 +02:00

130 lines
4.1 KiB
Kotlin

import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.baselineprofile)
}
// Release signing credentials, loaded from android/keystore.properties (gitignored).
// Falls back to no release signing when the file/keystore is absent (e.g. CI without secrets).
val keystoreProps = Properties().apply {
val f = rootProject.file("keystore.properties")
if (f.exists()) load(FileInputStream(f))
}
val hasReleaseSigning = keystoreProps.getProperty("storeFile")?.let { file(it).exists() } == true
android {
namespace = "app.voltplan.cable"
compileSdk = 36
defaultConfig {
applicationId = "app.voltplan.cable"
minSdk = 26
targetSdk = 36
versionCode = 88
versionName = "1.8.1"
// Aptabase analytics — mirrors the iOS configuration (the iPhone app's tracker).
buildConfigField("String", "APTABASE_APP_KEY", "\"A-SH-4260269603\"")
buildConfigField("String", "APTABASE_HOST", "\"https://apta.yuzuhub.com\"")
vectorDrawables { useSupportLibrary = true }
resourceConfigurations += listOf("en", "de", "es", "fr", "nl")
}
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(keystoreProps.getProperty("storeFile"))
storePassword = keystoreProps.getProperty("storePassword")
keyAlias = keystoreProps.getProperty("keyAlias")
keyPassword = keystoreProps.getProperty("keyPassword")
}
}
}
buildTypes {
debug {
isMinifyEnabled = false
}
release {
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
// Bundle native debug symbols so Play can symbolicate native crashes/ANRs.
ndk {
debugSymbolLevel = "FULL"
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.navigation.compose)
debugImplementation(libs.androidx.ui.tooling)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.datastore.preferences)
implementation(libs.retrofit)
implementation(libs.retrofit.serialization)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.kotlinx.serialization.json)
implementation(libs.coil.compose)
implementation(libs.play.review.ktx)
// Installs the bundled baseline profile on devices that do not get it from Play.
implementation(libs.androidx.profileinstaller)
testImplementation(libs.junit)
// Consumes the profile produced by :baselineprofile.
baselineProfile(project(":baselineprofile"))
}