diff --git a/changelog.d/7076.misc b/changelog.d/7076.misc new file mode 100644 index 0000000000..009b24b149 --- /dev/null +++ b/changelog.d/7076.misc @@ -0,0 +1 @@ +Add basic integration of Sentry to capture errors and crashes if user has given consent. diff --git a/dependencies.gradle b/dependencies.gradle index 610f8b97aa..3bf3ab746d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -29,6 +29,8 @@ def jjwt = "0.11.5" // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" +def sentry = "6.4.1" + def fragment = "1.5.3" // Testing @@ -165,6 +167,9 @@ ext.libs = [ apache : [ 'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator" ], + sentry: [ + 'sentryAndroid' : "io.sentry:sentry-android:$sentry" + ], tests : [ 'kluent' : "org.amshove.kluent:kluent-android:1.68", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 149a1fbac5..cdab6172d1 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -148,6 +148,7 @@ ext.groups = [ 'io.opencensus', 'io.reactivex.rxjava2', 'io.realm', + 'io.sentry', 'it.unimi.dsi', 'jakarta.activation', 'jakarta.xml.bind', diff --git a/tools/danger/dangerfile.js b/tools/danger/dangerfile.js index 6314ec8f68..1a36474470 100644 --- a/tools/danger/dangerfile.js +++ b/tools/danger/dangerfile.js @@ -70,6 +70,7 @@ const signOff = "Signed-off-by:" // Please add new names following the alphabetical order. const allowList = [ + "amitkma", "aringenbach", "BillCarsonFr", "bmarty", diff --git a/vector-config/src/main/java/im/vector/app/config/Analytics.kt b/vector-config/src/main/java/im/vector/app/config/Analytics.kt index 7fdc78dc8a..d944a84f94 100644 --- a/vector-config/src/main/java/im/vector/app/config/Analytics.kt +++ b/vector-config/src/main/java/im/vector/app/config/Analytics.kt @@ -27,9 +27,9 @@ sealed interface Analytics { object Disabled : Analytics /** - * Analytics integration via PostHog. + * Analytics integration via PostHog and Sentry. */ - data class PostHog( + data class Enabled( /** * The PostHog instance url. */ @@ -44,5 +44,15 @@ sealed interface Analytics { * A URL to more information about the analytics collection. */ val policyLink: String, + + /** + * The Sentry DSN url. + */ + val sentryDSN: String, + + /** + * Environment for Sentry. + */ + val sentryEnvironment: String ) : Analytics } diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index f660799d06..c91987dbfd 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -68,25 +68,29 @@ object Config { * The analytics configuration to use for the Debug build type. * Can be disabled by providing Analytics.Disabled */ - val DEBUG_ANALYTICS_CONFIG = Analytics.PostHog( + val DEBUG_ANALYTICS_CONFIG = Analytics.Enabled( postHogHost = "https://posthog.element.dev", postHogApiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", policyLink = "https://element.io/cookie-policy", + sentryDSN = "https://f6acc9cfc2024641b28c87ad95e73e66@sentry.tools.element.io/49", + sentryEnvironment = "DEBUG" ) /** * The analytics configuration to use for the Release build type. * Can be disabled by providing Analytics.Disabled */ - val RELEASE_ANALYTICS_CONFIG = Analytics.PostHog( + val RELEASE_ANALYTICS_CONFIG = Analytics.Enabled( postHogHost = "https://posthog.hss.element.io", postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", policyLink = "https://element.io/cookie-policy", + sentryDSN = "https://f6acc9cfc2024641b28c87ad95e73e66@sentry.tools.element.io/49", + sentryEnvironment = "RELEASE" ) /** * The analytics configuration to use for the Nightly build type. * Can be disabled by providing Analytics.Disabled */ - val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG + val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") } diff --git a/vector/build.gradle b/vector/build.gradle index e10d2a3436..0ddee2428a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -231,6 +231,7 @@ dependencies { implementation('com.posthog.android:posthog:1.1.2') { exclude group: 'com.android.support', module: 'support-annotations' } + implementation libs.sentry.sentryAndroid // UnifiedPush implementation 'com.github.UnifiedPush:android-connector:2.1.0' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index dbc6458713..f079d3429e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -69,6 +69,9 @@ + + + throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}") } return when (config) { - Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "") - is Analytics.PostHog -> AnalyticsConfig( + Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "", "", "") + is Analytics.Enabled -> AnalyticsConfig( isEnabled = true, postHogHost = config.postHogHost, postHogApiKey = config.postHogApiKey, - policyLink = config.policyLink + policyLink = config.policyLink, + sentryDSN = config.sentryDSN, + sentryEnvironment = config.sentryEnvironment ) } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt index bffba6fa9c..cc3eed306d 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt @@ -21,4 +21,6 @@ data class AnalyticsConfig( val postHogHost: String, val postHogApiKey: String, val policyLink: String, + val sentryDSN: String, + val sentryEnvironment: String ) diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index be847dcb7f..553d699d86 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -41,6 +41,7 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( postHogFactory: PostHogFactory, + private val sentryFactory: SentryFactory, analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, @@ -94,6 +95,9 @@ class DefaultVectorAnalytics @Inject constructor( override suspend fun onSignOut() { // reset the analyticsId setAnalyticsId("") + + // Close Sentry SDK. + sentryFactory.stopSentry() } private fun observeAnalyticsId() { @@ -123,10 +127,20 @@ class DefaultVectorAnalytics @Inject constructor( Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent optOutPostHog() + initOrStopSentry() } .launchIn(globalScope) } + private fun initOrStopSentry() { + userConsent?.let { + when (it) { + true -> sentryFactory.initSentry() + false -> sentryFactory.stopSentry() + } + } + } + private fun optOutPostHog() { userConsent?.let { posthog?.optOut(!it) } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt new file mode 100644 index 0000000000..a000f2a77a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.impl + +import android.content.Context +import im.vector.app.features.analytics.AnalyticsConfig +import im.vector.app.features.analytics.log.analyticsTag +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroid +import timber.log.Timber +import javax.inject.Inject + +class SentryFactory @Inject constructor( + private val context: Context, + private val analyticsConfig: AnalyticsConfig, +) { + + fun initSentry() { + Timber.tag(analyticsTag.value).d("Initializing Sentry") + if (Sentry.isEnabled()) return + SentryAndroid.init(context) { options -> + options.dsn = analyticsConfig.sentryDSN + options.beforeSend = SentryOptions.BeforeSendCallback { event, _ -> event } + options.tracesSampleRate = 1.0 + options.isEnableUserInteractionTracing = true + options.environment = analyticsConfig.sentryEnvironment + options.diagnosticLevel + } + } + + fun stopSentry() { + Timber.tag(analyticsTag.value).d("Stopping Sentry") + Sentry.close() + } +} diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt index 543d517db1..be53f1b908 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -23,6 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHogFactory +import im.vector.app.test.fakes.FakeSentryFactory import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig import im.vector.app.test.fixtures.aUserProperties import im.vector.app.test.fixtures.aVectorAnalyticsEvent @@ -45,9 +46,11 @@ class DefaultVectorAnalyticsTest { private val fakePostHog = FakePostHog() private val fakeAnalyticsStore = FakeAnalyticsStore() private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory() + private val fakeSentryFactory = FakeSentryFactory() private val defaultVectorAnalytics = DefaultVectorAnalytics( postHogFactory = FakePostHogFactory(fakePostHog.instance).instance, + sentryFactory = fakeSentryFactory.instance, analyticsStore = fakeAnalyticsStore.instance, globalScope = CoroutineScope(Dispatchers.Unconfined), analyticsConfig = anAnalyticsConfig(isEnabled = true), @@ -67,17 +70,21 @@ class DefaultVectorAnalyticsTest { } @Test - fun `when consenting to analytics then updates posthog opt out to false`() = runTest { + fun `when consenting to analytics then updates posthog opt out to false and initialize Sentry`() = runTest { fakeAnalyticsStore.givenUserContent(consent = true) fakePostHog.verifyOptOutStatus(optedOut = false) + + fakeSentryFactory.verifySentryInit() } @Test - fun `when revoking consent to analytics then updates posthog opt out to true`() = runTest { + fun `when revoking consent to analytics then updates posthog opt out to true and closes Sentry`() = runTest { fakeAnalyticsStore.givenUserContent(consent = false) fakePostHog.verifyOptOutStatus(optedOut = true) + + fakeSentryFactory.verifySentryClose() } @Test @@ -97,12 +104,14 @@ class DefaultVectorAnalyticsTest { } @Test - fun `when signing out then resets posthog`() = runTest { + fun `when signing out then resets posthog and closes Sentry`() = runTest { fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow() defaultVectorAnalytics.onSignOut() fakePostHog.verifyReset() + + fakeSentryFactory.verifySentryClose() } @Test diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt new file mode 100644 index 0000000000..2628f80435 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.analytics.impl.SentryFactory +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class FakeSentryFactory { + private var isSentryEnabled = false + + val instance = mockk().also { + every { it.initSentry() } answers { + isSentryEnabled = true + } + + every { it.stopSentry() } answers { + isSentryEnabled = false + } + } + + fun verifySentryInit() { + verify { instance.initSentry() } + } + + fun verifySentryClose() { + verify { instance.stopSentry() } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt index ea1769ecb2..a53043774d 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt @@ -23,6 +23,8 @@ object AnalyticsConfigFixture { isEnabled: Boolean = false, postHogHost: String = "http://posthog.url", postHogApiKey: String = "api-key", - policyLink: String = "http://policy.link" - ) = AnalyticsConfig(isEnabled, postHogHost, postHogApiKey, policyLink) + policyLink: String = "http://policy.link", + sentryDSN: String = "http://sentry.dsn", + sentryEnvironment: String = "sentry-env" + ) = AnalyticsConfig(isEnabled, postHogHost, postHogApiKey, policyLink, sentryDSN, sentryEnvironment) }