Add Plausible analytics
This commit is contained in:
parent
f2fb3f6c2e
commit
6c18dc6d1d
12 changed files with 123 additions and 25 deletions
|
@ -26,5 +26,10 @@
|
|||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://s01.oss.sonatype.org/content/repositories/snapshots/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
|
@ -26,10 +26,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.wbrawner.pihelper.shared.Action
|
||||
import com.wbrawner.pihelper.shared.AuthenticationString
|
||||
import com.wbrawner.pihelper.shared.Effect
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
import com.wbrawner.pihelper.shared.*
|
||||
|
||||
const val AUTH_SCREEN_TAG = "authScreen"
|
||||
const val SUCCESS_TEXT_TAG = "successText"
|
||||
|
|
|
@ -22,8 +22,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.wbrawner.pihelper.shared.Action
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
import com.wbrawner.pihelper.shared.*
|
||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||
|
||||
@Composable
|
||||
|
@ -107,6 +106,11 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
|
|||
) {
|
||||
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
||||
uriHandler.openUri(annotation.item)
|
||||
// TODO: Move this to the store?
|
||||
PlausibleAnalyticsHelper.event(
|
||||
AnalyticsEvent.LinkClicked(annotation.item),
|
||||
Route.ABOUT
|
||||
)
|
||||
}
|
||||
}
|
||||
TextButton(onClick = onForgetPiholeClicked) {
|
||||
|
|
|
@ -65,7 +65,6 @@ class MainActivity : AppCompatActivity() {
|
|||
val state by store.state.collectAsState()
|
||||
val navController = rememberNavController()
|
||||
LaunchedEffect(state.route) {
|
||||
println("navigating to ${state.route.name}")
|
||||
navController.navigate(state.route.name)
|
||||
}
|
||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
import com.wbrawner.pihelper.shared.create
|
||||
import com.wbrawner.pihelper.shared.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -16,9 +14,14 @@ object PiHelperModule {
|
|||
@Singleton
|
||||
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAnalyticsHelper(): AnalyticsHelper = PlausibleAnalyticsHelper
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesStore(
|
||||
apiService: PiholeAPIService,
|
||||
): Store = Store(apiService)
|
||||
analyticsHelper: AnalyticsHelper
|
||||
): Store = Store(apiService, analyticsHelper)
|
||||
}
|
|
@ -7,4 +7,6 @@
|
|||
<string name="action_disable_30_seconds_short">Disable for 30 seconds</string>
|
||||
<string name="action_disable_5_minutes">Disable for 5 minutes</string>
|
||||
<string name="action_disable_5_minutes_short">Disable for 5 minutes</string>
|
||||
<string name="plausible_domain">pihelper.android.wbrawner.com</string>
|
||||
<string name="plausible_host">https://plausible.wbrawner.com</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import java.net.URI
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
@ -13,5 +15,8 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ maxSdk = "33"
|
|||
minSdk = "23"
|
||||
navigation = "2.4.1"
|
||||
okhttp = "4.10.0"
|
||||
plausible = "0.1.0-SNAPSHOT"
|
||||
settings = "0.8.1"
|
||||
versionCode = "1"
|
||||
versionName = "1.0"
|
||||
|
@ -66,6 +67,7 @@ multiplatform-settings = { module = "com.russhwolf:multiplatform-settings-no-arg
|
|||
navigation-compose = { module = "androidx.navigation:navigation-compose", version = "navigation" }
|
||||
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
plausible = { module = "com.wbrawner.plausible:plausible-android", version.ref = "plausible" }
|
||||
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
||||
test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ kotlin {
|
|||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.plausible)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.wbrawner.pihelper.shared
|
||||
|
||||
import com.wbrawner.plausible.android.Plausible
|
||||
|
||||
object PlausibleAnalyticsHelper : AnalyticsHelper {
|
||||
override fun pageView(route: Route) {
|
||||
Plausible.pageView(route.path)
|
||||
}
|
||||
|
||||
override fun event(event: AnalyticsEvent, route: Route) {
|
||||
val props = when (event) {
|
||||
is AnalyticsEvent.DisableButtonClicked -> mapOf("duration" to event.duration)
|
||||
is AnalyticsEvent.LinkClicked -> mapOf("link" to event.link)
|
||||
else -> null
|
||||
}
|
||||
Plausible.event(event.name, route.path, props = props)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.wbrawner.pihelper.shared
|
||||
|
||||
interface AnalyticsHelper {
|
||||
fun pageView(route: Route)
|
||||
fun event(event: AnalyticsEvent, route: Route)
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
sealed class AnalyticsEvent(val name: String) {
|
||||
object ScanButtonClick : AnalyticsEvent("Scan button clicked")
|
||||
object ConnectButtonClick : AnalyticsEvent("Connect button clicked")
|
||||
object AuthenticateWithPasswordButtonClicked
|
||||
: AnalyticsEvent("Authenticate with password button clicked")
|
||||
|
||||
object AuthenticateWithApiKeyButtonClicked
|
||||
: AnalyticsEvent("Authenticate with API key button clicked")
|
||||
|
||||
object EnableButtonClicked : AnalyticsEvent("Enable button clicked")
|
||||
data class DisableButtonClicked(val duration: Long?) : AnalyticsEvent("Disable button clicked")
|
||||
object ForgetButtonClicked : AnalyticsEvent("Forget button clicked")
|
||||
data class LinkClicked(val link: String) : AnalyticsEvent("Link clicked")
|
||||
}
|
|
@ -63,6 +63,7 @@ private const val ONE_SECOND = 1_000
|
|||
|
||||
class Store(
|
||||
private val apiService: PiholeAPIService,
|
||||
private val analyticsHelper: AnalyticsHelper? = null,
|
||||
private val settings: Settings = Settings(),
|
||||
initialState: State = State()
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
||||
|
@ -74,14 +75,12 @@ class Store(
|
|||
private var scanJob: Job? = null
|
||||
|
||||
init {
|
||||
launch {
|
||||
_state.collect {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
var previousRoute: Route? = null
|
||||
val host: String? = initialState.host ?: settings[KEY_HOST]
|
||||
val apiKey: String? = initialState.apiKey ?: settings[KEY_API_KEY]
|
||||
if (!host.isNullOrBlank() && !apiKey.isNullOrBlank()) {
|
||||
// This avoids reporting a page view for the connect page when it isn't actually viewed
|
||||
previousRoute = Route.CONNECT
|
||||
apiService.baseUrl = host
|
||||
apiService.apiKey = apiKey
|
||||
_state.value = initialState.copy(
|
||||
|
@ -96,6 +95,16 @@ class Store(
|
|||
connect("pi.hole", false)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
delay(1000)
|
||||
_state.collect {
|
||||
println(it)
|
||||
if (it.route != previousRoute) {
|
||||
previousRoute = it.route
|
||||
analyticsHelper?.pageView(it.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dispatch(action: Action) {
|
||||
|
@ -104,17 +113,47 @@ class Store(
|
|||
is Action.Authenticate -> {
|
||||
when (action.authString) {
|
||||
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
||||
is AuthenticationString.Password -> authenticate(
|
||||
action.authString.value.hash().hash()
|
||||
)
|
||||
is AuthenticationString.Token -> authenticate(action.authString.value)
|
||||
is AuthenticationString.Password -> {
|
||||
authenticate(
|
||||
action.authString.value.hash().hash()
|
||||
)
|
||||
analyticsHelper?.event(
|
||||
AnalyticsEvent.AuthenticateWithPasswordButtonClicked,
|
||||
_state.value.route
|
||||
)
|
||||
}
|
||||
is AuthenticationString.Token -> {
|
||||
authenticate(action.authString.value)
|
||||
analyticsHelper?.event(
|
||||
AnalyticsEvent.AuthenticateWithApiKeyButtonClicked,
|
||||
_state.value.route
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Action.Connect -> connect(action.host)
|
||||
is Action.Disable -> disable(action.duration)
|
||||
Action.Enable -> enable()
|
||||
Action.Forget -> forget()
|
||||
is Action.Scan -> scan(action.deviceIp)
|
||||
is Action.Connect -> {
|
||||
connect(action.host)
|
||||
analyticsHelper?.event(AnalyticsEvent.ConnectButtonClick, _state.value.route)
|
||||
}
|
||||
is Action.Disable -> {
|
||||
disable(action.duration)
|
||||
analyticsHelper?.event(
|
||||
AnalyticsEvent.DisableButtonClicked(action.duration),
|
||||
_state.value.route
|
||||
)
|
||||
}
|
||||
Action.Enable -> {
|
||||
enable()
|
||||
analyticsHelper?.event(AnalyticsEvent.EnableButtonClicked, _state.value.route)
|
||||
}
|
||||
Action.Forget -> {
|
||||
forget()
|
||||
analyticsHelper?.event(AnalyticsEvent.ForgetButtonClicked, _state.value.route)
|
||||
}
|
||||
is Action.Scan -> {
|
||||
scan(action.deviceIp)
|
||||
analyticsHelper?.event(AnalyticsEvent.ScanButtonClick, _state.value.route)
|
||||
}
|
||||
Action.About -> _state.value = _state.value.copy(route = Route.ABOUT)
|
||||
Action.Back -> back()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue