[Rich text editor] Add error tracking for rich text editor (#7695)

This commit is contained in:
jonnyandrew 2022-12-08 11:43:19 +00:00 committed by GitHub
parent 72ecd1bbc9
commit de18f37849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 18 deletions

1
changelog.d/7695.bugfix Normal file
View File

@ -0,0 +1 @@
[Rich text editor] Add error tracking for rich text editor

View File

@ -46,6 +46,7 @@ import im.vector.app.core.utils.AndroidSystemSettingsProvider
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.errors.ErrorTracker
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.analytics.metrics.VectorPlugins
import im.vector.app.features.invite.AutoAcceptInvites
@ -84,6 +85,9 @@ import javax.inject.Singleton
@Binds
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
@Binds
abstract fun bindErrorTracker(analytics: DefaultVectorAnalytics): ErrorTracker
@Binds
abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker

View File

@ -16,9 +16,10 @@
package im.vector.app.features.analytics
import im.vector.app.features.analytics.errors.ErrorTracker
import kotlinx.coroutines.flow.Flow
interface VectorAnalytics : AnalyticsTracker {
interface VectorAnalytics : AnalyticsTracker, ErrorTracker {
/**
* Return a Flow of Boolean, true if the user has given their consent.
*/

View File

@ -0,0 +1,21 @@
/*
* 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.errors
interface ErrorTracker {
fun trackError(throwable: Throwable)
}

View File

@ -41,7 +41,7 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton
class DefaultVectorAnalytics @Inject constructor(
postHogFactory: PostHogFactory,
private val sentryFactory: SentryFactory,
private val sentryAnalytics: SentryAnalytics,
analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@ -97,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("")
// Close Sentry SDK.
sentryFactory.stopSentry()
sentryAnalytics.stopSentry()
}
private fun observeAnalyticsId() {
@ -135,8 +135,8 @@ class DefaultVectorAnalytics @Inject constructor(
private fun initOrStopSentry() {
userConsent?.let {
when (it) {
true -> sentryFactory.initSentry()
false -> sentryFactory.stopSentry()
true -> sentryAnalytics.initSentry()
false -> sentryAnalytics.stopSentry()
}
}
}
@ -180,4 +180,10 @@ class DefaultVectorAnalytics @Inject constructor(
putAll(this@toPostHogUserProperties.filter { it.value != null })
}
}
override fun trackError(throwable: Throwable) {
sentryAnalytics
.takeIf { userConsent == true }
?.trackError(throwable)
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.analytics.impl
import android.content.Context
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.analytics.log.analyticsTag
import io.sentry.Sentry
import io.sentry.SentryOptions
@ -25,10 +26,10 @@ import io.sentry.android.core.SentryAndroid
import timber.log.Timber
import javax.inject.Inject
class SentryFactory @Inject constructor(
class SentryAnalytics @Inject constructor(
private val context: Context,
private val analyticsConfig: AnalyticsConfig,
) {
) : ErrorTracker {
fun initSentry() {
Timber.tag(analyticsTag.value).d("Initializing Sentry")
@ -47,4 +48,8 @@ class SentryFactory @Inject constructor(
Timber.tag(analyticsTag.value).d("Stopping Sentry")
Sentry.close()
}
override fun trackError(throwable: Throwable) {
Sentry.captureException(throwable)
}
}

View File

@ -60,6 +60,7 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentComposerBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.attachments.AttachmentType
import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
@ -116,6 +117,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
@Inject lateinit var vectorFeatures: VectorFeatures
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var session: Session
@Inject lateinit var errorTracker: ErrorTracker
private val roomId: String get() = withState(timelineViewModel) { it.roomId }
@ -171,6 +173,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled()
views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled()
views.richTextComposerLayout.setOnErrorListener(errorTracker::trackError)
messageComposerViewModel.observeViewEvents {
when (it) {

View File

@ -49,10 +49,11 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
class RichTextComposerLayout @JvmOverloads constructor(
internal class RichTextComposerLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
@ -248,10 +249,15 @@ class RichTextComposerLayout @JvmOverloads constructor(
updateMenuStateFor(action, state)
}
}
updateEditTextVisibility()
}
fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) {
views.richTextComposerEditText.rustErrorCollector = RustErrorCollector {
onError(RichTextEditorException(it))
}
}
private fun updateEditTextVisibility() {
views.richTextComposerEditText.isVisible = isTextFormattingEnabled
views.richTextMenu.isVisible = isTextFormattingEnabled

View File

@ -0,0 +1,21 @@
/*
* 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.home.room.detail.composer
internal class RichTextEditorException(
cause: Throwable,
) : Exception(cause)

View File

@ -23,7 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog
import im.vector.app.test.fakes.FakePostHogFactory
import im.vector.app.test.fakes.FakeSentryFactory
import im.vector.app.test.fakes.FakeSentryAnalytics
import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig
import im.vector.app.test.fixtures.aUserProperties
import im.vector.app.test.fixtures.aVectorAnalyticsEvent
@ -46,11 +46,11 @@ class DefaultVectorAnalyticsTest {
private val fakePostHog = FakePostHog()
private val fakeAnalyticsStore = FakeAnalyticsStore()
private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
private val fakeSentryFactory = FakeSentryFactory()
private val fakeSentryAnalytics = FakeSentryAnalytics()
private val defaultVectorAnalytics = DefaultVectorAnalytics(
postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
sentryFactory = fakeSentryFactory.instance,
sentryAnalytics = fakeSentryAnalytics.instance,
analyticsStore = fakeAnalyticsStore.instance,
globalScope = CoroutineScope(Dispatchers.Unconfined),
analyticsConfig = anAnalyticsConfig(isEnabled = true),
@ -75,7 +75,7 @@ class DefaultVectorAnalyticsTest {
fakePostHog.verifyOptOutStatus(optedOut = false)
fakeSentryFactory.verifySentryInit()
fakeSentryAnalytics.verifySentryInit()
}
@Test
@ -84,7 +84,7 @@ class DefaultVectorAnalyticsTest {
fakePostHog.verifyOptOutStatus(optedOut = true)
fakeSentryFactory.verifySentryClose()
fakeSentryAnalytics.verifySentryClose()
}
@Test
@ -111,7 +111,7 @@ class DefaultVectorAnalyticsTest {
fakePostHog.verifyReset()
fakeSentryFactory.verifySentryClose()
fakeSentryAnalytics.verifySentryClose()
}
@Test
@ -149,6 +149,25 @@ class DefaultVectorAnalyticsTest {
fakePostHog.verifyNoEventTracking()
}
@Test
fun `given user has consented, when tracking exception, then submits to sentry`() = runTest {
fakeAnalyticsStore.givenUserContent(consent = true)
val exception = Exception("test")
defaultVectorAnalytics.trackError(exception)
fakeSentryAnalytics.verifySentryTrackError(exception)
}
@Test
fun `given user has not consented, when tracking exception, then does not track to sentry`() = runTest {
fakeAnalyticsStore.givenUserContent(consent = false)
defaultVectorAnalytics.trackError(Exception("test"))
fakeSentryAnalytics.verifyNoErrorTracking()
}
}
private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? {

View File

@ -16,15 +16,15 @@
package im.vector.app.test.fakes
import im.vector.app.features.analytics.impl.SentryFactory
import im.vector.app.features.analytics.impl.SentryAnalytics
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
class FakeSentryFactory {
class FakeSentryAnalytics {
private var isSentryEnabled = false
val instance = mockk<SentryFactory>().also {
val instance = mockk<SentryAnalytics>(relaxUnitFun = true).also {
every { it.initSentry() } answers {
isSentryEnabled = true
}
@ -41,4 +41,13 @@ class FakeSentryFactory {
fun verifySentryClose() {
verify { instance.stopSentry() }
}
fun verifySentryTrackError(error: Throwable) {
verify { instance.trackError(error) }
}
fun verifyNoErrorTracking() =
verify(inverse = true) {
instance.trackError(any())
}
}