Add metrics plugin to track device download keys task (#7438)

* Add metrics tracking plugin for download device keys

* Add support for multiple metrics plugin

* Update copyright license header in matrix-sdk-android

* Add tests for MetricExtension

* Update changelog

* Improve MetricsExtension and reformatting
This commit is contained in:
Amit Kumar 2022-11-02 13:43:57 +05:30 committed by GitHub
parent 646cc7d67d
commit b6746653f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 236 additions and 18 deletions

1
changelog.d/7438.sdk Normal file
View file

@ -0,0 +1 @@
Add MetricPlugin interface to implement metrics in SDK clients.

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api
import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.metrics.MetricPlugin
import java.net.Proxy
data class MatrixConfiguration(
@ -74,4 +75,9 @@ data class MatrixConfiguration(
* Sync configuration.
*/
val syncConfig: SyncConfig = SyncConfig(),
/**
* Metrics plugin that can be used to capture metrics from matrix-sdk-android.
*/
val metricPlugins: List<MetricPlugin> = emptyList()
)

View file

@ -0,0 +1,41 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.extensions
import org.matrix.android.sdk.api.metrics.MetricPlugin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Executes the given [block] while measuring the transaction.
*/
@OptIn(ExperimentalContracts::class)
inline fun measureMetric(metricMeasurementPlugins: List<MetricPlugin>, block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
try {
metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
block()
} catch (throwable: Throwable) {
metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
throw throwable
} finally {
metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.metrics
import org.matrix.android.sdk.api.logger.LoggerTag
import timber.log.Timber
private val loggerTag = LoggerTag("DownloadKeysMetricsPlugin", LoggerTag.CRYPTO)
/**
* Extension of MetricPlugin for download_device_keys task.
*/
interface DownloadDeviceKeysMetricsPlugin : MetricPlugin {
override fun logTransaction(message: String?) {
Timber.tag(loggerTag.value).v("## downloadDeviceKeysMetricPlugin() : $message")
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.metrics
/**
* A plugin that can be used to capture metrics in Client.
*/
interface MetricPlugin {
/**
* Start the measurement of the metrics as soon as task is started.
*/
fun startTransaction()
/**
* Mark the measuring transaction finished once the task is completed.
*/
fun finishTransaction()
/**
* Invoked when there is any error in the ongoing task. The metrics tool can use this information to attach to the ongoing transaction.
*
* @param throwable Exception thrown in the running task.
*/
fun onError(throwable: Throwable)
/**
* Can be used to log this transaction.
*/
fun logTransaction(message: String? = "") {
// no-op
}
}

View file

@ -18,13 +18,17 @@ package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.extensions.measureMetric
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope
@ -47,8 +51,11 @@ internal class DeviceListManager @Inject constructor(
coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val clock: Clock,
matrixConfiguration: MatrixConfiguration
) {
private val metricPlugins = matrixConfiguration.metricPlugins
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(userIds: List<String>)
}
@ -345,19 +352,25 @@ internal class DeviceListManager @Inject constructor(
return MXUsersDevicesMap()
}
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
val response = try {
downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
if (throwable is CancellationException) {
// the crypto module is getting closed, so we cannot access the DB anymore
Timber.w("The crypto module is closed, ignoring this error")
} else {
onKeysDownloadFailed(filteredUsers)
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
val response: KeysQueryResponse
measureMetric(relevantPlugins) {
response = try {
downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
if (throwable is CancellationException) {
// the crypto module is getting closed, so we cannot access the DB anymore
Timber.w("The crypto module is closed, ignoring this error")
} else {
onKeysDownloadFailed(filteredUsers)
}
throw throwable
}
throw throwable
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
}
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
for (userId in filteredUsers) {
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }

View file

@ -47,6 +47,7 @@ import im.vector.app.core.utils.SystemSettingsProvider
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.analytics.metrics.VectorPlugins
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
import im.vector.app.features.navigation.DefaultNavigator
@ -75,9 +76,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class VectorBindModule {
@InstallIn(SingletonComponent::class) @Module abstract class VectorBindModule {
@Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@ -119,9 +118,7 @@ abstract class VectorBindModule {
abstract fun bindGetDeviceInfoUseCase(getDeviceInfoUseCase: DefaultGetDeviceInfoUseCase): GetDeviceInfoUseCase
}
@InstallIn(SingletonComponent::class)
@Module
object VectorStaticModule {
@InstallIn(SingletonComponent::class) @Module object VectorStaticModule {
@Provides
fun providesContext(application: Application): Context {
@ -143,6 +140,7 @@ object VectorStaticModule {
vectorPreferences: VectorPreferences,
vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider,
flipperProxy: FlipperProxy,
vectorPlugins: VectorPlugins,
): MatrixConfiguration {
return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
@ -150,7 +148,8 @@ object VectorStaticModule {
threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(),
networkInterceptors = listOfNotNull(
flipperProxy.networkInterceptor(),
)
),
metricPlugins = vectorPlugins.plugins(),
)
}

View file

@ -0,0 +1,35 @@
/*
* 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.metrics
import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics
import org.matrix.android.sdk.api.metrics.MetricPlugin
import javax.inject.Inject
import javax.inject.Singleton
/**
* Class that contains the all plugins which can be used for tracking.
*/
@Singleton
data class VectorPlugins @Inject constructor(
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
) {
/**
* Returns [List] of all [MetricPlugin] hold by this class.
*/
fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics)
}

View file

@ -0,0 +1,45 @@
/*
* 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.metrics.sentry
import io.sentry.ITransaction
import io.sentry.Sentry
import io.sentry.SpanStatus
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
import javax.inject.Inject
class SentryDownloadDeviceKeysMetrics @Inject constructor() : DownloadDeviceKeysMetricsPlugin {
private var transaction: ITransaction? = null
override fun startTransaction() {
transaction = Sentry.startTransaction("download_device_keys", "task")
logTransaction("Sentry transaction started")
}
override fun finishTransaction() {
transaction?.finish()
logTransaction("Sentry transaction finished")
}
override fun onError(throwable: Throwable) {
transaction?.apply {
this.throwable = throwable
this.status = SpanStatus.INTERNAL_ERROR
}
logTransaction("Sentry transaction encountered error ${throwable.message}")
}
}