Skip to content

Commit de18f37

Browse files
authored
[Rich text editor] Add error tracking for rich text editor (#7695)
1 parent 72ecd1b commit de18f37

File tree

11 files changed

+114
-18
lines changed

11 files changed

+114
-18
lines changed

changelog.d/7695.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Rich text editor] Add error tracking for rich text editor

vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import im.vector.app.core.utils.AndroidSystemSettingsProvider
4646
import im.vector.app.core.utils.SystemSettingsProvider
4747
import im.vector.app.features.analytics.AnalyticsTracker
4848
import im.vector.app.features.analytics.VectorAnalytics
49+
import im.vector.app.features.analytics.errors.ErrorTracker
4950
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
5051
import im.vector.app.features.analytics.metrics.VectorPlugins
5152
import im.vector.app.features.invite.AutoAcceptInvites
@@ -84,6 +85,9 @@ import javax.inject.Singleton
8485
@Binds
8586
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
8687

88+
@Binds
89+
abstract fun bindErrorTracker(analytics: DefaultVectorAnalytics): ErrorTracker
90+
8791
@Binds
8892
abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker
8993

vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
package im.vector.app.features.analytics
1818

19+
import im.vector.app.features.analytics.errors.ErrorTracker
1920
import kotlinx.coroutines.flow.Flow
2021

21-
interface VectorAnalytics : AnalyticsTracker {
22+
interface VectorAnalytics : AnalyticsTracker, ErrorTracker {
2223
/**
2324
* Return a Flow of Boolean, true if the user has given their consent.
2425
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.analytics.errors
18+
19+
interface ErrorTracker {
20+
fun trackError(throwable: Throwable)
21+
}

vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt

+10-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private val IGNORED_OPTIONS: Options? = null
4141
@Singleton
4242
class DefaultVectorAnalytics @Inject constructor(
4343
postHogFactory: PostHogFactory,
44-
private val sentryFactory: SentryFactory,
44+
private val sentryAnalytics: SentryAnalytics,
4545
analyticsConfig: AnalyticsConfig,
4646
private val analyticsStore: AnalyticsStore,
4747
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@@ -97,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor(
9797
setAnalyticsId("")
9898

9999
// Close Sentry SDK.
100-
sentryFactory.stopSentry()
100+
sentryAnalytics.stopSentry()
101101
}
102102

103103
private fun observeAnalyticsId() {
@@ -135,8 +135,8 @@ class DefaultVectorAnalytics @Inject constructor(
135135
private fun initOrStopSentry() {
136136
userConsent?.let {
137137
when (it) {
138-
true -> sentryFactory.initSentry()
139-
false -> sentryFactory.stopSentry()
138+
true -> sentryAnalytics.initSentry()
139+
false -> sentryAnalytics.stopSentry()
140140
}
141141
}
142142
}
@@ -180,4 +180,10 @@ class DefaultVectorAnalytics @Inject constructor(
180180
putAll(this@toPostHogUserProperties.filter { it.value != null })
181181
}
182182
}
183+
184+
override fun trackError(throwable: Throwable) {
185+
sentryAnalytics
186+
.takeIf { userConsent == true }
187+
?.trackError(throwable)
188+
}
183189
}

vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt renamed to vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ package im.vector.app.features.analytics.impl
1818

1919
import android.content.Context
2020
import im.vector.app.features.analytics.AnalyticsConfig
21+
import im.vector.app.features.analytics.errors.ErrorTracker
2122
import im.vector.app.features.analytics.log.analyticsTag
2223
import io.sentry.Sentry
2324
import io.sentry.SentryOptions
2425
import io.sentry.android.core.SentryAndroid
2526
import timber.log.Timber
2627
import javax.inject.Inject
2728

28-
class SentryFactory @Inject constructor(
29+
class SentryAnalytics @Inject constructor(
2930
private val context: Context,
3031
private val analyticsConfig: AnalyticsConfig,
31-
) {
32+
) : ErrorTracker {
3233

3334
fun initSentry() {
3435
Timber.tag(analyticsTag.value).d("Initializing Sentry")
@@ -47,4 +48,8 @@ class SentryFactory @Inject constructor(
4748
Timber.tag(analyticsTag.value).d("Stopping Sentry")
4849
Sentry.close()
4950
}
51+
52+
override fun trackError(throwable: Throwable) {
53+
Sentry.captureException(throwable)
54+
}
5055
}

vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
6060
import im.vector.app.core.utils.registerForPermissionsResult
6161
import im.vector.app.databinding.FragmentComposerBinding
6262
import im.vector.app.features.VectorFeatures
63+
import im.vector.app.features.analytics.errors.ErrorTracker
6364
import im.vector.app.features.attachments.AttachmentType
6465
import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
6566
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
@@ -116,6 +117,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
116117
@Inject lateinit var vectorFeatures: VectorFeatures
117118
@Inject lateinit var buildMeta: BuildMeta
118119
@Inject lateinit var session: Session
120+
@Inject lateinit var errorTracker: ErrorTracker
119121

120122
private val roomId: String get() = withState(timelineViewModel) { it.roomId }
121123

@@ -171,6 +173,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
171173

172174
views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled()
173175
views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled()
176+
views.richTextComposerLayout.setOnErrorListener(errorTracker::trackError)
174177

175178
messageComposerViewModel.observeViewEvents {
176179
when (it) {

vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
4949
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
5050
import io.element.android.wysiwyg.EditorEditText
5151
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
52+
import io.element.android.wysiwyg.utils.RustErrorCollector
5253
import uniffi.wysiwyg_composer.ActionState
5354
import uniffi.wysiwyg_composer.ComposerAction
5455

55-
class RichTextComposerLayout @JvmOverloads constructor(
56+
internal class RichTextComposerLayout @JvmOverloads constructor(
5657
context: Context,
5758
attrs: AttributeSet? = null,
5859
defStyleAttr: Int = 0
@@ -248,10 +249,15 @@ class RichTextComposerLayout @JvmOverloads constructor(
248249
updateMenuStateFor(action, state)
249250
}
250251
}
251-
252252
updateEditTextVisibility()
253253
}
254254

255+
fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) {
256+
views.richTextComposerEditText.rustErrorCollector = RustErrorCollector {
257+
onError(RichTextEditorException(it))
258+
}
259+
}
260+
255261
private fun updateEditTextVisibility() {
256262
views.richTextComposerEditText.isVisible = isTextFormattingEnabled
257263
views.richTextMenu.isVisible = isTextFormattingEnabled
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.home.room.detail.composer
18+
19+
internal class RichTextEditorException(
20+
cause: Throwable,
21+
) : Exception(cause)

vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt

+25-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore
2323
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
2424
import im.vector.app.test.fakes.FakePostHog
2525
import im.vector.app.test.fakes.FakePostHogFactory
26-
import im.vector.app.test.fakes.FakeSentryFactory
26+
import im.vector.app.test.fakes.FakeSentryAnalytics
2727
import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig
2828
import im.vector.app.test.fixtures.aUserProperties
2929
import im.vector.app.test.fixtures.aVectorAnalyticsEvent
@@ -46,11 +46,11 @@ class DefaultVectorAnalyticsTest {
4646
private val fakePostHog = FakePostHog()
4747
private val fakeAnalyticsStore = FakeAnalyticsStore()
4848
private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
49-
private val fakeSentryFactory = FakeSentryFactory()
49+
private val fakeSentryAnalytics = FakeSentryAnalytics()
5050

5151
private val defaultVectorAnalytics = DefaultVectorAnalytics(
5252
postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
53-
sentryFactory = fakeSentryFactory.instance,
53+
sentryAnalytics = fakeSentryAnalytics.instance,
5454
analyticsStore = fakeAnalyticsStore.instance,
5555
globalScope = CoroutineScope(Dispatchers.Unconfined),
5656
analyticsConfig = anAnalyticsConfig(isEnabled = true),
@@ -75,7 +75,7 @@ class DefaultVectorAnalyticsTest {
7575

7676
fakePostHog.verifyOptOutStatus(optedOut = false)
7777

78-
fakeSentryFactory.verifySentryInit()
78+
fakeSentryAnalytics.verifySentryInit()
7979
}
8080

8181
@Test
@@ -84,7 +84,7 @@ class DefaultVectorAnalyticsTest {
8484

8585
fakePostHog.verifyOptOutStatus(optedOut = true)
8686

87-
fakeSentryFactory.verifySentryClose()
87+
fakeSentryAnalytics.verifySentryClose()
8888
}
8989

9090
@Test
@@ -111,7 +111,7 @@ class DefaultVectorAnalyticsTest {
111111

112112
fakePostHog.verifyReset()
113113

114-
fakeSentryFactory.verifySentryClose()
114+
fakeSentryAnalytics.verifySentryClose()
115115
}
116116

117117
@Test
@@ -149,6 +149,25 @@ class DefaultVectorAnalyticsTest {
149149

150150
fakePostHog.verifyNoEventTracking()
151151
}
152+
153+
@Test
154+
fun `given user has consented, when tracking exception, then submits to sentry`() = runTest {
155+
fakeAnalyticsStore.givenUserContent(consent = true)
156+
val exception = Exception("test")
157+
158+
defaultVectorAnalytics.trackError(exception)
159+
160+
fakeSentryAnalytics.verifySentryTrackError(exception)
161+
}
162+
163+
@Test
164+
fun `given user has not consented, when tracking exception, then does not track to sentry`() = runTest {
165+
fakeAnalyticsStore.givenUserContent(consent = false)
166+
167+
defaultVectorAnalytics.trackError(Exception("test"))
168+
169+
fakeSentryAnalytics.verifyNoErrorTracking()
170+
}
152171
}
153172

154173
private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? {

vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt renamed to vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt

+12-3
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616

1717
package im.vector.app.test.fakes
1818

19-
import im.vector.app.features.analytics.impl.SentryFactory
19+
import im.vector.app.features.analytics.impl.SentryAnalytics
2020
import io.mockk.every
2121
import io.mockk.mockk
2222
import io.mockk.verify
2323

24-
class FakeSentryFactory {
24+
class FakeSentryAnalytics {
2525
private var isSentryEnabled = false
2626

27-
val instance = mockk<SentryFactory>().also {
27+
val instance = mockk<SentryAnalytics>(relaxUnitFun = true).also {
2828
every { it.initSentry() } answers {
2929
isSentryEnabled = true
3030
}
@@ -41,4 +41,13 @@ class FakeSentryFactory {
4141
fun verifySentryClose() {
4242
verify { instance.stopSentry() }
4343
}
44+
45+
fun verifySentryTrackError(error: Throwable) {
46+
verify { instance.trackError(error) }
47+
}
48+
49+
fun verifyNoErrorTracking() =
50+
verify(inverse = true) {
51+
instance.trackError(any())
52+
}
4453
}

0 commit comments

Comments
 (0)