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
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",

View file

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

View file

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

View file

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

View file

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

View file

@ -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'

View file

@ -69,6 +69,9 @@
<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 -->
<meta-data
android:name="android.max_aspect"

View file

@ -44,12 +44,14 @@ object ConfigurationModule {
else -> 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
)
}
}

View file

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

View file

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

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.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

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,
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)
}