Add tests for SentryAnalyticsProvider
This commit is contained in:
committed by
Jorge Martin Espinosa
parent
4ef0dfed8c
commit
9a9e84f6c8
@@ -2,6 +2,7 @@ import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.readLocalProperty
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
@@ -54,4 +55,8 @@ dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.services.analyticsproviders.api)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
|
||||
testCommonDependencies(libs, false)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package io.element.android.services.analyticsproviders.sentry
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
@@ -31,6 +32,7 @@ import io.sentry.Breadcrumb
|
||||
import io.sentry.Sentry
|
||||
import io.sentry.SentryOptions
|
||||
import io.sentry.android.core.SentryAndroid
|
||||
import io.sentry.protocol.SentryTransaction
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -56,24 +58,7 @@ class SentryAnalyticsProvider(
|
||||
SentryAndroid.init(context) { options ->
|
||||
options.dsn = dsn
|
||||
options.beforeSendTransaction = SentryOptions.BeforeSendTransactionCallback { transaction, _ ->
|
||||
// Ensure we'll never upload any session ids
|
||||
val possibleSessionIds = transaction.extras?.filter { (it.value as? String)?.startsWith("@") == true }.orEmpty()
|
||||
for (invalidExtra in possibleSessionIds) {
|
||||
transaction.extras?.remove(invalidExtra.key)
|
||||
}
|
||||
|
||||
val sessionId = appNavigationStateService.appNavigationState.value.navigationState.currentSessionId()
|
||||
if (sessionId != null) {
|
||||
// This runs in a separate thread, so although using `runBlocking` is not great, at least it shouldn't freeze the app
|
||||
// Also, the method is fairly quick, so the blocking shouldn't take longer than a few ms
|
||||
val databaseSizes = runBlocking { getDatabaseSizesUseCase(sessionId) }.getOrNull()
|
||||
|
||||
databaseSizes?.stateStore?.let { transaction.setExtra(AnalyticsUserData.STATE_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.eventCacheStore?.let { transaction.setExtra(AnalyticsUserData.EVENT_CACHE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.mediaStore?.let { transaction.setExtra(AnalyticsUserData.MEDIA_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.cryptoStore?.let { transaction.setExtra(AnalyticsUserData.CRYPTO_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
}
|
||||
transaction
|
||||
prepareTransactionBeforeSend(transaction)
|
||||
}
|
||||
options.tracesSampleRate = 1.0
|
||||
options.isEnableUserInteractionTracing = true
|
||||
@@ -128,6 +113,28 @@ class SentryAnalyticsProvider(
|
||||
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? {
|
||||
return SentryAnalyticsTransaction(name, operation)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun prepareTransactionBeforeSend(transaction: SentryTransaction): SentryTransaction {
|
||||
// Ensure we'll never upload any session ids
|
||||
val possibleSessionIds = transaction.extras?.filter { (it.value as? String)?.startsWith("@") == true }.orEmpty()
|
||||
for (invalidExtra in possibleSessionIds) {
|
||||
transaction.removeExtra(invalidExtra.key)
|
||||
}
|
||||
|
||||
val sessionId = appNavigationStateService.appNavigationState.value.navigationState.currentSessionId()
|
||||
if (sessionId != null) {
|
||||
// This runs in a separate thread, so although using `runBlocking` is not great, at least it shouldn't freeze the app
|
||||
// Also, the method is fairly quick, so the blocking shouldn't take longer than a few ms
|
||||
val databaseSizes = runBlocking { getDatabaseSizesUseCase(sessionId) }.getOrNull()
|
||||
|
||||
databaseSizes?.stateStore?.let { transaction.setExtra(AnalyticsUserData.STATE_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.eventCacheStore?.let { transaction.setExtra(AnalyticsUserData.EVENT_CACHE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.mediaStore?.let { transaction.setExtra(AnalyticsUserData.MEDIA_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
databaseSizes?.cryptoStore?.let { transaction.setExtra(AnalyticsUserData.CRYPTO_STORE_SIZE, it.into(ByteUnit.MB)) }
|
||||
}
|
||||
return transaction
|
||||
}
|
||||
}
|
||||
|
||||
private fun BuildType.toSentryEnv() = when (this) {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.services.analyticsproviders.sentry
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.SuperProperties
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.sentry.Sentry
|
||||
import io.sentry.SentryTracer
|
||||
import io.sentry.protocol.SentryId
|
||||
import io.sentry.protocol.SentryTransaction
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SentryAnalyticsProviderTest {
|
||||
@Test
|
||||
fun `init enables Sentry`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
}
|
||||
assertThat(Sentry.isEnabled()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop disables Sentry`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
stop()
|
||||
}
|
||||
assertThat(Sentry.isEnabled()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capture adds a breadcrumb`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
capture(object : VectorAnalyticsEvent {
|
||||
override fun getName(): String = "Test"
|
||||
override fun getProperties(): Map<String, Any?>? = null
|
||||
})
|
||||
}
|
||||
assertThat(Sentry.getCurrentScopes().scope.breadcrumbs.isNotEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen adds a breadcrumb`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
screen(object : VectorAnalyticsScreen {
|
||||
override fun getName(): String = "Test"
|
||||
override fun getProperties(): Map<String, Any>? = null
|
||||
})
|
||||
}
|
||||
assertThat(Sentry.getCurrentScopes().scope.breadcrumbs.isNotEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateUserProperties and updateSuperProperties do nothing`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
updateUserProperties(UserProperties())
|
||||
updateSuperProperties(SuperProperties())
|
||||
}
|
||||
val scope = Sentry.getCurrentScopes().scope
|
||||
assertThat(scope.extras.isEmpty()).isTrue()
|
||||
assertThat(scope.tags.isEmpty()).isTrue()
|
||||
assertThat(scope.contexts.isEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addExtraData adds a global extra`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
addExtraData("foo", "bar")
|
||||
}
|
||||
val scope = Sentry.getCurrentScopes().scope
|
||||
assertThat(scope.extras.get("foo")).isEqualTo("bar")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addIndexableData adds a global tag`() {
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
addIndexableData("foo", "bar")
|
||||
}
|
||||
val scope = Sentry.getCurrentScopes().scope
|
||||
assertThat(scope.tags.get("foo")).isEqualTo("bar")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `trackError adds a throwable to the global scope`() {
|
||||
var initialLastId: SentryId? = null
|
||||
createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
initialLastId = Sentry.getLastEventId()
|
||||
trackError(IllegalStateException("foo"))
|
||||
}
|
||||
assertThat(Sentry.getLastEventId()).isNotEqualTo(initialLastId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startTransaction starts a SentryAnalyticsTransaction`() {
|
||||
val transaction = createSentryAnalyticsProvider().run {
|
||||
init()
|
||||
startTransaction("foo")
|
||||
}
|
||||
assertThat(transaction).isNotNull()
|
||||
assertThat(transaction).isInstanceOf(SentryAnalyticsTransaction::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `prepareTransactionBeforeSend removes unwanted data and adds DB size extras`() {
|
||||
createSentryAnalyticsProvider(
|
||||
getDatabaseSizesUseCase = GetDatabaseSizesUseCase {
|
||||
Result.success(
|
||||
SdkStoreSizes(stateStore = 10.megaBytes, eventCacheStore = 11.megaBytes, mediaStore = 12.megaBytes, cryptoStore = 13.megaBytes)
|
||||
)
|
||||
},
|
||||
appNavigationStateService = FakeAppNavigationStateService(
|
||||
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true))
|
||||
)
|
||||
).run {
|
||||
init()
|
||||
|
||||
val transaction = SentryTransaction(Sentry.startTransaction("foo", "bar") as SentryTracer)
|
||||
// Add a user id value
|
||||
transaction.setExtra("user", "@some:user")
|
||||
|
||||
val result = prepareTransactionBeforeSend(transaction)
|
||||
|
||||
// The user id value should have been removed
|
||||
assertThat(result.getExtra("user")).isNull()
|
||||
|
||||
// The DB sizes should be included
|
||||
assertThat(result.getExtra(AnalyticsUserData.STATE_STORE_SIZE)).isEqualTo(10)
|
||||
assertThat(result.getExtra(AnalyticsUserData.EVENT_CACHE_SIZE)).isEqualTo(11)
|
||||
assertThat(result.getExtra(AnalyticsUserData.MEDIA_STORE_SIZE)).isEqualTo(12)
|
||||
assertThat(result.getExtra(AnalyticsUserData.CRYPTO_STORE_SIZE)).isEqualTo(13)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSentryAnalyticsProvider(
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
getDatabaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
||||
appNavigationStateService: FakeAppNavigationStateService = FakeAppNavigationStateService(),
|
||||
) = SentryAnalyticsProvider(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
buildMeta = buildMeta,
|
||||
getDatabaseSizesUseCase = getDatabaseSizesUseCase,
|
||||
appNavigationStateService = appNavigationStateService,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user