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="name" value="MavenRepo" />
|
||||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||||
</remote-repository>
|
</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>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -26,10 +26,7 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.pihelper.shared.Action
|
import com.wbrawner.pihelper.shared.*
|
||||||
import com.wbrawner.pihelper.shared.AuthenticationString
|
|
||||||
import com.wbrawner.pihelper.shared.Effect
|
|
||||||
import com.wbrawner.pihelper.shared.Store
|
|
||||||
|
|
||||||
const val AUTH_SCREEN_TAG = "authScreen"
|
const val AUTH_SCREEN_TAG = "authScreen"
|
||||||
const val SUCCESS_TEXT_TAG = "successText"
|
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.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.pihelper.shared.Action
|
import com.wbrawner.pihelper.shared.*
|
||||||
import com.wbrawner.pihelper.shared.Store
|
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -107,6 +106,11 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
|
||||||
) {
|
) {
|
||||||
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
||||||
uriHandler.openUri(annotation.item)
|
uriHandler.openUri(annotation.item)
|
||||||
|
// TODO: Move this to the store?
|
||||||
|
PlausibleAnalyticsHelper.event(
|
||||||
|
AnalyticsEvent.LinkClicked(annotation.item),
|
||||||
|
Route.ABOUT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextButton(onClick = onForgetPiholeClicked) {
|
TextButton(onClick = onForgetPiholeClicked) {
|
||||||
|
|
|
@ -65,7 +65,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
val state by store.state.collectAsState()
|
val state by store.state.collectAsState()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
LaunchedEffect(state.route) {
|
LaunchedEffect(state.route) {
|
||||||
println("navigating to ${state.route.name}")
|
|
||||||
navController.navigate(state.route.name)
|
navController.navigate(state.route.name)
|
||||||
}
|
}
|
||||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package com.wbrawner.pihelper
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import com.wbrawner.pihelper.shared.PiholeAPIService
|
import com.wbrawner.pihelper.shared.*
|
||||||
import com.wbrawner.pihelper.shared.Store
|
|
||||||
import com.wbrawner.pihelper.shared.create
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -16,9 +14,14 @@ object PiHelperModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
|
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesAnalyticsHelper(): AnalyticsHelper = PlausibleAnalyticsHelper
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesStore(
|
fun providesStore(
|
||||||
apiService: PiholeAPIService,
|
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_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">Disable for 5 minutes</string>
|
||||||
<string name="action_disable_5_minutes_short">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>
|
</resources>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
@ -13,5 +15,8 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,6 +19,7 @@ maxSdk = "33"
|
||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
navigation = "2.4.1"
|
navigation = "2.4.1"
|
||||||
okhttp = "4.10.0"
|
okhttp = "4.10.0"
|
||||||
|
plausible = "0.1.0-SNAPSHOT"
|
||||||
settings = "0.8.1"
|
settings = "0.8.1"
|
||||||
versionCode = "1"
|
versionCode = "1"
|
||||||
versionName = "1.0"
|
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-compose = { module = "androidx.navigation:navigation-compose", version = "navigation" }
|
||||||
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||||
navigation-ui = { module = "androidx.navigation:navigation-ui-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" }
|
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
||||||
test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
|
test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ kotlin {
|
||||||
val androidMain by getting {
|
val androidMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.ktor.client.android)
|
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(
|
class Store(
|
||||||
private val apiService: PiholeAPIService,
|
private val apiService: PiholeAPIService,
|
||||||
|
private val analyticsHelper: AnalyticsHelper? = null,
|
||||||
private val settings: Settings = Settings(),
|
private val settings: Settings = Settings(),
|
||||||
initialState: State = State()
|
initialState: State = State()
|
||||||
) : CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
) : CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
||||||
|
@ -74,14 +75,12 @@ class Store(
|
||||||
private var scanJob: Job? = null
|
private var scanJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launch {
|
var previousRoute: Route? = null
|
||||||
_state.collect {
|
|
||||||
println(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val host: String? = initialState.host ?: settings[KEY_HOST]
|
val host: String? = initialState.host ?: settings[KEY_HOST]
|
||||||
val apiKey: String? = initialState.apiKey ?: settings[KEY_API_KEY]
|
val apiKey: String? = initialState.apiKey ?: settings[KEY_API_KEY]
|
||||||
if (!host.isNullOrBlank() && !apiKey.isNullOrBlank()) {
|
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.baseUrl = host
|
||||||
apiService.apiKey = apiKey
|
apiService.apiKey = apiKey
|
||||||
_state.value = initialState.copy(
|
_state.value = initialState.copy(
|
||||||
|
@ -96,6 +95,16 @@ class Store(
|
||||||
connect("pi.hole", false)
|
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) {
|
fun dispatch(action: Action) {
|
||||||
|
@ -104,17 +113,47 @@ class Store(
|
||||||
is Action.Authenticate -> {
|
is Action.Authenticate -> {
|
||||||
when (action.authString) {
|
when (action.authString) {
|
||||||
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
||||||
is AuthenticationString.Password -> authenticate(
|
is AuthenticationString.Password -> {
|
||||||
|
authenticate(
|
||||||
action.authString.value.hash().hash()
|
action.authString.value.hash().hash()
|
||||||
)
|
)
|
||||||
is AuthenticationString.Token -> authenticate(action.authString.value)
|
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)
|
is Action.Connect -> {
|
||||||
Action.Enable -> enable()
|
connect(action.host)
|
||||||
Action.Forget -> forget()
|
analyticsHelper?.event(AnalyticsEvent.ConnectButtonClick, _state.value.route)
|
||||||
is Action.Scan -> scan(action.deviceIp)
|
}
|
||||||
|
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.About -> _state.value = _state.value.copy(route = Route.ABOUT)
|
||||||
Action.Back -> back()
|
Action.Back -> back()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue