diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4ea8ece9d..004dfbfd0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -428,8 +428,6 @@ dependencies {
implementation 'com.stripe:stripe-android:20.17.0'
- implementation 'com.datadoghq:dd-sdk-android:1.19.3'
-
annotationProcessor "org.androidannotations:androidannotations:$androidAnnotationsVersion"
implementation("org.androidannotations:androidannotations-api:$androidAnnotationsVersion")
kapt "org.androidannotations:androidannotations:$androidAnnotationsVersion"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f95e8b487..d986dd656 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -105,6 +105,16 @@
android:resource="@xml/file_paths" />
+
+
+
+
diff --git a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt
index 5cf700ceb..361665b8a 100644
--- a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt
+++ b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt
@@ -29,6 +29,7 @@ import org.getlantern.lantern.model.LanternHttpClient.ProUserCallback
import org.getlantern.lantern.model.ProError
import org.getlantern.lantern.model.ProUser
import org.getlantern.lantern.model.Utils
+import org.getlantern.lantern.plausible.Plausible
import org.getlantern.lantern.util.AutoUpdater
import org.getlantern.lantern.util.PaymentsUtil
import org.getlantern.lantern.util.PermissionUtil
@@ -181,6 +182,10 @@ class SessionModel(
}
}
+ "trackUserAction" -> {
+ Plausible.event(call.argument("message")!!)
+ }
+
"acceptTerms" -> {
LanternApp.getSession().acceptTerms()
}
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt
index 44275691b..62fd3d72f 100644
--- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt
@@ -44,6 +44,7 @@ import org.getlantern.lantern.model.Utils
import org.getlantern.lantern.model.VpnState
import org.getlantern.lantern.notification.NotificationHelper
import org.getlantern.lantern.notification.NotificationReceiver
+import org.getlantern.lantern.plausible.Plausible
import org.getlantern.lantern.service.LanternService_
import org.getlantern.lantern.util.PermissionUtil
import org.getlantern.lantern.util.PlansUtil
@@ -90,6 +91,7 @@ class MainActivity :
eventManager = object : EventManager("lantern_event_channel", flutterEngine) {
override fun onListen(event: Event) {
if (LanternApp.getSession().lanternDidStart()) {
+ Plausible.enable(true)
fetchLoConf()
Logger.debug(
TAG,
@@ -129,7 +131,6 @@ class MainActivity :
override fun onCreate(savedInstanceState: Bundle?) {
val start = System.currentTimeMillis()
super.onCreate(savedInstanceState)
-
Logger.debug(TAG, "Default Locale is %1\$s", Locale.getDefault())
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this)
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Event.kt b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Event.kt
new file mode 100644
index 000000000..074086112
--- /dev/null
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Event.kt
@@ -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?
+) {
+ 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)
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Plausible.kt b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Plausible.kt
new file mode 100644
index 000000000..c6e9a5302
--- /dev/null
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/Plausible.kt
@@ -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 = AtomicReference(null)
+ private val config: AtomicReference = 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? = 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? = 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()?")
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt
new file mode 100644
index 000000000..a97f56b8a
--- /dev/null
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt
@@ -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? = 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()
+ }
+}
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleConfig.kt b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleConfig.kt
new file mode 100644
index 000000000..8b66312e6
--- /dev/null
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleConfig.kt
@@ -0,0 +1,90 @@
+package org.getlantern.lantern.plausible
+
+import android.content.Context
+import android.content.res.Resources
+import android.os.Build
+import java.io.File
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.math.roundToInt
+import org.getlantern.lantern.BuildConfig
+import org.getlantern.lantern.R
+
+private val DEFAULT_USER_AGENT =
+ "Android ${Build.VERSION.RELEASE} ${Build.MANUFACTURER} ${Build.PRODUCT} ${Build.FINGERPRINT.hashCode()}"
+private const val DEFAULT_PLAUSIBLE_HOST = "https://plausible.io"
+
+// Configuration options for the Plausible SDK. See the [Events API reference](https://plausible.io/docs/events-api) for more details
+interface PlausibleConfig {
+
+ // Domain name of the site in Plausible
+ var domain: String
+
+ // Whether or not events should be sent. Use this to allow users to opt-in or opt-out for
+ // example.
+ var enable: Boolean
+
+ // Directory to persist events upon upload failure.
+ val eventDir: File
+
+ // The host for the Plausible backend server. Defaults to `https://plausible.io`
+ var host: String
+
+ // Whether or not to attempt to resend events upon failure. If true, events will be serialized
+ // to disk in [eventDir] and the upload will be retried later.
+ var retryOnFailure: Boolean
+
+ // Width of the screen in dp.
+ val screenWidth: Int
+
+ // The raw value of User-Agent is used to calculate the user_id which identifies a unique
+ // visitor in Plausible.
+ var userAgent: String
+}
+
+open class ThreadSafePlausibleConfig(
+ override val eventDir: File,
+ override val screenWidth: Int
+) : PlausibleConfig {
+
+ private val enableRef = AtomicBoolean(true)
+ override var enable: Boolean
+ get() = enableRef.get()
+ set(value) = enableRef.set(value)
+
+ private val domainRef = AtomicReference("")
+ override var domain: String
+ get() = domainRef.get()
+ set(value) = domainRef.set(value)
+
+ private val hostRef = AtomicReference(DEFAULT_PLAUSIBLE_HOST)
+ override var host: String
+ get() = hostRef.get() ?: ""
+ set(value) = hostRef.set(value.ifBlank { DEFAULT_PLAUSIBLE_HOST })
+
+ private val retryRef = AtomicBoolean(true)
+ override var retryOnFailure: Boolean
+ get() = retryRef.get()
+ set(value) = retryRef.set(value)
+
+ private val userAgentRef = AtomicReference(DEFAULT_USER_AGENT)
+ override var userAgent: String
+ get() = userAgentRef.get()
+ set(value) = userAgentRef.set(value.ifBlank { DEFAULT_USER_AGENT })
+}
+
+class AndroidResourcePlausibleConfig(context: Context) : ThreadSafePlausibleConfig(
+ eventDir = File(context.applicationContext.filesDir, "events"),
+ screenWidth = with(Resources.getSystem().displayMetrics) {
+ widthPixels / density
+ }.roundToInt()
+) {
+ init {
+ domain = context.resources.getString(R.string.plausible_domain)
+ host = context.resources.getString(R.string.plausible_host)
+ context.resources.getString(R.string.plausible_enable_startup).toBooleanStrictOrNull()
+ ?.let {
+ enable = it
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleInitializer.kt b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleInitializer.kt
new file mode 100644
index 000000000..02dc89943
--- /dev/null
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleInitializer.kt
@@ -0,0 +1,16 @@
+package org.getlantern.lantern.plausible
+
+import android.content.Context
+import androidx.startup.Initializer
+
+// Automatically initializes the Plausible SDK for sending events.
+class PlausibleInitializer : Initializer {
+ override fun create(context: Context): Plausible {
+ Plausible.init(context.applicationContext)
+ return Plausible
+ }
+
+ override fun dependencies(): List>> {
+ return emptyList()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt
index 70f551a93..c703c75d2 100644
--- a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt
+++ b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt
@@ -7,6 +7,7 @@ import android.content.ServiceConnection
import android.net.VpnService
import android.os.IBinder
import org.getlantern.lantern.LanternApp
+import org.getlantern.lantern.plausible.Plausible
import org.getlantern.lantern.service.LanternService_
import org.getlantern.mobilesdk.Logger
@@ -58,6 +59,7 @@ class LanternVpnService : VpnService(), Runnable {
return START_STICKY
}
return if (intent.action == ACTION_DISCONNECT) {
+ Plausible.event("switchVPN", "", "", mapOf("status" to "disconnect"))
stop()
START_NOT_STICKY
} else {
@@ -69,6 +71,7 @@ class LanternVpnService : VpnService(), Runnable {
private fun connect() {
Logger.d(TAG, "connect")
+ Plausible.event("switchVPN", "", "", mapOf("status" to "connect"))
Thread(this, "VpnService").start()
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 4ebb652bd..e121c1dcb 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -499,4 +499,10 @@
Calls
Lantern Service
Replica Download
+
+ true
+
+ https://plausible.io
+
+ android.lantern.io
diff --git a/android/settings.gradle b/android/settings.gradle
index 31a9ce0ec..32344b562 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,3 +1,11 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
diff --git a/lib/ad_helper.dart b/lib/ad_helper.dart
index c9b6f12e4..4ac77acaa 100644
--- a/lib/ad_helper.dart
+++ b/lib/ad_helper.dart
@@ -11,6 +11,7 @@ import 'package:clever_ads_solutions/public/MediationManager.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:logger/logger.dart';
+import 'package:lantern/common/common.dart';
import 'package:lantern/replica/common.dart';
enum AdType { Google, CAS }
@@ -109,6 +110,8 @@ class AdHelper {
},
onAdShowedFullScreenContent: (ad) {
logger.i('[Ads Manager] Showing Ads');
+ PlausibleUtils.trackUserAction(
+ 'User shown interstitial ad', googleAttributes);
},
onAdFailedToShowFullScreenContent: (ad, error) {
logger.i(
@@ -123,10 +126,12 @@ class AdHelper {
);
_interstitialAd = ad;
logger.i('[Ads Manager] to loaded $ad');
+ PlausibleUtils.trackUserAction('Interstitial ad loaded', googleAttributes);
},
onAdFailedToLoad: (err) {
_failedLoadAttempts++; // increment the count on failure
logger.i('[Ads Manager] failed to load $err');
+ PlausibleUtils.trackUserAction('Interstitial ad failed to load', googleAttributes);
_postShowingAds();
},
),
diff --git a/lib/common/common.dart b/lib/common/common.dart
index 64a1b3506..f304fc8b9 100644
--- a/lib/common/common.dart
+++ b/lib/common/common.dart
@@ -37,6 +37,7 @@ export 'lru_cache.dart';
export 'model.dart';
export 'model_event_channel.dart';
export 'once.dart';
+export 'plausible.dart';
export 'session_model.dart';
export 'single_value_subscriber.dart';
export 'ui/audio.dart';
diff --git a/lib/common/plausible.dart b/lib/common/plausible.dart
new file mode 100644
index 000000000..22c55b044
--- /dev/null
+++ b/lib/common/plausible.dart
@@ -0,0 +1,10 @@
+import 'package:plausible_analytics/plausible_analytics.dart';
+
+class PlausibleUtils {
+ static trackUserAction(String name, [Map props = const {}]) {
+ Plausible plausible =
+ Plausible("https://plausible.io", "android.lantern.io");
+ // Send goal
+ plausible.event(name: name, props: props);
+ }
+}
diff --git a/lib/replica/logic/api.dart b/lib/replica/logic/api.dart
index aa7759eb9..004609f59 100644
--- a/lib/replica/logic/api.dart
+++ b/lib/replica/logic/api.dart
@@ -61,11 +61,13 @@ class ReplicaApi {
break;
}
logger.v('_search(): uri: ${Uri.parse(s)}');
-
final resp = await dio.get(s);
if (resp.statusCode == 200) {
logger
.v('Statuscode: ${resp.statusCode} || body: ${resp.data.toString()}');
+ PlausibleUtils.trackUserAction('User searched for Replica content', {
+ s: s,
+ });
return ReplicaSearchItem.fromJson(category, resp.data);
} else {
logger.e(
diff --git a/lib/replica/logic/uploader.dart b/lib/replica/logic/uploader.dart
index a0013621a..ef2c5ae0d 100644
--- a/lib/replica/logic/uploader.dart
+++ b/lib/replica/logic/uploader.dart
@@ -60,6 +60,9 @@ class ReplicaUploader {
method: UploadMethod.POST,
),
);
+ PlausibleUtils.trackUserAction('User uploaded Replica content', {
+ fileTitle: fileTitle,
+ });
}
// TODO <08-10-22, kalli> Figure out how to query endpoint with infohash (for rendering preview after uploading a file)
diff --git a/lib/replica/ui/viewers/layout.dart b/lib/replica/ui/viewers/layout.dart
index 1fd54a841..97c573441 100644
--- a/lib/replica/ui/viewers/layout.dart
+++ b/lib/replica/ui/viewers/layout.dart
@@ -32,6 +32,9 @@ abstract class ReplicaViewerLayoutState extends State {
// For the Viewers in Replica, we are sending another request to fetch the below params.
// That request goes to `/object_info` endpoint (as opposed it coming bundled in our ReplicaSearchItem)
doFetchObjectInfo();
+ PlausibleUtils.trackUserAction('User viewed Replica content', {
+ 'title': infoTitle,
+ });
}
void doFetchObjectInfo() async {
diff --git a/pubspec.lock b/pubspec.lock
index 16831dac9..440b81fd2 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1085,6 +1085,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
+ plausible_analytics:
+ dependency: "direct main"
+ description:
+ name: plausible_analytics
+ sha256: be9f0b467d23cd94861737f10101431ad8b7d280dc0c14f7251e0e24655b07fa
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.0"
plugin_platform_interface:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 2192591fa..2fb5be89f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -102,6 +102,9 @@ dependencies:
flutter_mailer: ^2.0.0
fluttertoast: ^8.2.2
+ # Analytics
+ plausible_analytics: ^0.3.0
+
# Package information
package_info_plus: ^4.1.0