Add initial Sentry setup for crashes and perf tracking (#7141)

* Add initial Sentry setup for crashes and perf tracking

* Fix failing analytics tests

* Reformat code to fix style issue

* Close sentry when user signs out

* Add initial unit tests for Sentry

* Remove unused import

* Exclude amitkma from signoff requirements for PRs
This commit is contained in:
Amit Kumar 2022-10-05 16:49:14 +05:30 committed by GitHub
parent 70976c355a
commit aad2eed396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 162 additions and 13 deletions

1
changelog.d/7076.misc Normal file
View file

@ -0,0 +1 @@
Add basic integration of Sentry to capture errors and crashes if user has given consent.

View file

@ -29,6 +29,8 @@ def jjwt = "0.11.5"
// the whole commit which set version 0.16.0-SNAPSHOT // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT" def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.4.1"
def fragment = "1.5.3" def fragment = "1.5.3"
// Testing // Testing
@ -165,6 +167,9 @@ ext.libs = [
apache : [ apache : [
'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator" 'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator"
], ],
sentry: [
'sentryAndroid' : "io.sentry:sentry-android:$sentry"
],
tests : [ tests : [
'kluent' : "org.amshove.kluent:kluent-android:1.68", 'kluent' : "org.amshove.kluent:kluent-android:1.68",
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",

View file

@ -148,6 +148,7 @@ ext.groups = [
'io.opencensus', 'io.opencensus',
'io.reactivex.rxjava2', 'io.reactivex.rxjava2',
'io.realm', 'io.realm',
'io.sentry',
'it.unimi.dsi', 'it.unimi.dsi',
'jakarta.activation', 'jakarta.activation',
'jakarta.xml.bind', 'jakarta.xml.bind',

View file

@ -70,6 +70,7 @@ const signOff = "Signed-off-by:"
// Please add new names following the alphabetical order. // Please add new names following the alphabetical order.
const allowList = [ const allowList = [
"amitkma",
"aringenbach", "aringenbach",
"BillCarsonFr", "BillCarsonFr",
"bmarty", "bmarty",

View file

@ -27,9 +27,9 @@ sealed interface Analytics {
object Disabled : Analytics object Disabled : Analytics
/** /**
* Analytics integration via PostHog. * Analytics integration via PostHog and Sentry.
*/ */
data class PostHog( data class Enabled(
/** /**
* The PostHog instance url. * The PostHog instance url.
*/ */
@ -44,5 +44,15 @@ sealed interface Analytics {
* A URL to more information about the analytics collection. * A URL to more information about the analytics collection.
*/ */
val policyLink: String, val policyLink: String,
/**
* The Sentry DSN url.
*/
val sentryDSN: String,
/**
* Environment for Sentry.
*/
val sentryEnvironment: String
) : Analytics ) : Analytics
} }

View file

@ -68,25 +68,29 @@ object Config {
* The analytics configuration to use for the Debug build type. * The analytics configuration to use for the Debug build type.
* Can be disabled by providing Analytics.Disabled * Can be disabled by providing Analytics.Disabled
*/ */
val DEBUG_ANALYTICS_CONFIG = Analytics.PostHog( val DEBUG_ANALYTICS_CONFIG = Analytics.Enabled(
postHogHost = "https://posthog.element.dev", postHogHost = "https://posthog.element.dev",
postHogApiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", postHogApiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
policyLink = "https://element.io/cookie-policy", 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. * The analytics configuration to use for the Release build type.
* Can be disabled by providing Analytics.Disabled * 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", postHogHost = "https://posthog.hss.element.io",
postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
policyLink = "https://element.io/cookie-policy", 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. * The analytics configuration to use for the Nightly build type.
* Can be disabled by providing Analytics.Disabled * Can be disabled by providing Analytics.Disabled
*/ */
val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY")
} }

View file

@ -231,6 +231,7 @@ dependencies {
implementation('com.posthog.android:posthog:1.1.2') { implementation('com.posthog.android:posthog:1.1.2') {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
} }
implementation libs.sentry.sentryAndroid
// UnifiedPush // UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:2.1.0' implementation 'com.github.UnifiedPush:android-connector:2.1.0'

View file

@ -69,6 +69,9 @@
<application android:supportsRtl="true"> <application android:supportsRtl="true">
<!-- Sentry auto-initialization disable -->
<meta-data android:name="io.sentry.auto-init" android:value="false" />
<!-- No limit for screen ratio: avoid black strips --> <!-- No limit for screen ratio: avoid black strips -->
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"

View file

@ -44,12 +44,14 @@ object ConfigurationModule {
else -> throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}") else -> throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}")
} }
return when (config) { return when (config) {
Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "") Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "", "", "")
is Analytics.PostHog -> AnalyticsConfig( is Analytics.Enabled -> AnalyticsConfig(
isEnabled = true, isEnabled = true,
postHogHost = config.postHogHost, postHogHost = config.postHogHost,
postHogApiKey = config.postHogApiKey, postHogApiKey = config.postHogApiKey,
policyLink = config.policyLink policyLink = config.policyLink,
sentryDSN = config.sentryDSN,
sentryEnvironment = config.sentryEnvironment
) )
} }
} }

View file

@ -21,4 +21,6 @@ data class AnalyticsConfig(
val postHogHost: String, val postHogHost: String,
val postHogApiKey: String, val postHogApiKey: String,
val policyLink: String, val policyLink: String,
val sentryDSN: String,
val sentryEnvironment: String
) )

View file

@ -41,6 +41,7 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
postHogFactory: PostHogFactory, postHogFactory: PostHogFactory,
private val sentryFactory: SentryFactory,
analyticsConfig: AnalyticsConfig, analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@ -94,6 +95,9 @@ class DefaultVectorAnalytics @Inject constructor(
override suspend fun onSignOut() { override suspend fun onSignOut() {
// reset the analyticsId // reset the analyticsId
setAnalyticsId("") setAnalyticsId("")
// Close Sentry SDK.
sentryFactory.stopSentry()
} }
private fun observeAnalyticsId() { private fun observeAnalyticsId() {
@ -123,10 +127,20 @@ class DefaultVectorAnalytics @Inject constructor(
Timber.tag(analyticsTag.value).d("User consent updated to $consent") Timber.tag(analyticsTag.value).d("User consent updated to $consent")
userConsent = consent userConsent = consent
optOutPostHog() optOutPostHog()
initOrStopSentry()
} }
.launchIn(globalScope) .launchIn(globalScope)
} }
private fun initOrStopSentry() {
userConsent?.let {
when (it) {
true -> sentryFactory.initSentry()
false -> sentryFactory.stopSentry()
}
}
}
private fun optOutPostHog() { private fun optOutPostHog() {
userConsent?.let { posthog?.optOut(!it) } userConsent?.let { posthog?.optOut(!it) }
} }

View file

@ -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()
}
}

View file

@ -23,6 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHog
import im.vector.app.test.fakes.FakePostHogFactory 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.AnalyticsConfigFixture.anAnalyticsConfig
import im.vector.app.test.fixtures.aUserProperties import im.vector.app.test.fixtures.aUserProperties
import im.vector.app.test.fixtures.aVectorAnalyticsEvent import im.vector.app.test.fixtures.aVectorAnalyticsEvent
@ -45,9 +46,11 @@ class DefaultVectorAnalyticsTest {
private val fakePostHog = FakePostHog() private val fakePostHog = FakePostHog()
private val fakeAnalyticsStore = FakeAnalyticsStore() private val fakeAnalyticsStore = FakeAnalyticsStore()
private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory() private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
private val fakeSentryFactory = FakeSentryFactory()
private val defaultVectorAnalytics = DefaultVectorAnalytics( private val defaultVectorAnalytics = DefaultVectorAnalytics(
postHogFactory = FakePostHogFactory(fakePostHog.instance).instance, postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
sentryFactory = fakeSentryFactory.instance,
analyticsStore = fakeAnalyticsStore.instance, analyticsStore = fakeAnalyticsStore.instance,
globalScope = CoroutineScope(Dispatchers.Unconfined), globalScope = CoroutineScope(Dispatchers.Unconfined),
analyticsConfig = anAnalyticsConfig(isEnabled = true), analyticsConfig = anAnalyticsConfig(isEnabled = true),
@ -67,17 +70,21 @@ class DefaultVectorAnalyticsTest {
} }
@Test @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) fakeAnalyticsStore.givenUserContent(consent = true)
fakePostHog.verifyOptOutStatus(optedOut = false) fakePostHog.verifyOptOutStatus(optedOut = false)
fakeSentryFactory.verifySentryInit()
} }
@Test @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) fakeAnalyticsStore.givenUserContent(consent = false)
fakePostHog.verifyOptOutStatus(optedOut = true) fakePostHog.verifyOptOutStatus(optedOut = true)
fakeSentryFactory.verifySentryClose()
} }
@Test @Test
@ -97,12 +104,14 @@ class DefaultVectorAnalyticsTest {
} }
@Test @Test
fun `when signing out then resets posthog`() = runTest { fun `when signing out then resets posthog and closes Sentry`() = runTest {
fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow() fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow()
defaultVectorAnalytics.onSignOut() defaultVectorAnalytics.onSignOut()
fakePostHog.verifyReset() fakePostHog.verifyReset()
fakeSentryFactory.verifySentryClose()
} }
@Test @Test

View file

@ -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<SentryFactory>().also {
every { it.initSentry() } answers {
isSentryEnabled = true
}
every { it.stopSentry() } answers {
isSentryEnabled = false
}
}
fun verifySentryInit() {
verify { instance.initSentry() }
}
fun verifySentryClose() {
verify { instance.stopSentry() }
}
}

View file

@ -23,6 +23,8 @@ object AnalyticsConfigFixture {
isEnabled: Boolean = false, isEnabled: Boolean = false,
postHogHost: String = "http://posthog.url", postHogHost: String = "http://posthog.url",
postHogApiKey: String = "api-key", postHogApiKey: String = "api-key",
policyLink: String = "http://policy.link" policyLink: String = "http://policy.link",
) = AnalyticsConfig(isEnabled, postHogHost, postHogApiKey, policyLink) sentryDSN: String = "http://sentry.dsn",
sentryEnvironment: String = "sentry-env"
) = AnalyticsConfig(isEnabled, postHogHost, postHogApiKey, policyLink, sentryDSN, sentryEnvironment)
} }