1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Amit Kumar
42d8bbb7b3 Add changelog about sentry tracking 2022-11-21 18:40:47 +05:30
Amit Kumar
98762ef042 Update documentation of metrics extensions 2022-11-21 17:37:27 +05:30
Amit Kumar
b6c0151e97 Merge branch 'develop' into feature/amitkma/send-message-tracking 2022-11-17 18:43:49 +05:30
Amit Kumar
6e6a2fff31 Add sentry metrics around send event 2022-11-17 18:42:56 +05:30
12 changed files with 358 additions and 125 deletions

1
changelog.d/7602.sdk Normal file
View File

@@ -0,0 +1 @@
Add sentry tracking around send message task.

View File

@@ -25,14 +25,17 @@ import kotlin.contracts.contract
/**
* Executes the given [block] while measuring the transaction.
*
* @param T type of the output from [block]
* @param block Action/Task to be executed within this span.
*
* @return Output of the [block()]
*/
@OptIn(ExperimentalContracts::class)
inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
inline fun <T> List<MetricPlugin>.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) {
@@ -46,16 +49,19 @@ inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
/**
* Executes the given [block] while measuring a span.
*
* @param T type of the output from [block]
* @param operation Name of the new span.
* @param description Description of the new span.
* @param block Action/Task to be executed within this span.
*
* @return Output of the [block]
*/
@OptIn(ExperimentalContracts::class)
inline fun List<SpannableMetricPlugin>.measureSpan(operation: String, description: String, block: () -> Unit) {
inline fun <T> List<SpannableMetricPlugin>.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) {

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("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")
}
}

View File

@@ -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<DownloadDeviceKeysMetricsPlugin>()
val response: KeysQueryResponse
relevantPlugins.measureMetric {
relevantPlugins.measureTransaction {
response = try {
downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) {

View File

@@ -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<RedactEventTask.Params, String> {
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<SendServiceMetricPlugin>()
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
}
}

View File

@@ -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<SendServiceMetricPlugin>()
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

View File

@@ -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<SendServiceMetricPlugin>()
@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<String>, 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

View File

@@ -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<SendServiceMetricPlugin>()
/**
* 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) {

View File

@@ -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

View File

@@ -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<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics)
fun plugins(): List<MetricPlugin> = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics, sentrySendServiceMetrics)
}

View File

@@ -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<ISpan> = 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}")
}
}

View File

@@ -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]")
}
}
}