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
|
// 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",
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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.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
|
||||||
|
@ -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,
|
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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user