Add tests for SentryAnalyticsProvider
This commit is contained in:
parent
4ef0dfed8c
commit
9a9e84f6c8
3 changed files with 203 additions and 18 deletions
|
|
@ -2,6 +2,7 @@ import config.BuildTimeConfig
|
||||||
import extension.buildConfigFieldStr
|
import extension.buildConfigFieldStr
|
||||||
import extension.readLocalProperty
|
import extension.readLocalProperty
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
|
@ -54,4 +55,8 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.services.analyticsproviders.api)
|
implementation(projects.services.analyticsproviders.api)
|
||||||
implementation(projects.services.appnavstate.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
|
package io.element.android.services.analyticsproviders.sentry
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesIntoSet
|
import dev.zacsweers.metro.ContributesIntoSet
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
|
@ -31,6 +32,7 @@ import io.sentry.Breadcrumb
|
||||||
import io.sentry.Sentry
|
import io.sentry.Sentry
|
||||||
import io.sentry.SentryOptions
|
import io.sentry.SentryOptions
|
||||||
import io.sentry.android.core.SentryAndroid
|
import io.sentry.android.core.SentryAndroid
|
||||||
|
import io.sentry.protocol.SentryTransaction
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -56,24 +58,7 @@ class SentryAnalyticsProvider(
|
||||||
SentryAndroid.init(context) { options ->
|
SentryAndroid.init(context) { options ->
|
||||||
options.dsn = dsn
|
options.dsn = dsn
|
||||||
options.beforeSendTransaction = SentryOptions.BeforeSendTransactionCallback { transaction, _ ->
|
options.beforeSendTransaction = SentryOptions.BeforeSendTransactionCallback { transaction, _ ->
|
||||||
// Ensure we'll never upload any session ids
|
prepareTransactionBeforeSend(transaction)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
options.tracesSampleRate = 1.0
|
options.tracesSampleRate = 1.0
|
||||||
options.isEnableUserInteractionTracing = true
|
options.isEnableUserInteractionTracing = true
|
||||||
|
|
@ -128,6 +113,28 @@ class SentryAnalyticsProvider(
|
||||||
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? {
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? {
|
||||||
return SentryAnalyticsTransaction(name, operation)
|
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) {
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue