Fix room list duplicate-detection telemetry crashing before it can report (#6791)

* Add room dupe regression tests

* Fix telemetry path for dedupe discovery
This commit is contained in:
Jenna Vassar 2026-05-15 01:43:21 -07:00 committed by GitHub
parent 432a7712c4
commit cbc677b80d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 165 additions and 10 deletions

View file

@ -16,25 +16,25 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
internal fun RoomListEntriesUpdate.describe(): String {
return when (this) {
is RoomListEntriesUpdate.Set -> {
"Set #$index to '${value.displayName()}'"
"Set #$index to '${value.id()}'"
}
is RoomListEntriesUpdate.Append -> {
"Append ${values.map { "'" + it.displayName() + "'" }}"
"Append ${values.map { "'" + it.id() + "'" }}"
}
is RoomListEntriesUpdate.PushBack -> {
"PushBack '${value.displayName()}'"
"PushBack '${value.id()}'"
}
is RoomListEntriesUpdate.PushFront -> {
"PushFront '${value.displayName()}'"
"PushFront '${value.id()}'"
}
is RoomListEntriesUpdate.Insert -> {
"Insert at #$index: '${value.displayName()}'"
"Insert at #$index: '${value.id()}'"
}
is RoomListEntriesUpdate.Remove -> {
"Remove #$index"
}
is RoomListEntriesUpdate.Reset -> {
"Reset all to ${values.map { "'" + it.displayName() + "'" }}"
"Reset all to ${values.map { "'" + it.id() + "'" }}"
}
RoomListEntriesUpdate.PopBack -> {
"PopBack"

View file

@ -112,6 +112,11 @@ class RoomSummaryListProcessor(
private suspend fun updateRoomSummaries(updates: List<RoomListEntriesUpdate>, block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(
coroutineContext
) {
// Capture the description before applying updates: applyUpdate consumes each Room via
// `entry.use { ... }` which destroys it, and the duplicate-detection branch below reads
// id() through `describe()`. Without this capture the trackError call crashes before it
// can be reported.
val updatesDescription = updates.description()
mutex.withLock {
val current = roomSummaries.replayCache.lastOrNull()
val mutableRoomSummaries = current.orEmpty().toMutableList()
@ -126,7 +131,7 @@ class RoomSummaryListProcessor(
analyticsService.trackError(
IllegalStateException(
"Found duplicates in room summaries after a list update from the SDK: $duplicates. " +
"Updates: ${updates.description()}"
"Updates: $updatesDescription"
)
)
}

View file

@ -173,16 +173,68 @@ class RoomSummaryListProcessorTest {
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
}
/**
* Tracking issue #4182 / #5031: rooms duplicated in the room list.
*
* If duplicates are present in the upstream summaries flow, the dedupe safety net in
* [RoomSummaryListProcessor.updateRoomSummaries] must remove them and report the incident via
* [analyticsService.trackError]. Uses an empty update to drive the dedupe path without
* passing a Rust Room through the destroy-on-use path.
*/
@Test
fun `pre-existing duplicates in summaries are deduped on next update and trackError fires`() = runTest {
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(roomId = A_ROOM_ID), // simulated SDK-side leak
aRoomSummary(roomId = A_ROOM_ID_2),
)
val analyticsService = FakeAnalyticsService()
val processor = createProcessor(analyticsService = analyticsService)
processor.postUpdate(emptyList())
assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
assertThat(analyticsService.trackedErrors).hasSize(1)
}
/**
* Tracking issue #4182 / #5031.
*
* Insert is the most likely Rust-SDK trigger for a duplicate-room report: it blindly inserts
* a new entry at an index without checking whether the roomId already exists. Before the
* describe-capture fix, the dedupe branch in [updateRoomSummaries] would call `Room.id()`
* on an already-destroyed Room (because [applyUpdate] consumes each value via
* `entry.use { ... }`) and crash before [trackError] could be invoked. This test guards the
* fix: the Insert is processed, the list is emitted deduplicated, and the tracked error
* message carries the human-readable description of the offending update.
*/
@Test
fun `Insert that triggers dedupe is reported via trackError without crashing`() = runTest {
summaries.value = listOf(aRoomSummary(roomId = A_ROOM_ID))
val analyticsService = FakeAnalyticsService()
val processor = createProcessor(analyticsService = analyticsService)
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(0u, aRustRoom(A_ROOM_ID))))
assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID)
assertThat(analyticsService.trackedErrors).hasSize(1)
val message = analyticsService.trackedErrors.single().message.orEmpty()
assertThat(message).contains("Found duplicates")
assertThat(message).contains("Insert at #0")
}
private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom(
roomId = roomId,
latestEventLambda = { LatestEventValue.None }
)
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
private fun TestScope.createProcessor(
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = RoomSummaryListProcessor(
summaries,
FakeFfiRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryFactory = RoomSummaryFactory(),
analyticsService = FakeAnalyticsService(),
analyticsService = analyticsService,
)
}