mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-26 15:18:19 +08:00
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:
parent
70976c355a
commit
aad2eed396
1
changelog.d/7076.misc
Normal file
1
changelog.d/7076.misc
Normal file
@ -0,0 +1 @@
|
||||
Add basic integration of Sentry to capture errors and crashes if user has given consent.
|
@ -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",
|
||||
|
@ -148,6 +148,7 @@ ext.groups = [
|
||||
'io.opencensus',
|
||||
'io.reactivex.rxjava2',
|
||||
'io.realm',
|
||||
'io.sentry',
|
||||
'it.unimi.dsi',
|
||||
'jakarta.activation',
|
||||
'jakarta.xml.bind',
|
||||
|
@ -70,6 +70,7 @@ const signOff = "Signed-off-by:"
|
||||
|
||||
// Please add new names following the alphabetical order.
|
||||
const allowList = [
|
||||
"amitkma",
|
||||
"aringenbach",
|
||||
"BillCarsonFr",
|
||||
"bmarty",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -21,4 +21,6 @@ data class AnalyticsConfig(
|
||||
val postHogHost: String,
|
||||
val postHogApiKey: String,
|
||||
val policyLink: String,
|
||||
val sentryDSN: String,
|
||||
val sentryEnvironment: String
|
||||
)
|
||||
|
@ -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) }
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user