From 6e6a2fff317d3a76f64c3180bde51abc7b276a5b Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 17 Nov 2022 18:42:56 +0530 Subject: [PATCH] Add sentry metrics around send event --- .../sdk/api/extensions/MetricsExtensions.kt | 8 +- .../api/metrics/SendServiceMetricPlugin.kt | 32 ++++ .../sdk/internal/crypto/DeviceListManager.kt | 4 +- .../internal/crypto/tasks/RedactEventTask.kt | 26 ++-- .../internal/crypto/tasks/SendEventTask.kt | 104 +++++++------ .../session/room/send/DefaultSendService.kt | 139 +++++++++++++----- .../queue/EventSenderProcessorCoroutine.kt | 54 ++++--- .../session/sync/SyncResponseHandler.kt | 4 +- .../analytics/metrics/VectorPlugins.kt | 4 +- .../sentry/SentrySendServiceMetrics.kt | 91 ++++++++++++ .../sentry/SentrySyncDurationMetrics.kt | 10 +- 11 files changed, 351 insertions(+), 125 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SendServiceMetricPlugin.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySendServiceMetrics.kt 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 index 7f0e828f62..19cd799267 100644 --- 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 @@ -28,11 +28,11 @@ import kotlin.contracts.contract * @param block Action/Task to be executed within this span. */ @OptIn(ExperimentalContracts::class) -inline fun List.measureMetric(block: () -> Unit) { +inline fun List.measureTransaction(block: () -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - try { + return try { this.forEach { plugin -> plugin.startTransaction() } // Start the transaction. block() } catch (throwable: Throwable) { @@ -51,11 +51,11 @@ inline fun List.measureMetric(block: () -> Unit) { * @param block Action/Task to be executed within this span. */ @OptIn(ExperimentalContracts::class) -inline fun List.measureSpan(operation: String, description: String, block: () -> Unit) { +inline fun List.measureSpan(operation: String, description: String, block: () -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - try { + return try { this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction. block() } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SendServiceMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SendServiceMetricPlugin.kt new file mode 100644 index 0000000000..fc22ffd760 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SendServiceMetricPlugin.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("SendServiceMetricPlugin", LoggerTag.CRYPTO) + +/** + * An spannable metric plugin for tracking send message, events or command task. + */ +interface SendServiceMetricPlugin : SpannableMetricPlugin { + + override fun logTransaction(message: String?) { + Timber.tag(loggerTag.value).v("## sendService() : $message") + } +} 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 7e9e156003..894fa829b8 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 @@ -22,7 +22,7 @@ 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.extensions.measureTransaction 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 @@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor( val relevantPlugins = metricPlugins.filterIsInstance() val response: KeysQueryResponse - relevantPlugins.measureMetric { + relevantPlugins.measureTransaction { response = try { downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt index 56bdc8cae8..0de7f2f4cf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt @@ -15,6 +15,9 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.metrics.SendServiceMetricPlugin import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI @@ -32,18 +35,23 @@ internal interface RedactEventTask : Task { internal class DefaultRedactEventTask @Inject constructor( private val roomAPI: RoomAPI, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + matrixConfiguration: MatrixConfiguration ) : RedactEventTask { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + override suspend fun execute(params: RedactEventTask.Params): String { - val response = executeRequest(globalErrorReceiver) { - roomAPI.redactEvent( - txId = params.txID, - roomId = params.roomId, - eventId = params.eventId, - reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) - ) + relevantPlugins.measureSpan("send_service.execute_redact_event_task", "Execute redact event task") { + val response = executeRequest(globalErrorReceiver) { + roomAPI.redactEvent( + txId = params.txID, + roomId = params.roomId, + eventId = params.eventId, + reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) + } + return response.eventId } - return response.eventId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 405757e3b3..b7fe334dfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,6 +15,9 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.metrics.SendServiceMetricPlugin import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.send.SendState @@ -41,47 +44,58 @@ internal class DefaultSendEventTask @Inject constructor( private val loadRoomMembersTask: LoadRoomMembersTask, private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask, private val roomAPI: RoomAPI, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + matrixConfiguration: MatrixConfiguration ) : SendEventTask { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + override suspend fun execute(params: SendEventTask.Params): String { - try { - if (params.event.isLocalRoomEvent) { - return createRoomAndSendEvent(params) - } - - // Make sure to load all members in the room before sending the event. - params.event.roomId - ?.takeIf { params.encrypt } - ?.let { roomId -> - try { - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) - } catch (failure: Throwable) { - // send any way? - // the result is that some users won't probably be able to decrypt :/ - Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}") - } + relevantPlugins.measureSpan("send_service.execute_send_event_task", "Execute send event task") { + try { + if (params.event.isLocalRoomEvent) { + return relevantPlugins.measureSpan("send_service.create_room_send_event", "Create room and send event") { + createRoomAndSendEvent(params) } + } - val event = handleEncryption(params) - val localId = event.eventId!! - localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING) - val response = executeRequest(globalErrorReceiver) { - roomAPI.send( - localId, - roomId = event.roomId ?: "", - content = event.content, - eventType = event.type ?: "" - ) - } - localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) - return response.eventId.also { - Timber.d("Event: $it just sent in ${params.event.roomId}") - } - } catch (e: Throwable) { + // Make sure to load all members in the room before sending the event. + relevantPlugins.measureSpan("send_service.load_all_members", "Load all members of room") { + params.event.roomId + ?.takeIf { params.encrypt } + ?.let { roomId -> + try { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } catch (failure: Throwable) { + // send any way? + // the result is that some users won't probably be able to decrypt :/ + Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}") + } + } + } + + val event = handleEncryption(params) + val localId = event.eventId!! + localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING) + relevantPlugins.measureSpan("send_service.room_send_event", "Send event in room") { + val response = executeRequest(globalErrorReceiver) { + roomAPI.send( + localId, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type ?: "" + ) + } + localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) + return response.eventId.also { + Timber.d("Event: $it just sent in ${params.event.roomId}") + } + } + } catch (e: Throwable) { // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED) - Timber.w(e, "Unable to send the Event") - throw e + Timber.w(e, "Unable to send the Event") + throw e + } } } @@ -93,16 +107,18 @@ internal class DefaultSendEventTask @Inject constructor( @Throws private suspend fun handleEncryption(params: SendEventTask.Params): Event { - if (params.encrypt && !params.event.isEncrypted()) { - return encryptEventTask.execute( - EncryptEventTask.Params( - params.event.roomId ?: "", - params.event, - listOf("m.relates_to") - ) - ) + relevantPlugins.measureSpan("send_service.encrypt_event", "Encrypt event") { + if (params.encrypt && !params.event.isEncrypted()) { + return encryptEventTask.execute( + EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to") + ) + ) + } + return params.event } - return params.event } private val Event.isLocalRoomEvent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 9cdbc7ff46..428190bb63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -25,7 +25,11 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.extensions.measureTransaction +import org.matrix.android.sdk.api.metrics.SendServiceMetricPlugin import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -73,9 +77,12 @@ internal class DefaultSendService @AssistedInject constructor( private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, - private val cancelSendTracker: CancelSendTracker + private val cancelSendTracker: CancelSendTracker, + matrixConfiguration: MatrixConfiguration, ) : SendService { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + @AssistedFactory interface Factory { fun create(roomId: String): DefaultSendService @@ -84,21 +91,45 @@ internal class DefaultSendService @AssistedInject constructor( private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendEvent(eventType: String, content: JsonDict?): Cancelable { - return localEchoEventFactory.createEvent(roomId, eventType, content) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.create_event", "Create local event") { + localEchoEventFactory.createEvent(roomId, eventType, content) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean, additionalContent: Content?): Cancelable { - return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.create_text_event", "Create local text event") { + localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String, additionalContent: Content?): Cancelable { - return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.create_formatted_text_event", "Create local formatted text event") { + localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun sendQuotedTextMessage( @@ -109,35 +140,67 @@ internal class DefaultSendService @AssistedInject constructor( rootThreadEventId: String?, additionalContent: Content?, ): Cancelable { - return localEchoEventFactory.createQuotedTextEvent( - roomId = roomId, - quotedEvent = quotedEvent, - text = text, - formattedText = formattedText, - autoMarkdown = autoMarkdown, - rootThreadEventId = rootThreadEventId, - additionalContent = additionalContent, - ) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.create_quoted_text_event", "Create local quoted text event") { + localEchoEventFactory.createQuotedTextEvent( + roomId = roomId, + quotedEvent = quotedEvent, + text = text, + formattedText = formattedText, + autoMarkdown = autoMarkdown, + rootThreadEventId = rootThreadEventId, + additionalContent = additionalContent, + ) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content?): Cancelable { - return localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.create_poll", "Create local poll event") { + localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content?): Cancelable { - return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.reply_to_poll", "Create local reply poll event") { + localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun endPoll(pollEventId: String, additionalContent: Content?): Cancelable { - return localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent) - .also { createLocalEcho(it) } - .let { sendEvent(it) } + return relevantPlugins.measureTransaction { + relevantPlugins.measureSpan("send_service.end_poll", "Create local end poll event") { + localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent) + }.also { + relevantPlugins.measureSpan("send_service.create_local_echo", "Create local echo") { + createLocalEcho(it) + } + }.let { + relevantPlugins.measureSpan("send_service.send_event", "Send event") { sendEvent(it) } + } + } } override fun redactEvent(event: Event, reason: String?, additionalContent: Content?): Cancelable { @@ -148,11 +211,13 @@ internal class DefaultSendService @AssistedInject constructor( } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { - if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { - localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT) - return sendEvent(localEcho.root) + relevantPlugins.measureTransaction { + if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { + localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT) + return relevantPlugins.measureSpan("send_service.resend_text_event", "Resend text message") { sendEvent(localEcho.root) } + } + return NoOpCancellable } - return NoOpCancellable } override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable { @@ -163,8 +228,10 @@ internal class DefaultSendService @AssistedInject constructor( val url = messageContent.getFileUrl() ?: return NoOpCancellable if (url.isMxcUrl()) { // We need to resend only the message as the attachment is ok - localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT) - return sendEvent(localEcho.root) + return relevantPlugins.measureTransaction { + localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT) + relevantPlugins.measureSpan("send_service.resend_media_message", "Resend media message") { sendEvent(localEcho.root) } + } } // we need to resend the media diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 2c7eea1e54..bb0227f22d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -21,10 +21,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.extensions.measureTransaction import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.getRetryDelay import org.matrix.android.sdk.api.failure.isLimitExceededError +import org.matrix.android.sdk.api.metrics.SendServiceMetricPlugin import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable @@ -58,11 +61,14 @@ internal class EventSenderProcessorCoroutine @Inject constructor( private val sessionParams: SessionParams, private val queuedTaskFactory: QueuedTaskFactory, private val taskExecutor: TaskExecutor, - private val memento: QueueMemento + private val memento: QueueMemento, + matrixConfiguration: MatrixConfiguration, ) : EventSenderProcessor { private val waitForNetworkSequencer = SemaphoreCoroutineSequencer() + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + /** * sequencers use QueuedTask.queueIdentifier as key. */ @@ -137,33 +143,35 @@ internal class EventSenderProcessorCoroutine @Inject constructor( } private suspend fun executeTask(task: QueuedTask) { - try { - if (task.isCancelled()) { - Timber.v("## $task has been cancelled, try next task") - return - } - task.waitForNetwork() - task.execute() - } catch (exception: Throwable) { - when { - exception is IOException || exception is Failure.NetworkConnection -> { - canReachServer.set(false) - task.markAsFailedOrRetry(exception, 0) - } - (exception.isLimitExceededError()) -> { - task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) - } - exception is CancellationException -> { + relevantPlugins.measureTransaction { + try { + if (task.isCancelled()) { Timber.v("## $task has been cancelled, try next task") + return } - else -> { - Timber.v("## un-retryable error for $task, try next task") - // this task is in error, check next one? - task.onTaskFailed() + task.waitForNetwork() + task.execute() + } catch (exception: Throwable) { + when { + exception is IOException || exception is Failure.NetworkConnection -> { + canReachServer.set(false) + task.markAsFailedOrRetry(exception, 0) + } + (exception.isLimitExceededError()) -> { + task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) + } + exception is CancellationException -> { + Timber.v("## $task has been cancelled, try next task") + } + else -> { + Timber.v("## un-retryable error for $task, try next task") + // this task is in error, check next one? + task.onTaskFailed() + } } } + markAsFinished(task) } - markAsFinished(task) } private suspend fun QueuedTask.markAsFailedOrRetry(failure: Throwable, retryDelay: Long) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 05d50d9595..8e0ea2a5b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.sync import com.zhuinden.monarchy.Monarchy import io.realm.Realm import org.matrix.android.sdk.api.MatrixConfiguration -import org.matrix.android.sdk.api.extensions.measureMetric import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.extensions.measureTransaction import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.pushrules.RuleScope @@ -71,7 +71,7 @@ internal class SyncResponseHandler @Inject constructor( val isInitialSync = fromToken == null Timber.v("Start handling sync, is InitialSync: $isInitialSync") - relevantPlugins.measureMetric { + relevantPlugins.measureTransaction { startCryptoService(isInitialSync) // Handle the to device events before the room ones 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 index 4278c1011b..3278439140 100644 --- 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 @@ -17,6 +17,7 @@ package im.vector.app.features.analytics.metrics import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics +import im.vector.app.features.analytics.metrics.sentry.SentrySendServiceMetrics import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics import org.matrix.android.sdk.api.metrics.MetricPlugin import javax.inject.Inject @@ -29,9 +30,10 @@ import javax.inject.Singleton data class VectorPlugins @Inject constructor( val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics, val sentrySyncDurationMetrics: SentrySyncDurationMetrics, + val sentrySendServiceMetrics: SentrySendServiceMetrics ) { /** * Returns [List] of all [MetricPlugin] hold by this class. */ - fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics) + fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics, sentrySendServiceMetrics) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySendServiceMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySendServiceMetrics.kt new file mode 100644 index 0000000000..769a3a446c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySendServiceMetrics.kt @@ -0,0 +1,91 @@ +/* + * 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.ISpan +import io.sentry.ITransaction +import io.sentry.Sentry +import io.sentry.SpanStatus +import org.matrix.android.sdk.api.metrics.SendServiceMetricPlugin +import java.util.EmptyStackException +import java.util.Stack +import javax.inject.Inject + +/** + * Sentry based implementation of SyncDurationMetricPlugin. + */ +class SentrySendServiceMetrics @Inject constructor() : SendServiceMetricPlugin { + private var transaction: ITransaction? = null + + // Stacks to keep spans in LIFO order. + private var spans: Stack = Stack() + + /** + * Starts the span for a sub-task. + * + * @param operation Name of the new span. + * @param description Description of the new span. + * + * @throws IllegalStateException if this is called without starting a transaction ie. `measureSpan` must be called within `measureMetric`. + */ + override fun startSpan(operation: String, description: String) { + if (Sentry.isEnabled()) { + val span = Sentry.getSpan() ?: transaction + span?.let { + val innerSpan = it.startChild(operation, description) + spans.push(innerSpan) + logTransaction("Sentry span started: operation=[$operation], description=[$description]") + } + } + } + + override fun finishSpan() { + try { + spans.pop() + } catch (e: EmptyStackException) { + null + }?.finish() + logTransaction("Sentry span finished") + } + + override fun startTransaction() { + if (Sentry.isEnabled()) { + transaction = Sentry.startTransaction("SendService", "action.handle", true) + logTransaction("Sentry transaction started") + } + } + + override fun finishTransaction() { + transaction?.finish() + logTransaction("Sentry transaction finished") + } + + override fun onError(throwable: Throwable) { + try { + spans.peek() + } catch (e: EmptyStackException) { + null + }?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } ?: transaction?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } + logTransaction("Sentry transaction encountered error ${throwable.message}") + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt index d69ed01526..34a021de76 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt @@ -44,10 +44,12 @@ class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin */ override fun startSpan(operation: String, description: String) { if (Sentry.isEnabled()) { - val span = Sentry.getSpan() ?: throw IllegalStateException("measureSpan block must be called within measureMetric") - val innerSpan = span.startChild(operation, description) - spans.push(innerSpan) - logTransaction("Sentry span started: operation=[$operation], description=[$description]") + val span = Sentry.getSpan() + span?.let { + val innerSpan = it.startChild(operation, description) + spans.push(innerSpan) + logTransaction("Sentry span started: operation=[$operation], description=[$description]") + } } }