Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics plugin to track device download keys task #7438

Merged
merged 8 commits into from
Nov 2, 2022
Merged
1 change: 1 addition & 0 deletions changelog.d/7438.sdk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add MetricPlugin interface to implement metrics in SDK clients.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
)
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's contract? Do we really need it (i can see it's experimental)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is basically a way to tell the compiler about how the function behaves. Sometimes the compiler doesn't know or is not fully aware of certain conditions or returns. So you can specify an effect when a function is invoked to help the compiler.
In our case, it allows us to write this code:

val response: KeysQueryResponse 

measureMetric(plugin) {
    response = block() // this won't be possible without contracts.
}

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.
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>)
}
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -143,14 +140,16 @@ object VectorStaticModule {
vectorPreferences: VectorPreferences,
vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider,
flipperProxy: FlipperProxy,
vectorPlugins: VectorPlugins,
): MatrixConfiguration {
return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider,
threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(),
networkInterceptors = listOfNotNull(
flipperProxy.networkInterceptor(),
)
),
metricPlugins = vectorPlugins.plugins(),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
data class VectorPlugins @Inject constructor(
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
) {
/**
* Returns [List] of all [MetricPlugin] hold by this class.
*/
fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics)
}
Original file line number Diff line number Diff line change
@@ -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}")
}
}