diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 2c03f87d424..d74d2594db4 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -24,6 +24,8 @@ plugins { alias(libs.plugins.com.diffplug.spotless) apply false alias(libs.plugins.android.junit5) apply false alias(libs.plugins.org.jetbrains.kotlin.plugin.compose) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.firebase.crashlytics) apply false } tasks.dokkaHtmlMultiModule { diff --git a/android/buildSrc/src/main/kotlin/project-properties.gradle.kts b/android/buildSrc/src/main/kotlin/project-properties.gradle.kts index bc186f2337a..7828cec109c 100644 --- a/android/buildSrc/src/main/kotlin/project-properties.gradle.kts +++ b/android/buildSrc/src/main/kotlin/project-properties.gradle.kts @@ -26,7 +26,6 @@ val requiredFhirProperties = "OAUTH_CLIENT_ID", "OAUTH_SCOPE", "MAPBOX_SDK_TOKEN", - "SENTRY_DSN", "OPENSRP_APP_ID", "GEMINI_API_KEY", "SPEECH_TO_TEXT_API_KEY", @@ -58,23 +57,3 @@ requiredKeystoreProperties.forEach { property -> project.extra.set(property, keystoreProperties.getProperty(property, "sample_$property")) } -// Set Sentry properties -val requiredSentryProperties = - listOf( - "org", - "project", - "auth.token", - "url" - ) - -val sentryProperties = try { - readProperties((project.properties["sentryPropertiesFile"] ?: "${rootProject.projectDir}/sentry.properties").toString()) -} catch (e: FileNotFoundException) { - if (project.properties["sentryPropertiesFile"] != null) { - throw e - } else Properties() -} - -requiredSentryProperties.forEach { property -> - project.extra.set(property, sentryProperties.getProperty(property, "sentry_$property")) -} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index bc5581098a8..03ce5193791 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -95,6 +95,9 @@ uiautomator = "2.3.0" work = "2.9.1" xercesImpl = "2.12.2" zip4j = "2.11.5" +firebase-bom = "34.2.0" +firebase-crashlytics-gradle = "3.0.6" +google-services = "4.4.3" [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } @@ -226,6 +229,9 @@ workflow = { group = "org.smartregister", name = "workflow", version.ref = "fhir xercesImpl = { group = "xerces", name = "xercesImpl", version.ref = "xercesImpl" } androidx-fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "compose-fragment" } zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" } +firebase-crashlytics-ndk = { group = "com.google.firebase", name = "firebase-crashlytics-ndk" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } [plugins] @@ -239,6 +245,8 @@ org-jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "jetbrains" } org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } org-owasp-dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "owasp" } org-jetbrains-kotlin-plugin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" } [bundles] accompanist = ["accompanist-placeholder", "accompanist-flowlayout"] diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 7fe8627ff1c..76c70bc6879 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -1,6 +1,4 @@ import com.android.build.api.variant.FilterConfiguration.FilterType -import io.sentry.android.gradle.extensions.InstrumentationFeature -import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel import java.io.FileReader import java.text.SimpleDateFormat import java.util.Date @@ -22,10 +20,11 @@ plugins { id("dagger.hilt.android.plugin") id("androidx.navigation.safeargs") id("org.sonarqube") version "3.5.0.2730" - id("io.sentry.android.gradle") version "3.11.1" id("com.google.devtools.ksp") id("com.google.dagger.hilt.android") alias(libs.plugins.org.jetbrains.kotlin.plugin.compose) + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") } sonar { @@ -77,7 +76,6 @@ android { buildConfigField("String", "OAUTH_SCOPE", """"${project.extra["OAUTH_SCOPE"]}"""") buildConfigField("String", "OPENSRP_APP_ID", """${project.extra["OPENSRP_APP_ID"]}""") buildConfigField("String", "CONFIGURATION_SYNC_PAGE_SIZE", """"100"""") - buildConfigField("String", "SENTRY_DSN", """"${project.extra["SENTRY_DSN"]}"""") buildConfigField("String", "BUILD_DATE", "\"$buildDate\"") buildConfigField("String", "GEMINI_API_KEY", """"${project.extra["GEMINI_API_KEY"]}"""") buildConfigField( @@ -559,6 +557,9 @@ dependencies { implementation(libs.androidx.fragment.compose) implementation(libs.bundles.cameraX) implementation(libs.log4j) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.crashlytics.ndk) + implementation(libs.firebase.analytics) // AI dependencies implementation(libs.google.cloud.speech) { @@ -706,154 +707,6 @@ tasks.register("evaluatePerformanceBenchmarkResults") { } } -sentry { - // Disables or enables debug log output, e.g. for for sentry-cli. - // Default is disabled. - debug.set(true) - - // The slug of the Sentry organization to use for uploading proguard mappings/source contexts. - org.set("""${project.extra["org"]}""") - - // The slug of the Sentry project to use for uploading proguard mappings/source contexts. - projectName.set("""${project.extra["project"]}""") - - // The authentication token to use for uploading proguard mappings/source contexts. - // WARNING: Do not expose this token in your build.gradle files, but rather set an environment - // variable and read it into this property. - authToken.set("""${project.extra["auth.token"]}""") - - // The url of your Sentry instance. If you're using SAAS (not self hosting) you do not have to - // set this. If you are self hosting you can set your URL here - // url = null - - // 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.set(false) - - // 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.set(false) - - // Experimental flag to turn on support for GuardSquare's tools integration (Dexguard and - // External Proguard). - // If enabled, the plugin will try to consume and upload the mapping file produced by Dexguard - // and External Proguard. - // Default is disabled. - // dexguardEnabled.set(false) - - // 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.set(false) - - // Whether the plugin should attempt to auto-upload the native debug symbols to Sentry or not. - // If disabled the plugin will run a dry-run. - // Default is enabled. - autoUploadNativeSymbols.set(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.set(false) - - // Generates a JVM (Java, Kotlin, etc.) source bundle and uploads your source code to Sentry. - // This enables source context, allowing you to see your source - // code as part of your stack traces in Sentry. - includeSourceContext.set(false) - - // Configure additional directories to be included in the source bundle which is used for - // source context. The directories should be specified relative to the Gradle module/project's - // root. For example, if you have a custom source set alongside 'main', the parameter would be - // 'src/custom/java'. - additionalSourceDirsForSourceContext.set(emptySet()) - - // Enable or disable the tracing instrumentation. - // Does auto instrumentation for specified features through bytecode manipulation. - // Default is enabled. - tracingInstrumentation { - enabled.set(true) - - // Specifies a set of instrumentation features that are eligible for bytecode manipulation. - // Defaults to all available values of InstrumentationFeature enum class. - features.set( - setOf( - InstrumentationFeature.DATABASE, - InstrumentationFeature.FILE_IO, - InstrumentationFeature.OKHTTP, - InstrumentationFeature.COMPOSE, - ), - ) - - // Enable or disable logcat instrumentation through bytecode manipulation. - // Default is enabled. - logcat { - enabled.set(true) - - // Specifies a minimum log level for the logcat breadcrumb logging. - // Defaults to LogcatLevel.WARNING. - minLevel.set(LogcatLevel.WARNING) - } - - // The set of glob patterns to exclude from instrumentation. Classes matching any of these - // patterns in the project's sources and dependencies JARs won't be instrumented by the - // Sentry Gradle plugin. - // - // Don't include the file extension. Filtering is done on compiled classes and - // the .class suffix isn't included in the pattern matching. - // - // Example usage: - // ``` - // excludes.set(setOf("com/example/donotinstrument/**", "**/*Test")) - // ``` - // - // Only supported when using Android Gradle plugin (AGP) version 7.4.0 and above. - // excludes.set(emptySet()) - } - - // Enable auto-installation of Sentry components (sentry-android SDK and okhttp, timber, fragment - // and compose integrations). - // Default is enabled. - // Only available v3.1.0 and above. - autoInstallation { - enabled.set(true) - - // Specifies a version of the sentry-android SDK and fragment, timber and okhttp integrations. - // - // This is also useful, when you have the sentry-android SDK already included into a transitive - // dependency/module and want to - // align integration versions with it (if it's a direct dependency, the version will be - // inferred). - // - // NOTE: if you have a higher version of the sentry-android SDK or integrations on the - // classpath, this setting will have no effect - // as Gradle will resolve it to the latest version. - // - // Defaults to the latest published Sentry version. - sentryVersion.set("7.14.0") - } - - // Disables or enables dependencies metadata reporting for Sentry. - // If enabled, the plugin will collect external dependencies and - // upload them to Sentry as part of events. If disabled, all the logic - // related to the dependencies metadata report will be excluded. - // - // Default is enabled. - includeDependenciesReport.set(true) - - // Whether the plugin should send telemetry data to Sentry. - // If disabled the plugin won't send telemetry data. - // This is auto disabled if running against a self hosted instance of Sentry. - // Default is enabled. - // telemetry.set(true) -} - fun JSONObject.getTestName(): String { val className = getString("className").substringAfterLast(".") val methodName = getString("name").substringAfterLast("_") diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index 47b29ba70cd..79540f2d001 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -34,9 +34,6 @@ android:enabled="true" tools:targetApi="29" /> - - options.dsn = dsn.trim { it <= ' ' } - // To set a uniform sample rate - options.tracesSampleRate = 1.0 - options.isEnableUserInteractionTracing = true - options.isEnableUserInteractionBreadcrumbs = true - options.addIntegration( - FragmentLifecycleIntegration( - this, - enableFragmentLifecycleBreadcrumbs = true, - enableAutoFragmentLifecycleTracing = true, - ), - ) - try { - options.environment = URL(BuildConfig.FHIR_BASE_URL).getSubDomain().replace('-', '.') - } catch (e: Exception) { - Timber.e(e) - } - } - - SentryAndroid.init(this, sentryConfiguration) + private fun initFirebaseCrashlytics() { + try { + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true) + Timber.d("Firebase Crashlytics initialized successfully") + } catch (e: Exception) { + Timber.e(e, "Failed to initialize Firebase Crashlytics") } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ReleaseTree.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ReleaseTree.kt index 560a0ced3cc..21044bc8de2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ReleaseTree.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ReleaseTree.kt @@ -18,7 +18,7 @@ package org.smartregister.fhircore.quest import android.util.Log import android.util.Log.ERROR -import io.sentry.Sentry +import com.google.firebase.crashlytics.FirebaseCrashlytics import timber.log.Timber class ReleaseTree : Timber.Tree() { @@ -33,7 +33,9 @@ class ReleaseTree : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { if (priority == ERROR || priority == Log.WARN || priority == Log.ASSERT) { if (throwable != null) { - Sentry.captureException(throwable) + FirebaseCrashlytics.getInstance().recordException(throwable) + } else { + FirebaseCrashlytics.getInstance().log("$tag: $message") } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt index 3dac0b323a3..8f43b3dfbb0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt @@ -28,8 +28,6 @@ import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import dagger.hilt.android.lifecycle.HiltViewModel -import io.sentry.Sentry -import io.sentry.protocol.User import java.net.SocketTimeoutException import java.net.UnknownHostException import java.util.concurrent.TimeUnit @@ -68,7 +66,6 @@ import org.smartregister.fhircore.engine.util.extension.practitionerEndpointUrl import org.smartregister.fhircore.engine.util.extension.removeHashPrefix import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.extension.valueToString -import org.smartregister.fhircore.quest.BuildConfig import org.smartregister.model.location.LocationHierarchy import org.smartregister.model.practitioner.PractitionerDetails import retrofit2.HttpException @@ -171,16 +168,6 @@ constructor( } else if ( accountAuthenticator.validateLoginCredentials(trimmedUsername, passwordAsCharArray) ) { - try { - // Configure Sentry scope - Sentry.configureScope { scope -> - scope.setTag("versionCode", BuildConfig.VERSION_CODE.toString()) - scope.setTag("versionName", BuildConfig.VERSION_NAME) - scope.user = User().apply { username = trimmedUsername } - } - } catch (e: Exception) { - Timber.e(e) - } _showProgressBar.postValue(false) updateNavigateHome(true) } else { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt index 58d4b1b0f43..884c71e0d89 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt @@ -30,13 +30,11 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope -import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import dagger.hilt.android.AndroidEntryPoint -import io.sentry.android.navigation.SentryNavigationListener import java.time.Instant import javax.inject.Inject import kotlinx.coroutines.async @@ -84,8 +82,6 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, @Inject lateinit var dispatcherProvider: DispatcherProvider val appMainViewModel by viewModels() - private val sentryNavListener = - SentryNavigationListener(enableNavigationBreadcrumbs = true, enableNavigationTracing = true) private val locationPermissionLauncher: ActivityResultLauncher> = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { @@ -163,13 +159,11 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, override fun onResume() { super.onResume() - findNavController(R.id.nav_host).addOnDestinationChangedListener(sentryNavListener) syncListenerManager.registerSyncListener(this, lifecycle) } override fun onPause() { super.onPause() - findNavController(R.id.nav_host).removeOnDestinationChangedListener(sentryNavListener) } override fun onQuestionnaireLaunched(questionnaireConfig: QuestionnaireConfig) { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestApplicationTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestApplicationTest.kt index 34616b0c359..171faef6f1f 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestApplicationTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/QuestApplicationTest.kt @@ -19,13 +19,7 @@ package org.smartregister.fhircore.quest import android.content.Intent import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.spyk -import io.sentry.Sentry -import io.sentry.android.core.SentryAndroid -import io.sentry.android.core.SentryAndroidOptions import kotlin.test.assertNotNull import org.junit.Assert import org.junit.Before @@ -48,30 +42,6 @@ class QuestApplicationTest : RobolectricTest() { application.xFhirQueryResolver = mockk() } - @Test - fun testSentryMonitoringWhenDsnNotBlank() { - val sentryDsn = "debb3087-167a-47ff-b6d4-737be3965a4c" - val spyApp = spyk(application) - val sentryOptions = spyk() - - mockkStatic(SentryAndroid::class) { - every { - SentryAndroid.init(any(), any>()) - } answers - { - val optionsConfiguration = secondArg>() - optionsConfiguration.configure(sentryOptions) - } - - spyApp.initSentryMonitoring(dsn = sentryDsn) - - Assert.assertEquals(sentryDsn, sentryOptions.dsn) - Assert.assertEquals(1.0, sentryOptions.tracesSampleRate) - Assert.assertTrue(sentryOptions.isEnableUserInteractionTracing) - Assert.assertTrue(sentryOptions.isEnableUserInteractionBreadcrumbs) - } - } - @Test fun testGetDataCaptureConfig() { val config = application.getDataCaptureConfig()