From 05e9628fede09137d435c8a7306e1ae270c88949 Mon Sep 17 00:00:00 2001 From: atavism Date: Thu, 5 Oct 2023 09:54:17 -0700 Subject: [PATCH 01/17] Remove datadog --- .../mobilesdk/model/SessionManager.kt | 2 -- .../kotlin/io/lantern/model/SessionModel.kt | 5 ---- .../org/getlantern/lantern/MainActivity.kt | 9 ++---- .../lantern/service/LanternService.kt | 3 -- .../getlantern/lantern/util/PaymentsUtil.kt | 27 ++--------------- .../lantern/vpn/LanternVpnService.kt | 3 -- lib/ad_helper.dart | 19 ------------ lib/common/common.dart | 3 -- lib/common/datadog.dart | 29 ------------------- lib/home.dart | 6 ---- 10 files changed, 6 insertions(+), 100 deletions(-) delete mode 100644 lib/common/datadog.dart diff --git a/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt b/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt index 58841b846..1c9292d3a 100644 --- a/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt +++ b/android/app/src/main/java/org/getlantern/mobilesdk/model/SessionManager.kt @@ -24,7 +24,6 @@ import io.lantern.model.BaseModel import io.lantern.model.Vpn import org.getlantern.lantern.BuildConfig import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.model.Bandwidth import org.getlantern.lantern.model.Stats import org.getlantern.lantern.model.Utils @@ -386,7 +385,6 @@ abstract class SessionManager(application: Application) : Session { override fun setCountry(country: String) { prefs.edit().putString(GEO_COUNTRY_CODE, country).apply() - Datadog.setCountry(country) } private val hasUpdatedStats = AtomicBoolean() 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 1ad9ca633..afdfa7c53 100644 --- a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt +++ b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt @@ -23,7 +23,6 @@ import org.getlantern.lantern.MainActivity import org.getlantern.lantern.R import org.getlantern.lantern.activity.FreeKassaActivity_ import org.getlantern.lantern.activity.WebViewActivity_ -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.model.LanternHttpClient import org.getlantern.lantern.model.LanternHttpClient.ProCallback import org.getlantern.lantern.model.LanternHttpClient.ProUserCallback @@ -182,10 +181,6 @@ class SessionModel( } } - "trackUserAction" -> { - Datadog.trackUserClick(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 c61b116b1..5791393db 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -25,7 +25,6 @@ import io.lantern.model.VpnModel import kotlinx.coroutines.* import okhttp3.Response import org.getlantern.lantern.activity.WebViewActivity_ -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.event.EventManager import org.getlantern.lantern.model.AccountInitializationStatus import org.getlantern.lantern.model.Bandwidth @@ -79,7 +78,6 @@ class MainActivity : override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { val start = System.currentTimeMillis() super.configureFlutterEngine(flutterEngine) - FlutterEngineCache.getInstance().put("datadoghq_engine", flutterEngine) messagingModel = MessagingModel(this, flutterEngine) vpnModel = VpnModel(this, flutterEngine, ::switchLantern) sessionModel = SessionModel(this, flutterEngine) @@ -89,7 +87,6 @@ class MainActivity : eventManager = object : EventManager("lantern_event_channel", flutterEngine) { override fun onListen(event: Event) { if (LanternApp.getSession().lanternDidStart()) { - flutterNavigation.invokeMethod("initDatadog", null) fetchLoConf() Logger.debug( TAG, @@ -305,7 +302,7 @@ class MainActivity : private fun updateUserData() { lanternClient.userData(object : ProUserCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { - Datadog.addError("Unable to fetch user data: $error", throwable) + Logger.error(TAG, "Unable to fetch user data: $error", throwable) } override fun onSuccess(response: Response, user: ProUser?) { @@ -330,7 +327,7 @@ class MainActivity : private fun updatePlans() { lanternClient.getPlans(object : PlansCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { - Datadog.addError("Unable to fetch user plans: $error", throwable) + Logger.error(TAG, "Unable to fetch user plans: $error", throwable) } override fun onSuccess(proPlans: Map) { @@ -347,7 +344,7 @@ class MainActivity : private fun updatePaymentMethods() { lanternClient.plansV3(object : PlansV3Callback { override fun onFailure(throwable: Throwable?, error: ProError?) { - Datadog.addError("Unable to fetch payment methods: $error", throwable) + Logger.error(TAG, "Unable to fetch payment methods: $error", throwable) } override fun onSuccess( diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt index a49cd12ba..ac39595ae 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt @@ -13,7 +13,6 @@ import org.androidannotations.annotations.EService import org.getlantern.lantern.BuildConfig import org.getlantern.lantern.LanternApp import org.getlantern.lantern.R -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.model.AccountInitializationStatus import org.getlantern.lantern.model.LanternHttpClient import org.getlantern.lantern.model.LanternStatus @@ -110,8 +109,6 @@ open class LanternService : Service(), Runnable { createUser(0) } - Datadog.initialize() - if (!BuildConfig.PLAY_VERSION && !BuildConfig.DEVELOPMENT_MODE) { // check if an update is available autoUpdater.checkForUpdates() diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt index d116a0dac..02d20db40 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt @@ -17,7 +17,6 @@ import okhttp3.FormBody import okhttp3.Response import org.getlantern.lantern.LanternApp import org.getlantern.lantern.R -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.model.LanternHttpClient import org.getlantern.lantern.model.LanternHttpClient.ProCallback import org.getlantern.lantern.model.LanternSessionManager @@ -41,10 +40,6 @@ class PaymentsUtil(private val activity: Activity) { methodCallResult: MethodChannel.Result, ) { try { - Datadog.trackUserClick("submitStripePayment", mapOf( - "email" to email, - "planID" to planID, - )) val date = expirationDate.split("/").toTypedArray() val card: CardParams = CardParams( cardNumber.replace("[\\s]", ""), @@ -69,7 +64,7 @@ class PaymentsUtil(private val activity: Activity) { override fun onError(@NonNull error: Exception) { dialog?.dismiss() - Datadog.addError("Error submitting to Stripe: $error") + Logger.error(TAG, "Error submitting to Stripe: $error") methodCallResult.error( "errorSubmittingToStripe", error.getLocalizedMessage(), @@ -80,7 +75,7 @@ class PaymentsUtil(private val activity: Activity) { ) } catch (t: Throwable) { dialog?.dismiss() - Datadog.addError("Error submitting to Stripe", t) + Logger.error(TAG, "Error submitting to Stripe", t) methodCallResult.error( "errorSubmittingToStripe", activity.getString(R.string.error_making_purchase), @@ -96,10 +91,6 @@ class PaymentsUtil(private val activity: Activity) { methodCallResult: MethodChannel.Result, ) { try { - Datadog.trackUserClick("submitBitcoinPayment", mapOf( - "email" to email, - "planID" to planID, - )) val provider = PaymentProvider.BTCPay.toString().lowercase() val params = mutableMapOf( "email" to email, @@ -111,7 +102,7 @@ class PaymentsUtil(private val activity: Activity) { LanternHttpClient.createProUrl("/payment-redirect", params), object : ProCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { - Datadog.addError("BTCPay is unavailable", throwable) + Logger.error(TAG, "BTCPay is unavailable", throwable) methodCallResult.error( "unknownError", "BTCPay is unavailable", // This error message is localized Flutter-side @@ -130,7 +121,6 @@ class PaymentsUtil(private val activity: Activity) { }, ) } catch (t: Throwable) { - Datadog.addError("BTCPay is unavailable", t) methodCallResult.error( "unknownError", "BTCPay is unavailable", // This error message is localized Flutter-side @@ -142,9 +132,6 @@ class PaymentsUtil(private val activity: Activity) { // Handles Google Play transactions fun submitGooglePlayPayment(planID: String, methodCallResult: MethodChannel.Result) { val inAppBilling = LanternApp.getInAppBilling() - Datadog.trackUserClick("googlePlayPayment", mapOf( - "planID" to planID, - )) if (inAppBilling == null) { Logger.error(TAG, "Missing inAppBilling") @@ -169,7 +156,6 @@ class PaymentsUtil(private val activity: Activity) { activity.resources.getString(R.string.error_making_purchase), null, ) - Datadog.addError("Google Play: error making purchase") return } @@ -213,7 +199,6 @@ class PaymentsUtil(private val activity: Activity) { formBody, object : ProCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { - Datadog.addError("Error retrieving referral code: $error", throwable) if (error != null && error.message != null) { methodCallResult.error( "unknownError", @@ -235,7 +220,6 @@ class PaymentsUtil(private val activity: Activity) { }, ) } catch (t: Throwable) { - Datadog.addError("Unable to apply referral code", t) methodCallResult.error( "unknownError", "Something went wrong while applying your referral code", @@ -328,11 +312,6 @@ class PaymentsUtil(private val activity: Activity) { override fun onFailure(t: Throwable?, error: ProError?) { Logger.e(TAG, "Error with purchase request: $error") - Datadog.addError("Error with purchase request: $error", t, mapOf( - "provider" to provider.toString().lowercase(), - "plan" to planID, - "deviceName" to session.deviceName(), - )) dialog?.dismiss() methodCallResult.error( "errorMakingPurchase", 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 9b0e29f72..70f551a93 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,7 +7,6 @@ import android.content.ServiceConnection import android.net.VpnService import android.os.IBinder import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.datadog.Datadog import org.getlantern.lantern.service.LanternService_ import org.getlantern.mobilesdk.Logger @@ -59,7 +58,6 @@ class LanternVpnService : VpnService(), Runnable { return START_STICKY } return if (intent.action == ACTION_DISCONNECT) { - Datadog.trackUserTap("switchVPN", mapOf("status" to "disconnect")) stop() START_NOT_STICKY } else { @@ -71,7 +69,6 @@ class LanternVpnService : VpnService(), Runnable { private fun connect() { Logger.d(TAG, "connect") - Datadog.trackUserTap("switchVPN", mapOf("status" to "connect")) Thread(this, "VpnService").start() } diff --git a/lib/ad_helper.dart b/lib/ad_helper.dart index 9802104ec..c9b6f12e4 100644 --- a/lib/ad_helper.dart +++ b/lib/ad_helper.dart @@ -11,7 +11,6 @@ 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/datadog.dart'; import 'package:lantern/replica/common.dart'; enum AdType { Google, CAS } @@ -107,20 +106,13 @@ class AdHelper { ad.fullScreenContentCallback = FullScreenContentCallback( onAdClicked: (ad) { logger.i('[Ads Manager] onAdClicked callback'); - Datadog.trackUserTap( - 'User tapped on interstitial ad', googleAttributes); }, onAdShowedFullScreenContent: (ad) { logger.i('[Ads Manager] Showing Ads'); - Datadog.trackUserCustom( - 'User shown interstitial ad', googleAttributes); }, onAdFailedToShowFullScreenContent: (ad, error) { logger.i( '[Ads Manager] onAdFailedToShowFullScreenContent callback'); - Datadog.addError( - 'Ad failed to show full screen content: $error', - attributes: googleAttributes); //if ads fail to load let user turn on VPN _postShowingAds(); }, @@ -131,13 +123,10 @@ class AdHelper { ); _interstitialAd = ad; logger.i('[Ads Manager] to loaded $ad'); - Datadog.trackUserCustom('Interstitial ad loaded', googleAttributes); }, onAdFailedToLoad: (err) { _failedLoadAttempts++; // increment the count on failure logger.i('[Ads Manager] failed to load $err'); - Datadog.addError('failed to load interstitial ad: $err', - attributes: googleAttributes); _postShowingAds(); }, ), @@ -207,7 +196,6 @@ class AdHelper { if (casMediationManager != null) { await casMediationManager!.loadInterstitial(); logger.i('[Ads Manager] Request: Initiating CAS Interstitial loading.'); - Datadog.trackUserCustom('Interstitial ad loaded', casAttributes); } } @@ -220,16 +208,12 @@ class AdHelper { void _onCASAdShowFailed() { logger.e('[Ads Manager] Error: CAS Interstitial failed to display.'); - Datadog.addError('Failed to display interstitial ad', - attributes: casAttributes); _failedCASLoadAttempts++; _postShowingAds(); // Reload or decide the next action } void _onCASAdClosedOrComplete() { logger.i('[Ads Manager] Completion: CAS Interstitial closed or completed.'); - Datadog.trackUserCustom( - 'Interstitial ad closed or completed', casAttributes); // Reset the counter when the ad successfully shows and closes/completes _failedCASLoadAttempts = 0; _postShowingAds(); @@ -289,14 +273,11 @@ class InterstitialListenerWrapper extends AdCallback { onFailed.call(); logger.i( '[CASIntegrationHelper] - InterstitialListenerWrapper onShowFailed-:$message'); - Datadog.addError('Interstitial ad onShowFailed: $message', - attributes: casAttributes); } @override void onShown() { // Called when ad is shown. logger.i('[CASIntegrationHelper] - InterstitialListenerWrapper onShown'); - Datadog.trackUserCustom('User shown interstitial ad', casAttributes); } } diff --git a/lib/common/common.dart b/lib/common/common.dart index db237aa49..64a1b3506 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -28,11 +28,8 @@ export 'package:provider/provider.dart'; export 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; export 'package:stop_watch_timer/stop_watch_timer.dart'; export 'package:wakelock/wakelock.dart'; -export 'package:datadog_flutter_plugin/datadog_flutter_plugin.dart'; -export 'package:datadog_tracking_http_client/datadog_tracking_http_client.dart'; export 'add_nonbreaking_spaces.dart'; -export 'datadog.dart'; export 'disable_back_button.dart'; export 'iterable_extension.dart'; export 'list_subscriber.dart'; diff --git a/lib/common/datadog.dart b/lib/common/datadog.dart deleted file mode 100644 index 6fbee1efa..000000000 --- a/lib/common/datadog.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:datadog_flutter_plugin/datadog_flutter_plugin.dart'; -import 'package:datadog_tracking_http_client/datadog_tracking_http_client.dart'; - -class Datadog { - static final DatadogSdk _instance = DatadogSdk.instance; - - static trackUserTap(String message, [Map attributes = const {}]) { - _instance.rum?.addUserAction(RumUserActionType.tap, message, attributes); - } - - static trackUserCustom(String message, [Map attributes = const {}]) { - _instance.rum?.addUserAction(RumUserActionType.custom, message, attributes); - } - - // Notifies Datadog that an Exception or Error [error] occurred in the currently - // presented View - static addError( - Object error, { - StackTrace? st, - Map attributes = const {}, - }) { - _instance.rum?.addErrorInfo( - error.toString(), - RumErrorSource.source, - stackTrace: st, - attributes: attributes, - ); - } -} diff --git a/lib/home.dart b/lib/home.dart index 9a407c52a..eb8babf8e 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -97,12 +97,6 @@ class _HomePageState extends State { Future _handleNativeNavigationRequest(MethodCall methodCall) async { switch (methodCall.method) { - case 'initDatadog': - final config = DdSdkExistingConfiguration( - loggingConfiguration: LoggingConfiguration(), - detectLongTasks: true, - )..enableHttpTracking(); - await DatadogSdk.instance.attachToExisting(config); case 'openConversation': final contact = Contact.fromBuffer(methodCall.arguments as Uint8List); await _context!.router.push(Conversation(contactId: contact.contactId)); From 1cac83208e890a51b940a665b6f5edf390439faa Mon Sep 17 00:00:00 2001 From: atavism Date: Thu, 5 Oct 2023 09:59:15 -0700 Subject: [PATCH 02/17] Update CI --- .github/workflows/release.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1e0f394c..e68b31d5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,6 @@ permissions: env: GOPRIVATE: github.com/getlantern S3_BUCKET: lantern - DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }} - DATADOG_SITE: datadoghq.eu jobs: set-version: runs-on: @@ -118,16 +116,9 @@ jobs: fileDir: './android/app' encodedString: ${{ secrets.KEYSTORE }} - - name: Install Datadog CI - run: npm install -g @datadog/datadog-ci - - name: Build Android installers run: make package-android env: - DD_APPLICATION_ID: ${{ secrets.DD_APPLICATION_ID }} - DD_CLIENT_TOKEN: ${{ secrets.DD_CLIENT_TOKEN }} - DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }} - DATADOG_SITE: datadoghq.eu INTERSTITIAL_AD_UNIT: "${{ secrets.INTERSTITIAL_AD_UNIT_ID }}" VERSION: "${{ env.version }}" From 609dce96f248ca9a20a62bed4920dba82764b6c8 Mon Sep 17 00:00:00 2001 From: atavism Date: Thu, 5 Oct 2023 10:00:27 -0700 Subject: [PATCH 03/17] update Makefile --- Makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3ca47d731..0d3273309 100644 --- a/Makefile +++ b/Makefile @@ -314,7 +314,7 @@ $(MOBILE_DEBUG_APK): $(MOBILE_SOURCES) $(GO_SOURCES) make do-android-debug && \ cp $(MOBILE_ANDROID_DEBUG) $(MOBILE_DEBUG_APK) -$(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) require-datadog-ci +$(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) echo $(MOBILE_ANDROID_LIB) && \ mkdir -p ~/.gradle && \ ln -fs $(MOBILE_DIR)/gradle.properties . && \ @@ -328,13 +328,12 @@ $(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) req -PandroidArchJava="$(ANDROID_ARCH_JAVA)" -PproServerUrl=$(PRO_SERVER_URL) -PpaymentProvider=$(PAYMENT_PROVIDER) \ -Pcountry=$(COUNTRY) -PplayVersion=$(FORCE_PLAY_VERSION) -PuseStaging=$(STAGING) -PstickyConfig=$(STICKY_CONFIG) \ -PversionCode=$(VERSION_CODE) -PdevelopmentMode=$(DEVELOPMENT_MODE) -b $(MOBILE_DIR)/app/build.gradle assembleProdSideload && \ - DATADOG_API_KEY=4901456bb88bbf1dc7799eab7d4f71ae DATADOG_SITE=datadoghq.eu datadog-ci flutter-symbols upload --service-name lantern-android --dart-symbols-location build/app/intermediates/merged_native_libs/prodSideload/out/lib \ --android-mapping-location build/app/outputs/mapping/prodSideload/mapping.txt --android-mapping --ios-dsyms && \ cp $(MOBILE_ANDROID_RELEASE) $(MOBILE_RELEASE_APK) && \ cat $(MOBILE_RELEASE_APK) | bzip2 > lantern_update_android_arm.bz2 -$(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) require-datadog-ci +$(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) @mkdir -p ~/.gradle && \ ln -fs $(MOBILE_DIR)/gradle.properties . && \ COUNTRY="$$COUNTRY" && \ From 01b6a92dbbb6a3f0194fbff64a0a45a14db717bc Mon Sep 17 00:00:00 2001 From: atavism Date: Thu, 5 Oct 2023 10:40:16 -0700 Subject: [PATCH 04/17] update Makefile --- Makefile | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 0d3273309..e2d0056cd 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,6 @@ ADB := $(call get-command,adb) OPENSSL := $(call get-command,openssl) GMSAAS := $(call get-command,gmsaas) SENTRY := $(call get-command,sentry-cli) -DATADOGCI := $(call get-command,datadog-ci) BASE64 := $(call get-command,base64) GIT_REVISION_SHORTCODE := $(shell git rev-parse --short HEAD) @@ -90,7 +89,6 @@ PROD_BASE_NAME ?= $(INSTALLER_NAME) ## secrets Keys INTERSTITIAL_AD_UNIT=ca-app-pub-2685698271254859/9922829329 ## vault secrets -VAULT_DD_SECRETS_PATH ?= secret/apps/datadog/android VAULT_ADS_SECRETS_PATH ?= secret/googleAds ## vault keys @@ -245,10 +243,6 @@ require-magick: require-sentry: @if [[ -z "$(SENTRY)" ]]; then echo 'Missing "sentry-cli" command. See sentry.io for installation instructions.'; exit 1; fi -.PHONY: require-datadog-ci -require-datadog-ci: - @if [[ -z "$(DATADOGCI)" ]]; then echo 'Missing "datadog-ci" command. See https://www.npmjs.com/package/@datadog/datadog-ci for installation instructions.'; exit 1; fi - release-autoupdate: require-version @TAG_COMMIT=$$(git rev-list --abbrev-commit -1 $(TAG)) && \ if [[ -z "$$TAG_COMMIT" ]]; then \ @@ -299,7 +293,7 @@ do-android-debug: $(MOBILE_SOURCES) $(MOBILE_ANDROID_LIB) echo "Value of DART_DEFINES is: $$DART_DEFINES" && \ CI="$$CI" && \ echo "Value of CI is: $$CI" && \ - $(GRADLE) -Pdart-defines="$$DART_DEFINES" -PlanternVersion=$(DEBUG_VERSION) -PddClientToken=$$DD_CLIENT_TOKEN -PddApplicationID=$$DD_APPLICATION_ID \ + $(GRADLE) -Pdart-defines="$$DART_DEFINES" -PlanternVersion=$(DEBUG_VERSION) \ -PproServerUrl=$(PRO_SERVER_URL) -PpaymentProvider=$(PAYMENT_PROVIDER) -Pcountry=$(COUNTRY) \ -PplayVersion=$(FORCE_PLAY_VERSION) -PuseStaging=$(STAGING) -PstickyConfig=$(STICKY_CONFIG) \ -PlanternRevisionDate=$(REVISION_DATE) -PandroidArch=$(ANDROID_ARCH) \ @@ -328,7 +322,6 @@ $(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) -PandroidArchJava="$(ANDROID_ARCH_JAVA)" -PproServerUrl=$(PRO_SERVER_URL) -PpaymentProvider=$(PAYMENT_PROVIDER) \ -Pcountry=$(COUNTRY) -PplayVersion=$(FORCE_PLAY_VERSION) -PuseStaging=$(STAGING) -PstickyConfig=$(STICKY_CONFIG) \ -PversionCode=$(VERSION_CODE) -PdevelopmentMode=$(DEVELOPMENT_MODE) -b $(MOBILE_DIR)/app/build.gradle assembleProdSideload && \ - --android-mapping-location build/app/outputs/mapping/prodSideload/mapping.txt --android-mapping --ios-dsyms && \ cp $(MOBILE_ANDROID_RELEASE) $(MOBILE_RELEASE_APK) && \ cat $(MOBILE_RELEASE_APK) | bzip2 > lantern_update_android_arm.bz2 From 509a31ed66839b59fcba0565780d01a2140feacf Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 9 Oct 2023 11:51:23 -0700 Subject: [PATCH 05/17] remove datadog directory --- .../org/getlantern/lantern/datadog/Datadog.kt | 151 ------------------ .../FlutterExcludingComponentPredicate.kt | 22 --- 2 files changed, 173 deletions(-) delete mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/datadog/Datadog.kt delete mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/datadog/FlutterExcludingComponentPredicate.kt diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/datadog/Datadog.kt b/android/app/src/main/kotlin/org/getlantern/lantern/datadog/Datadog.kt deleted file mode 100644 index c98085d2f..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/datadog/Datadog.kt +++ /dev/null @@ -1,151 +0,0 @@ -package org.getlantern.lantern.datadog - -import android.util.Log -import com.datadog.android.DatadogSite -import com.datadog.android.core.configuration.BatchSize -import com.datadog.android.core.configuration.Configuration -import com.datadog.android.core.configuration.Credentials -import com.datadog.android.core.configuration.UploadFrequency -import com.datadog.android.privacy.TrackingConsent -import com.datadog.android.rum.GlobalRum -import com.datadog.android.rum.RumActionType -import com.datadog.android.rum.RumErrorSource -import com.datadog.android.rum.RumMonitor -import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy -import org.getlantern.lantern.LanternApp -import org.getlantern.mobilesdk.Logger -import java.net.InetSocketAddress -import java.net.Proxy -import java.net.URI -import java.util.concurrent.atomic.AtomicBoolean -import com.datadog.android.Datadog as DatadogMain - -object Datadog { - private val tracedHosts = - listOf( - "datadoghq.eu", - "127.0.0.1", - "iantem.io", - "getlantern.org", - "getiantem.org", - "lantern.io", - ) - private val initialized = AtomicBoolean() - private lateinit var datadogConfig: Configuration - - fun initialize() { - if (initialized.get()) return - - DatadogMain.setVerbosity(Log.VERBOSE) - datadogConfig = createDatadogConfiguration() - - val datadogCredentials = - Credentials( - clientToken = "puba617ab01333a95a25a9d3709f04e1654", - envName = "prod", - rumApplicationId = "f8eabf3c-5db3-4f7e-8e6a-5a72433b46d2", - variant = "release", - serviceName = "lantern-android", - ) - - DatadogMain.initialize( - LanternApp.getAppContext(), - credentials = datadogCredentials, - configuration = datadogConfig, - TrackingConsent.GRANTED, - ) - - DatadogMain.setUserInfo( - id = LanternApp.getSession().userId().toString(), - ) - - GlobalRum.registerIfAbsent { - RumMonitor.Builder().build() - } - val session = LanternApp.getSession() - setCountry(session.countryCode) - initialized.set(true) - - // For some reason, sessions don't show up in DataDog RUM until we register a user action - // of some sort. So, here we fire the custom action "started" to get data to start flowing. - GlobalRum.get().addUserAction(RumActionType.CUSTOM, "started", emptyMap()) - } - - fun setCountry(country: String) { - GlobalRum.addAttribute("lantern.country_code", country) - } - - fun addError( - message: String, - throwable: Throwable? = null, - attributes: Map = emptyMap(), - ) { - Logger.e(TAG, message, throwable) - GlobalRum.get().addError(message, RumErrorSource.SOURCE, throwable, attributes) - } - - // trackUserAction is used to track specific user actions (such as taps, clicks, and scrolls) - // with RumMonitor - private fun trackUserAction( - actionType: RumActionType, - name: String, - actionAttributes: Map = emptyMap(), - ) { - GlobalRum.get().addUserAction(actionType, name, actionAttributes) - } - - // trackUserClick is used to track user clicks with RumMonitor - fun trackUserClick( - name: String, - actionAttributes: Map = emptyMap(), - ) { - trackUserAction(RumActionType.CLICK, name, actionAttributes) - } - - // trackUserTap is used to track user taps with RumMonitor - fun trackUserTap( - name: String, - actionAttributes: Map = emptyMap(), - ) { - trackUserAction(RumActionType.TAP, name, actionAttributes) - } - - private fun createDatadogConfiguration(): Configuration { - val session = LanternApp.getSession() - val hTTPAddr = session.hTTPAddr - val uri = URI("http://" + hTTPAddr) - return Configuration.Builder( - logsEnabled = true, - tracesEnabled = true, - crashReportsEnabled = true, - rumEnabled = true, - ) - .setBatchSize(BatchSize.SMALL) - .setProxy( - Proxy( - Proxy.Type.HTTP, - InetSocketAddress( - "127.0.0.1", - uri.getPort(), - ), - ), - null, - ) - .sampleRumSessions(100f) - .setUploadFrequency(UploadFrequency.FREQUENT) - .useSite(DatadogSite.EU1) - .trackBackgroundRumEvents(true) - .trackInteractions() - .trackLongTasks() - .setFirstPartyHosts(tracedHosts) - .useViewTrackingStrategy( - ActivityViewTrackingStrategy( - trackExtras = false, - componentPredicate = FlutterExcludingComponentPredicate(), - ), - ) - .build() - } - - private val TAG = Datadog::class.java.name -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/datadog/FlutterExcludingComponentPredicate.kt b/android/app/src/main/kotlin/org/getlantern/lantern/datadog/FlutterExcludingComponentPredicate.kt deleted file mode 100644 index 49d227731..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/datadog/FlutterExcludingComponentPredicate.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.getlantern.lantern.datadog - -import android.app.Activity -import com.datadog.android.rum.tracking.AcceptAllActivities -import com.datadog.android.rum.tracking.ComponentPredicate -import io.flutter.embedding.android.FlutterActivity - -class FlutterExcludingComponentPredicate : ComponentPredicate { - val innerPredicate = AcceptAllActivities() - - override fun accept(component: Activity): Boolean { - if (component is FlutterActivity) { - return false - } - - return innerPredicate.accept(component) - } - - override fun getViewName(component: Activity): String? { - return innerPredicate.getViewName(component) - } -} From 5fce6666ae68c03ab40528bab3cdf3ca74ec6deb Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 9 Oct 2023 20:19:52 -0700 Subject: [PATCH 06/17] Use Sentry for crash reporting (#927) * update Makefile * Add back Sentry for crash reporting --- .github/workflows/browerstack.yml | 8 ++ .github/workflows/release.yml | 8 ++ Makefile | 6 +- android/app/build.gradle | 33 ++++++- android/app/libs/liblantern-all.aar | 4 +- .../org/getlantern/lantern/LanternApp.kt | 6 +- .../org/getlantern/lantern/util/SentryUtil.kt | 56 +++++++++++ lib/catcher_setup.dart | 44 +++++++++ lib/main.dart | 3 +- pubspec.lock | 95 ++++++++++++------- pubspec.yaml | 8 +- 11 files changed, 223 insertions(+), 48 deletions(-) create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/util/SentryUtil.kt create mode 100644 lib/catcher_setup.dart diff --git a/.github/workflows/browerstack.yml b/.github/workflows/browerstack.yml index cd85f3afa..a34ddb1f1 100644 --- a/.github/workflows/browerstack.yml +++ b/.github/workflows/browerstack.yml @@ -36,6 +36,14 @@ jobs: run: | git config --global url."https://${{ secrets.GH_TOKEN }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" + - name: Setup Sentry CLI + uses: mathieu-bour/setup-sentry-cli@v1 + with: + version: latest + token: ${{ SECRETS.SENTRY_TOKEN }} # from GitHub secrets + organization: getlantern + project: android + - name: Setup JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e68b31d5e..dbc54fd5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,14 @@ jobs: run: | git config --global url."https://${{ secrets.GH_TOKEN }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" + - name: Setup Sentry CLI + uses: mathieu-bour/setup-sentry-cli@v1 + with: + version: latest + token: ${{ SECRETS.SENTRY_TOKEN }} # from GitHub secrets + organization: getlantern + project: android + - name: Setup JDK 11 uses: actions/setup-java@v3 with: diff --git a/Makefile b/Makefile index e2d0056cd..f5d3497a6 100644 --- a/Makefile +++ b/Makefile @@ -308,7 +308,7 @@ $(MOBILE_DEBUG_APK): $(MOBILE_SOURCES) $(GO_SOURCES) make do-android-debug && \ cp $(MOBILE_ANDROID_DEBUG) $(MOBILE_DEBUG_APK) -$(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) +$(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) require-sentry echo $(MOBILE_ANDROID_LIB) && \ mkdir -p ~/.gradle && \ ln -fs $(MOBILE_DIR)/gradle.properties . && \ @@ -322,11 +322,12 @@ $(MOBILE_RELEASE_APK): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) -PandroidArchJava="$(ANDROID_ARCH_JAVA)" -PproServerUrl=$(PRO_SERVER_URL) -PpaymentProvider=$(PAYMENT_PROVIDER) \ -Pcountry=$(COUNTRY) -PplayVersion=$(FORCE_PLAY_VERSION) -PuseStaging=$(STAGING) -PstickyConfig=$(STICKY_CONFIG) \ -PversionCode=$(VERSION_CODE) -PdevelopmentMode=$(DEVELOPMENT_MODE) -b $(MOBILE_DIR)/app/build.gradle assembleProdSideload && \ + sentry-cli upload-dif --wait -o getlantern -p android build/app/intermediates/merged_native_libs/prodSideload/out/lib && \ cp $(MOBILE_ANDROID_RELEASE) $(MOBILE_RELEASE_APK) && \ cat $(MOBILE_RELEASE_APK) | bzip2 > lantern_update_android_arm.bz2 -$(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) +$(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) require-sentry @mkdir -p ~/.gradle && \ ln -fs $(MOBILE_DIR)/gradle.properties . && \ COUNTRY="$$COUNTRY" && \ @@ -336,6 +337,7 @@ $(MOBILE_BUNDLE): $(MOBILE_SOURCES) $(GO_SOURCES) $(MOBILE_ANDROID_LIB) $(GRADLE) -PlanternVersion=$$VERSION -PlanternRevisionDate=$(REVISION_DATE) -PandroidArch=$(ANDROID_ARCH) -PandroidArchJava="$(ANDROID_ARCH_JAVA)" \ -PddClientToken=$(DD_CLIENT_TOKEN) -PddApplicationID=$(DD_APPLICATION_ID) -PproServerUrl=$(PRO_SERVER_URL) -PpaymentProvider=$(PAYMENT_PROVIDER) \ -Pcountry=$(COUNTRY) -PplayVersion=true -PuseStaging=$(STAGING) -PstickyConfig=$(STICKY_CONFIG) -b $(MOBILE_DIR)/app/build.gradle bundlePlay && \ + sentry-cli upload-dif --wait -o getlantern -p android build/app/intermediates/merged_native_libs/prodPlay/out/lib && \ cp $(MOBILE_ANDROID_BUNDLE) $(MOBILE_BUNDLE) android-debug: $(MOBILE_DEBUG_APK) diff --git a/android/app/build.gradle b/android/app/build.gradle index 418f9fde8..97a85d51c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,7 +6,7 @@ plugins { id 'kotlin-parcelize' id 'kotlin-kapt' id 'com.google.protobuf' - id("com.datadoghq.dd-sdk-android-gradle-plugin") version "1.10.0" + id "io.sentry.android.gradle" version "3.13.0" } def localProperties = new Properties() @@ -180,8 +180,6 @@ android { buildConfigField "boolean", "STICKY_CONFIG", getBoolean("stickyConfig") buildConfigField "boolean", "STAGING", getBoolean("useStaging") buildConfigField "boolean", "PLAY_VERSION", getBoolean("playVersion") - buildConfigField "String", "DD_CLIENT_TOKEN", ddClientToken() - buildConfigField "String", "DD_APPLICATION_ID", ddApplicationID() buildConfigField "String", "COUNTRY", userCountry() buildConfigField "String", "PAYMENT_PROVIDER", paymentProvider() buildConfigField "String", "PRO_SERVER_URL", proServerUrl() @@ -473,3 +471,32 @@ dependencies { } apply plugin: 'com.google.gms.google-services' + +sentry { + + // Disables or enables the handling of Proguard mapping for Sentry. + // If enabled the plugin will generate a UUID and will take care of + // uploading the mapping to Sentry. If disabled, all the logic + // related to proguard mapping will be excluded. + // Default is enabled. + includeProguardMapping = true + + + // Whether the plugin should attempt to auto-upload the mapping file to Sentry or not. + // If disabled the plugin will run a dry-run and just generate a UUID. + // The mapping file has to be uploaded manually via sentry-cli in this case. + // Default is enabled. + autoUploadProguardMapping = true + + // Disables or enables the automatic configuration of Native Symbols + // for Sentry. This executes sentry-cli automatically so + // you don't need to do it manually. + // Default is disabled. + uploadNativeSymbols = true + + // Does or doesn't include the source code of native code for Sentry. + // This executes sentry-cli with the --include-sources param. automatically so + // you don't need to do it manually. + // Default is disabled. + includeNativeSources = false +} diff --git a/android/app/libs/liblantern-all.aar b/android/app/libs/liblantern-all.aar index ad352be94..fd4be06b0 100644 --- a/android/app/libs/liblantern-all.aar +++ b/android/app/libs/liblantern-all.aar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c50fcd8ea9c441c0a81ee74117613ad6cbe32d90a1ed6dfba06e6207b8b71fe2 -size 78316381 +oid sha256:091cd2629b6eff2c058dcdf883fb75fa200f00a7cf768e6eae0b2d6b064f926f +size 77696936 diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt index 730fea353..65864e666 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt @@ -3,7 +3,6 @@ package org.getlantern.lantern import android.app.Application import android.content.Context import android.os.StrictMode -import android.util.Log import androidx.appcompat.app.AppCompatDelegate import androidx.multidex.MultiDex import org.getlantern.lantern.model.InAppBilling @@ -11,7 +10,7 @@ import org.getlantern.lantern.model.LanternHttpClient import org.getlantern.lantern.model.LanternSessionManager import org.getlantern.lantern.util.debugOnly import org.getlantern.lantern.util.LanternProxySelector -import org.getlantern.mobilesdk.Logger +import org.getlantern.lantern.util.SentryUtil import org.getlantern.mobilesdk.util.HttpClient open class LanternApp : Application() { @@ -40,13 +39,14 @@ open class LanternApp : Application() { override fun onCreate() { super.onCreate() + SentryUtil.enableGoPanicEnrichment(this) // Necessary to locate a back arrow resource we use from the // support library. See http://stackoverflow.com/questions/37615470/support-library-vectordrawable-resourcesnotfoundexception AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) appContext = applicationContext session = LanternSessionManager(this) - + LanternProxySelector(session) if (session.isPlayVersion) inAppBilling = InAppBilling(this) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/SentryUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/SentryUtil.kt new file mode 100644 index 000000000..6bb485387 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/SentryUtil.kt @@ -0,0 +1,56 @@ +package org.getlantern.lantern.util + +import android.content.Context +import android.os.Process +import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroid +import org.getlantern.mobilesdk.Logger +import java.io.BufferedReader +import java.io.InputStreamReader + +object SentryUtil { + private val TAG = SentryUtil::class.java.name + + /** + * Enables enrichment of sentry crash reports with the most recent Go panic from logcat. + * Keep in mind that Sentry only finds panics the next time that it runs after the process + * actually panicked. So, we can safely exclude logs from our current run. + * + * Keep in mind also that there's no guarantee that the panic log in question belongs to our + * specific panic, we're just picking up the most recent panic log information. + */ + @JvmStatic + fun enableGoPanicEnrichment(ctx: Context) { + SentryAndroid.init(ctx) { options -> + options.beforeSend = SentryOptions.BeforeSendCallback { event, _ -> + // enable enrichment only for exceptions related to OS signals like SIGABRT + if (event.exceptions?.firstOrNull()?.type?.startsWith("SIG") == true) { + val myPid = Process.myPid().toString() + val goErrorLog = StringBuilder() + val process = Runtime.getRuntime().exec( + "logcat -d -v brief" + ) + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + reader.forEachLine { line -> + if (!line.contains(myPid) && line.startsWith("E/Go ")) { + if (line.contains("panic: ")) { + // this is the first line of the most recent panic, remove old rows + // from what must be prior panics + goErrorLog.clear() + } + goErrorLog.appendLine(line) + } + } + } + + if (goErrorLog.isNotEmpty()) { + Logger.debug(TAG, "Attaching latestgopanic to event") + event.setExtra("latestgopanic", goErrorLog.toString()) + } + } + + event + } + } + } +} diff --git a/lib/catcher_setup.dart b/lib/catcher_setup.dart new file mode 100644 index 000000000..ee141d83e --- /dev/null +++ b/lib/catcher_setup.dart @@ -0,0 +1,44 @@ +import 'package:catcher_2/catcher_2.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry/sentry.dart'; + +final debugOption = Catcher2Options( + SilentReportMode(), + [ + ConsoleHandler( + enableApplicationParameters: true, + enableDeviceParameters: true, + enableCustomParameters: true, + enableStackTrace: true, + ), + ], +); + +final releaseOption = Catcher2Options( + SilentReportMode(), + [ + ConsoleHandler( + enableApplicationParameters: true, + enableDeviceParameters: true, + enableCustomParameters: true, + enableStackTrace: true, + ), + SentryHandler( + SentryClient( + SentryOptions( + dsn: + 'https://4753d78f885f4b79a497435907ce4210@o75725.ingest.sentry.io/5850353', + ), + ), + printLogs: true, + ), + ], +); + +Catcher2 setupCatcherAndRun(StatelessWidget root) { + return Catcher2( + rootWidget: root, + debugConfig: debugOption, + releaseConfig: releaseOption, + ); +} diff --git a/lib/main.dart b/lib/main.dart index e96ba72f6..f4f137df9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'package:lantern/app.dart'; +import 'package:lantern/catcher_setup.dart'; import 'package:lantern/common/common.dart'; Future main() async { @@ -14,7 +15,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await _initGoogleMobileAds(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - runApp(LanternApp()); + setupCatcherAndRun(LanternApp()); } Future _initGoogleMobileAds() async { diff --git a/pubspec.lock b/pubspec.lock index fb56a78dc..16831dac9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + catcher_2: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "7be5bb3ea4867f0ab549443511478303e54db4b7" + url: "https://github.com/ThexXTURBOXx/catcher.git" + source: git + version: "1.0.0-alpha.2" characters: dependency: transitive description: @@ -261,10 +270,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" convert: dependency: transitive description: @@ -337,22 +346,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" - datadog_flutter_plugin: - dependency: "direct main" - description: - name: datadog_flutter_plugin - sha256: "2db71372f12f250a549f249046912594f15bf59113dc64a7e2fd5ae85ad90729" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - datadog_tracking_http_client: - dependency: "direct main" - description: - name: datadog_tracking_http_client - sha256: "7274fee927d8ec012a6c4fd3e3333616035236875129f83525fa0bbbe2a66bbb" - url: "https://pub.dev" - source: hosted - version: "1.4.0" dbus: dependency: transitive description: @@ -784,10 +777,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -844,6 +837,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + mailer: + dependency: transitive + description: + name: mailer + sha256: "57f6dd1496699999a7bfd0aa6be0645384f477f4823e16d4321c40a434346382" + url: "https://pub.dev" + source: hosted + version: "6.0.1" markdown: dependency: transitive description: @@ -856,18 +857,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -1180,6 +1181,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + sentry: + dependency: "direct main" + description: + name: sentry + sha256: "830667eadc0398fea3a3424ed1b74568e2db603a42758d0922e2f2974ce55a60" + url: "https://pub.dev" + source: hosted + version: "7.10.1" share_plus: dependency: "direct main" description: @@ -1325,10 +1334,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -1429,26 +1438,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" url: "https://pub.dev" source: hosted - version: "1.24.1" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" timezone: dependency: transitive description: @@ -1473,6 +1482,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -1637,10 +1654,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.7.1" wakelock: dependency: "direct main" description: @@ -1689,6 +1706,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1794,5 +1819,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.3 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1ca26afbe..2192591fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,12 @@ dependencies: google_fonts: flutter_switch: ^0.3.2 flag: ^7.0.0 + # from sdk. See https://github.com/jhomlala/catcher/pull/234 for more details + catcher_2: + git: + url: https://github.com/ThexXTURBOXx/catcher.git + + sentry: ^7.10.1 badges: ^3.1.1 dotted_border: ^2.0.0+3 styled_text: ^8.1.0 @@ -95,8 +101,6 @@ dependencies: device_info_plus: ^9.0.3 flutter_mailer: ^2.0.0 fluttertoast: ^8.2.2 - datadog_flutter_plugin: ^1.4.0 - datadog_tracking_http_client: ^1.4.0 # Package information package_info_plus: ^4.1.0 From 86c68e124a62ea10f84c79987a3eef83c5757438 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 05:04:23 -0700 Subject: [PATCH 07/17] remove datadog from gradle config --- android/app/build.gradle | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 418f9fde8..0c840cabd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -180,8 +180,6 @@ android { buildConfigField "boolean", "STICKY_CONFIG", getBoolean("stickyConfig") buildConfigField "boolean", "STAGING", getBoolean("useStaging") buildConfigField "boolean", "PLAY_VERSION", getBoolean("playVersion") - buildConfigField "String", "DD_CLIENT_TOKEN", ddClientToken() - buildConfigField "String", "DD_APPLICATION_ID", ddApplicationID() buildConfigField "String", "COUNTRY", userCountry() buildConfigField "String", "PAYMENT_PROVIDER", paymentProvider() buildConfigField "String", "PRO_SERVER_URL", proServerUrl() @@ -323,22 +321,6 @@ def getInt(name) { return value.toInteger() } -def ddApplicationID() { - def value = project.getProperties().get("ddApplicationID") - if (value == null || !value?.trim()) { - return "\"\"" - } - return String.format("\"%s\"", value) -} - -def ddClientToken() { - def value = project.getProperties().get("ddClientToken") - if (value == null || !value?.trim()) { - return "\"\"" - } - return String.format("\"%s\"", value) -} - def userCountry() { def value = project.getProperties().get("country") if (value == null || !value?.trim()) { From 771140d9a319b7dd57f4b828b98120bd75aaf3f2 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 05:18:41 -0700 Subject: [PATCH 08/17] Add plausible --- android/app/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 418f9fde8..601e1cc24 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -446,6 +446,8 @@ dependencies { implementation 'com.datadoghq:dd-sdk-android:1.19.3' + implementation 'com.wbrawner.plausible:plausible-android:0.1.0-SNAPSHOT' + annotationProcessor "org.androidannotations:androidannotations:$androidAnnotationsVersion" implementation("org.androidannotations:androidannotations-api:$androidAnnotationsVersion") kapt "org.androidannotations:androidannotations:$androidAnnotationsVersion" From 27e8f3726ec01039c2a05d77bc4abbde6dd0165c Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 07:06:59 -0700 Subject: [PATCH 09/17] integrate plausible analytics --- android/app/build.gradle | 4 ---- android/app/src/main/AndroidManifest.xml | 10 ++++++++++ .../src/main/kotlin/io/lantern/model/SessionModel.kt | 5 +++++ .../main/kotlin/org/getlantern/lantern/MainActivity.kt | 3 ++- .../org/getlantern/lantern/vpn/LanternVpnService.kt | 3 +++ android/app/src/main/res/values/strings.xml | 6 ++++++ android/settings.gradle | 8 ++++++++ 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2662e4808..6fb3ee421 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -426,10 +426,6 @@ dependencies { implementation 'com.stripe:stripe-android:20.17.0' - implementation 'com.datadoghq:dd-sdk-android:1.19.3' - - implementation 'com.wbrawner.plausible:plausible-android:0.1.0-SNAPSHOT' - 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 afdfa7c53..6a09c8cf0 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 5791393db..39bd3401d 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -41,6 +41,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 @@ -87,6 +88,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, @@ -126,7 +128,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/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") From 91400cf5a3d01c7410257e6cfc8169a47d77c9eb Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 07:07:07 -0700 Subject: [PATCH 10/17] integrate plausible analytics --- .../org/getlantern/lantern/plausible/Event.kt | 22 +++ .../getlantern/lantern/plausible/Plausible.kt | 107 +++++++++++ .../lantern/plausible/PlausibleClient.kt | 179 ++++++++++++++++++ .../lantern/plausible/PlausibleConfig.kt | 90 +++++++++ .../lantern/plausible/PlausibleInitializer.kt | 16 ++ 5 files changed, 414 insertions(+) create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/plausible/Event.kt create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/plausible/Plausible.kt create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleConfig.kt create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleInitializer.kt 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..53dfe7412 --- /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) + } + } + } + }) + } + } + + private fun okHttpClient(): OkHttpClient { + 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(), + ), + ) + return 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 From 7d8a005006ec15b8ce61661223c7a47c2af3798e Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 08:18:08 -0700 Subject: [PATCH 11/17] track when ads are shown --- .../org/getlantern/lantern/plausible/PlausibleClient.kt | 6 +++--- lib/ad_helper.dart | 4 ++++ lib/common/common.dart | 1 + pubspec.lock | 8 ++++++++ pubspec.yaml | 3 +++ 5 files changed, 19 insertions(+), 3 deletions(-) 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 index 53dfe7412..a97f56b8a 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/plausible/PlausibleClient.kt @@ -138,7 +138,7 @@ internal class NetworkFirstPlausibleClient( .post(body) .build() suspendCancellableCoroutine { continuation -> - val call = okHttpClient().newCall(request) + val call = okHttpClient.newCall(request) continuation.invokeOnCancellation { call.cancel() } @@ -165,7 +165,7 @@ internal class NetworkFirstPlausibleClient( } } - private fun okHttpClient(): OkHttpClient { + val okHttpClient: OkHttpClient by lazy { val session = LanternApp.getSession() val hTTPAddr = session.hTTPAddr val uri = URI("http://" + hTTPAddr) @@ -174,6 +174,6 @@ internal class NetworkFirstPlausibleClient( uri.getPort(), ), ) - return OkHttpClient.Builder().proxy(proxy).build() + OkHttpClient.Builder().proxy(proxy).build() } } diff --git a/lib/ad_helper.dart b/lib/ad_helper.dart index c9b6f12e4..a3284eccf 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,6 +126,7 @@ 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 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/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 From 62591e346621aedddc34c6f97eac5c7e53547727 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 08:18:48 -0700 Subject: [PATCH 12/17] track when ads are shown --- lib/ad_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ad_helper.dart b/lib/ad_helper.dart index a3284eccf..896d2e503 100644 --- a/lib/ad_helper.dart +++ b/lib/ad_helper.dart @@ -126,7 +126,7 @@ class AdHelper { ); _interstitialAd = ad; logger.i('[Ads Manager] to loaded $ad'); - PlausibleUtils.trackUserAction('Interstitial ad loaded', googleAttributes) + PlausibleUtils.trackUserAction('Interstitial ad loaded', googleAttributes); }, onAdFailedToLoad: (err) { _failedLoadAttempts++; // increment the count on failure From 304a2a9789bc2c6a3e5f4dd61a2b2fd50dc4cf0a Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 10 Oct 2023 08:36:30 -0700 Subject: [PATCH 13/17] add Plausible class --- lib/common/plausible.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/common/plausible.dart diff --git a/lib/common/plausible.dart b/lib/common/plausible.dart new file mode 100644 index 000000000..c61b30542 --- /dev/null +++ b/lib/common/plausible.dart @@ -0,0 +1,9 @@ +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); + } +} \ No newline at end of file From 55fa800d85bce2e2787b53b87c17d06899b60497 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 16 Oct 2023 00:30:06 -0700 Subject: [PATCH 14/17] track when Replica content is viewed --- lib/replica/ui/viewers/layout.dart | 3 +++ 1 file changed, 3 insertions(+) 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 { From aba062a3009d4d4980e022a8767762f17bc4a98a Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 16 Oct 2023 00:42:53 -0700 Subject: [PATCH 15/17] track searches and uploads --- lib/replica/logic/api.dart | 4 +++- lib/replica/logic/uploader.dart | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) 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) From 21c7e5bbabb82fb8a252afcaa7580c47bc730af5 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 16 Oct 2023 00:50:43 -0700 Subject: [PATCH 16/17] Formatting --- lib/common/plausible.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/common/plausible.dart b/lib/common/plausible.dart index c61b30542..22c55b044 100644 --- a/lib/common/plausible.dart +++ b/lib/common/plausible.dart @@ -2,8 +2,9 @@ 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); + Plausible plausible = + Plausible("https://plausible.io", "android.lantern.io"); + // Send goal + plausible.event(name: name, props: props); } -} \ No newline at end of file +} From d63603f11981f76868b8f8d2280062f995450408 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 24 Oct 2023 07:30:49 -0700 Subject: [PATCH 17/17] Track when ads fail to load as well --- lib/ad_helper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ad_helper.dart b/lib/ad_helper.dart index 896d2e503..4ac77acaa 100644 --- a/lib/ad_helper.dart +++ b/lib/ad_helper.dart @@ -131,6 +131,7 @@ class AdHelper { 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(); }, ),