From b6746653f1dd126e7648f069c3eff9e0efad7c3b Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 2 Nov 2022 13:43:57 +0530 Subject: [PATCH] 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 --- changelog.d/7438.sdk | 1 + .../android/sdk/api/MatrixConfiguration.kt | 6 +++ .../sdk/api/extensions/MetricsExtensions.kt | 41 +++++++++++++++++ .../DownloadDeviceKeysMetricsPlugin.kt | 32 +++++++++++++ .../android/sdk/api/metrics/MetricPlugin.kt | 46 +++++++++++++++++++ .../sdk/internal/crypto/DeviceListManager.kt | 35 +++++++++----- .../im/vector/app/core/di/SingletonModule.kt | 13 +++--- .../analytics/metrics/VectorPlugins.kt | 35 ++++++++++++++ .../sentry/SentryDownloadDeviceKeysMetrics.kt | 45 ++++++++++++++++++ 9 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 changelog.d/7438.sdk create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt diff --git a/changelog.d/7438.sdk b/changelog.d/7438.sdk new file mode 100644 index 0000000000..60463aed68 --- /dev/null +++ b/changelog.d/7438.sdk @@ -0,0 +1 @@ +Add MetricPlugin interface to implement metrics in SDK clients. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 7119563617..00d74ab446 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -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 = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt new file mode 100644 index 0000000000..9487a27086 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt @@ -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, 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. + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt new file mode 100644 index 0000000000..66ec0abf51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/DownloadDeviceKeysMetricsPlugin.kt @@ -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") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt new file mode 100644 index 0000000000..3a4b13c494 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/MetricPlugin.kt @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 4f3900adb9..2ac6b8c854 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -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) } @@ -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() + + 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) } diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index dbfda024d8..3c1cea57ec 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -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(), ) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt new file mode 100644 index 0000000000..64f143a2fd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt @@ -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 = listOf(sentryDownloadDeviceKeysMetrics) +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt new file mode 100644 index 0000000000..92213d380c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt @@ -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}") + } +}