-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use Plausible for app analytics (#929)
* Remove datadog * Update CI * update Makefile * update Makefile * remove datadog directory * Use Sentry for crash reporting (#927) * update Makefile * Add back Sentry for crash reporting * remove datadog from gradle config * Add plausible * integrate plausible analytics * integrate plausible analytics * track when ads are shown * track when ads are shown * add Plausible class * track when Replica content is viewed * track searches and uploads * Formatting * Track when ads fail to load as well
- Loading branch information
Showing
20 changed files
with
484 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
android/app/src/main/kotlin/org/getlantern/lantern/plausible/Event.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package org.getlantern.lantern.plausible | ||
|
||
import org.getlantern.lantern.util.Json | ||
|
||
internal data class Event( | ||
val domain: String, | ||
val name: String, | ||
val url: String, | ||
val referrer: String, | ||
val screenWidth: Int, | ||
val props: Map<String, String>? | ||
) { | ||
companion object { | ||
fun fromJson(json: String): Event? = try { | ||
Json.gson.fromJson(json, Event::class.java) | ||
} catch (ignored: Exception) { | ||
null | ||
} | ||
} | ||
} | ||
|
||
internal fun Event.toJson(): String = Json.gson.toJson(this) |
107 changes: 107 additions & 0 deletions
107
android/app/src/main/kotlin/org/getlantern/lantern/plausible/Plausible.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package org.getlantern.lantern.plausible | ||
|
||
import android.content.Context | ||
import java.util.concurrent.atomic.AtomicReference | ||
import org.getlantern.mobilesdk.Logger | ||
|
||
// Singleton for sending events to Plausible. | ||
object Plausible { | ||
private val client: AtomicReference<PlausibleClient?> = AtomicReference(null) | ||
private val config: AtomicReference<PlausibleConfig?> = AtomicReference(null) | ||
|
||
fun init(context: Context) { | ||
val config = AndroidResourcePlausibleConfig(context) | ||
val client = NetworkFirstPlausibleClient(config) | ||
init(client, config) | ||
} | ||
|
||
internal fun init(client: PlausibleClient, config: PlausibleConfig) { | ||
this.client.set(client) | ||
this.config.set(config) | ||
} | ||
|
||
// Enable or disable event sending | ||
@Suppress("unused") | ||
fun enable(enable: Boolean) { | ||
config.get() | ||
?.let { | ||
it.enable = enable | ||
} | ||
?: Logger.d("Plausible", "Ignoring call to enable(). Did you forget to call Plausible.init()?") | ||
} | ||
|
||
/** | ||
* The raw value of User-Agent is used to calculate the user_id which identifies a unique | ||
* visitor in Plausible. | ||
* User-Agent is also used to populate the Devices report in your | ||
* Plausible dashboard. The device data is derived from the open source database | ||
* device-detector. If your User-Agent is not showing up in your dashboard, it's probably | ||
* because it is not recognized as one in the device-detector database. | ||
*/ | ||
@Suppress("unused") | ||
fun setUserAgent(userAgent: String) { | ||
config.get() | ||
?.let { | ||
it.userAgent = userAgent | ||
} | ||
?: Logger.d("Plausible", "Ignoring call to setUserAgent(). Did you forget to call Plausible.init()?") | ||
} | ||
|
||
/** | ||
* Send a `pageview` event. | ||
* | ||
* @param url URL of the page where the event was triggered. If the URL contains UTM parameters, | ||
* they will be extracted and stored. | ||
* The URL parameter will feel strange in a mobile app but you can manufacture something that looks | ||
* like a web URL. If you name your mobile app screens like page URLs, Plausible will know how to | ||
* handle it. So for example, on your login screen you could send something like | ||
* `app://localhost/login`. The pathname (/login) is what will be shown as the page value in the | ||
* Plausible dashboard. | ||
* @param referrer Referrer for this event. | ||
* Plausible uses the open source referer-parser database to parse referrers and assign these | ||
*/ | ||
fun pageView( | ||
url: String, | ||
referrer: String = "", | ||
props: Map<String, Any?>? = null | ||
) = event( | ||
name = "pageview", | ||
url = url, | ||
referrer = referrer, | ||
props = props | ||
) | ||
|
||
/** | ||
* Send a custom event. To send a `pageview` event, consider using [pageView] instead. | ||
* | ||
* @param name Name of the event. Can specify `pageview` which is a special type of event in | ||
* Plausible. All other names will be treated as custom events. | ||
* @param url URL of the page where the event was triggered. If the URL contains UTM parameters, | ||
* they will be extracted and stored. | ||
* The URL parameter will feel strange in a mobile app but you can manufacture something that looks | ||
* like a web URL. If you name your mobile app screens like page URLs, Plausible will know how to | ||
* handle it. So for example, on your login screen you could send something like | ||
* `app://localhost/login`. The pathname (/login) is what will be shown as the page value in the | ||
* Plausible dashboard. | ||
* @param referrer Referrer for this event. | ||
* Plausible uses the open source referer-parser database to parse referrers and assign these | ||
* source categories. | ||
*/ | ||
@Suppress("MemberVisibilityCanBePrivate") | ||
fun event( | ||
name: String, | ||
url: String = "", | ||
referrer: String = "", | ||
props: Map<String, Any?>? = null | ||
) { | ||
client.get() | ||
?.let { client -> | ||
config.get() | ||
?.let { config -> | ||
client.event(config.domain, name, url, referrer, config.screenWidth, props) | ||
} | ||
?: Logger.d("Plausible", "Ignoring call to event(). Did you forget to call Plausible.init()?") | ||
} | ||
?: Logger.d("Plausible", "Ignoring call to event(). Did you forget to call Plausible.init()?") | ||
} | ||
} |
179 changes: 179 additions & 0 deletions
179
android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
package org.getlantern.lantern.plausible | ||
|
||
import android.net.Uri | ||
import androidx.annotation.VisibleForTesting | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.delay | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.suspendCancellableCoroutine | ||
import okhttp3.Call | ||
import okhttp3.Callback | ||
import okhttp3.HttpUrl.Companion.toHttpUrl | ||
import okhttp3.MediaType.Companion.toMediaType | ||
import okhttp3.OkHttpClient | ||
import okhttp3.Request | ||
import okhttp3.RequestBody.Companion.toRequestBody | ||
import okhttp3.Response | ||
import java.io.File | ||
import java.io.IOException | ||
import java.net.InetSocketAddress | ||
import java.net.Proxy | ||
import java.net.URI | ||
import kotlin.coroutines.CoroutineContext | ||
import kotlin.coroutines.resume | ||
import kotlin.coroutines.resumeWithException | ||
import org.getlantern.lantern.LanternApp | ||
import org.getlantern.mobilesdk.Logger | ||
|
||
internal interface PlausibleClient { | ||
|
||
// See [Plausible.event] for details on parameters. | ||
// @return true if the event was successfully processed and false if not | ||
fun event( | ||
domain: String, | ||
name: String, | ||
url: String, | ||
referrer: String, | ||
screenWidth: Int, | ||
props: Map<String, Any?>? = null | ||
) { | ||
var correctedUrl = Uri.parse(url) | ||
if (correctedUrl.scheme.isNullOrBlank()) { | ||
correctedUrl = correctedUrl.buildUpon().scheme("app").build() | ||
} | ||
if (correctedUrl.authority.isNullOrBlank()) { | ||
correctedUrl = correctedUrl.buildUpon().authority("localhost").build() | ||
} | ||
return event(Event( | ||
domain, | ||
name, | ||
correctedUrl.toString(), | ||
referrer, | ||
screenWidth, | ||
props?.mapValues { (_, v) -> v.toString() } | ||
)) | ||
} | ||
|
||
fun event(event: Event) | ||
} | ||
|
||
// The primary client for sending events to Plausible. It will attempt to send events immediately, | ||
// caching them to disk to send later upon failure. | ||
internal class NetworkFirstPlausibleClient( | ||
private val config: PlausibleConfig, | ||
coroutineContext: CoroutineContext = Dispatchers.IO | ||
) : PlausibleClient { | ||
private val coroutineScope = CoroutineScope(coroutineContext) | ||
|
||
init { | ||
coroutineScope.launch { | ||
config.eventDir.mkdirs() | ||
config.eventDir.listFiles()?.forEach { | ||
val event = Event.fromJson(it.readText()) | ||
if (event == null) { | ||
Logger.e("Plausible", "Failed to decode event JSON, discarding") | ||
it.delete() | ||
return@forEach | ||
} | ||
try { | ||
postEvent(event) | ||
} catch (e: IOException) { | ||
return@forEach | ||
} | ||
it.delete() | ||
} | ||
} | ||
} | ||
|
||
override fun event(event: Event) { | ||
coroutineScope.launch { | ||
suspendEvent(event) | ||
} | ||
} | ||
|
||
@VisibleForTesting | ||
internal suspend fun suspendEvent(event: Event) { | ||
try { | ||
postEvent(event) | ||
} catch (e: IOException) { | ||
if (!config.retryOnFailure) return | ||
val file = File(config.eventDir, "event_${System.currentTimeMillis()}.json") | ||
file.writeText(event.toJson()) | ||
var retryAttempts = 0 | ||
var retryDelay = 1000L | ||
while (retryAttempts < 5) { | ||
delay(retryDelay) | ||
retryDelay = when (retryDelay) { | ||
1000L -> 60_000L | ||
60_000L -> 360_000L | ||
360_000L -> 600_000L | ||
else -> break | ||
} | ||
try { | ||
postEvent(event) | ||
file.delete() | ||
break | ||
} catch (e: IOException) { | ||
retryAttempts++ | ||
} | ||
} | ||
} | ||
} | ||
|
||
private suspend fun postEvent(event: Event) { | ||
if (!config.enable) { | ||
Logger.e("Plausible", "Plausible disabled, not sending event: $event") | ||
return | ||
} | ||
val body = event.toJson().toRequestBody("application/json".toMediaType()) | ||
val url = config.host | ||
.toHttpUrl() | ||
.newBuilder() | ||
.addPathSegments("api/event") | ||
.build() | ||
val request = Request.Builder() | ||
.url(url) | ||
.addHeader("User-Agent", config.userAgent) | ||
.post(body) | ||
.build() | ||
suspendCancellableCoroutine { continuation -> | ||
val call = okHttpClient.newCall(request) | ||
continuation.invokeOnCancellation { | ||
call.cancel() | ||
} | ||
|
||
call.enqueue(object : Callback { | ||
override fun onFailure(call: Call, e: IOException) { | ||
Logger.e("Plausible", "Failed to send event to backend") | ||
continuation.resumeWithException(e) | ||
} | ||
|
||
override fun onResponse(call: Call, response: Response) { | ||
response.use { res -> | ||
if (res.isSuccessful) { | ||
continuation.resume(Unit) | ||
} else { | ||
val e = IOException( | ||
"Received unexpected response: ${res.code} ${res.body?.string()}" | ||
) | ||
onFailure(call, e) | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
val okHttpClient: OkHttpClient by lazy { | ||
val session = LanternApp.getSession() | ||
val hTTPAddr = session.hTTPAddr | ||
val uri = URI("http://" + hTTPAddr) | ||
val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress( | ||
"127.0.0.1", | ||
uri.getPort(), | ||
), | ||
) | ||
OkHttpClient.Builder().proxy(proxy).build() | ||
} | ||
} |
Oops, something went wrong.