Add Plausible analytics

This commit is contained in:
William Brawner 2022-12-23 21:28:14 -07:00
parent f2fb3f6c2e
commit 6c18dc6d1d
12 changed files with 123 additions and 25 deletions

View file

@ -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>

View file

@ -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"

View file

@ -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) {

View file

@ -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)

View file

@ -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)
}

View file

@ -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>

View file

@ -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/")
}
}
}

View file

@ -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" }

View file

@ -29,6 +29,7 @@ kotlin {
val androidMain by getting {
dependencies {
implementation(libs.ktor.client.android)
implementation(libs.plausible)
}
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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()
}