Skip to content

Commit c3b1ddf

Browse files
committed
UI refinements for the Live Feed
1 parent 6c0bd64 commit c3b1ddf

5 files changed

Lines changed: 150 additions & 125 deletions

File tree

‎sentry-android-buddy/src/main/java/io/sentry/android/buddy/BuddyLiveFeed.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal data class BuddyLiveFeedItem(
1616
val severity: Severity = Severity.LOW,
1717
val adverse: Boolean = false,
1818
val viewed: Boolean = false,
19+
val visibleScreens: List<String> = emptyList(),
1920
) {
2021
enum class Category(val label: String) {
2122
SCREEN("Screen"),
@@ -39,6 +40,7 @@ internal class BuddyLiveFeedBuffer(private val capacity: Int) {
3940
category: BuddyLiveFeedItem.Category,
4041
severity: Severity,
4142
adverse: Boolean,
43+
visibleScreens: List<String>,
4244
): BuddyLiveFeed {
4345
nextId++
4446
items +=
@@ -48,6 +50,7 @@ internal class BuddyLiveFeedBuffer(private val capacity: Int) {
4850
category = category,
4951
severity = severity,
5052
adverse = adverse,
53+
visibleScreens = visibleScreens,
5154
)
5255
while (items.size > capacity) {
5356
items.removeFirst()

‎sentry-android-buddy/src/main/java/io/sentry/android/buddy/SentryBuddyUi.kt‎

Lines changed: 59 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -595,34 +595,36 @@ private fun LiveFeedSheet(
595595
BuddyButtonText("Start Recording")
596596
}
597597
Text(
598-
"Recent useful signals",
598+
"Live feed",
599599
style = MaterialTheme.typography.titleMedium,
600600
fontWeight = FontWeight.Bold,
601601
color = BuddyInk,
602602
)
603603
if (liveFeed.items.isEmpty()) {
604604
EmptyLiveFeedCard()
605605
} else {
606-
LiveFeedRows(liveFeed.items, sentryUiLinks, nowMs)
606+
LiveFeedRows(liveFeed.items.take(LIVE_FEED_VISIBLE_ITEM_LIMIT), sentryUiLinks, nowMs)
607607
}
608608
}
609609

610610
@Composable
611611
private fun AttentionCard(liveFeed: BuddyLiveFeed, sentryUiLinks: BuddySentryUiLinks, nowMs: Long) {
612+
Text(
613+
"Needs attention",
614+
style = MaterialTheme.typography.titleMedium,
615+
fontWeight = FontWeight.Bold,
616+
color = BuddyInk,
617+
)
618+
612619
val item = liveFeed.latestAdverseItem
613620
if (item == null) {
614-
Card(border = CardDefaults.outlinedCardBorder()) {
615-
Row(
616-
modifier = Modifier.fillMaxWidth().padding(16.dp),
617-
horizontalArrangement = Arrangement.SpaceBetween,
618-
verticalAlignment = Alignment.CenterVertically,
619-
) {
620-
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
621-
Text("Needs attention", color = BuddyInk, fontWeight = FontWeight.Bold)
622-
Text("No recent errors or slow spans.", color = BuddyMuted)
623-
}
624-
Text("OK", color = BuddyPurple, fontWeight = FontWeight.Bold)
625-
}
621+
Row(
622+
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
623+
horizontalArrangement = Arrangement.SpaceBetween,
624+
verticalAlignment = Alignment.CenterVertically,
625+
) {
626+
Text("No recent errors or slow spans.", color = BuddyMuted)
627+
Text("OK", color = BuddyPurple, fontWeight = FontWeight.Bold)
626628
}
627629
return
628630
}
@@ -634,34 +636,36 @@ private fun AttentionCard(liveFeed: BuddyLiveFeed, sentryUiLinks: BuddySentryUiL
634636
modifier =
635637
Modifier.clickable(enabled = link != null) { link?.let { openSentryLink(context, it) } },
636638
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.10f)),
637-
border = CardDefaults.outlinedCardBorder(),
638639
) {
639640
Column(
640641
modifier = Modifier.fillMaxWidth().padding(16.dp),
641642
verticalArrangement = Arrangement.spacedBy(10.dp),
642643
) {
643644
Row(
645+
modifier = Modifier.fillMaxWidth(),
644646
horizontalArrangement = Arrangement.spacedBy(10.dp),
645647
verticalAlignment = Alignment.CenterVertically,
646648
) {
647-
Box(modifier = Modifier.size(10.dp).background(color, CircleShape))
648-
Text("Needs attention", color = BuddyInk, fontWeight = FontWeight.Bold)
649-
Text(relativeTime(item.timestamp.time, nowMs), color = BuddyMuted)
650-
}
651-
Text(
652-
item.title(),
653-
color = BuddyInk,
654-
style = MaterialTheme.typography.titleMedium,
655-
fontWeight = FontWeight.Bold,
656-
)
657-
item.subtitle()?.let { Text(it, color = BuddyMuted) }
658-
if (link != null) {
649+
LiveFeedCategoryPill(item.category.label, color)
659650
Text(
660-
"Open in Sentry",
661-
color = BuddyPurple,
662-
style = MaterialTheme.typography.labelMedium,
651+
item.title(),
652+
modifier = Modifier.weight(1f),
653+
color = BuddyInk,
654+
style = MaterialTheme.typography.titleMedium,
663655
fontWeight = FontWeight.Bold,
664656
)
657+
Text(
658+
relativeTime(item.timestamp.time, nowMs),
659+
color = BuddyMuted,
660+
style = MaterialTheme.typography.labelMedium,
661+
)
662+
}
663+
item.screenContextText()?.let { screenContext ->
664+
Text(
665+
screenContext,
666+
color = BuddyMuted,
667+
style = MaterialTheme.typography.bodySmall,
668+
)
665669
}
666670
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
667671
adverseCountChips(liveFeed).forEach { chip ->
@@ -707,29 +711,26 @@ private fun LiveFeedRows(
707711
val color =
708712
if (item.adverse) severityColor(item.severity) else timelineColor(item.timelineItem)
709713
val link = sentryUiLinks.linkFor(item)
710-
Card(
714+
Row(
711715
modifier =
712-
Modifier.clickable(enabled = link != null) { link?.let { openSentryLink(context, it) } },
713-
border = CardDefaults.outlinedCardBorder(),
716+
Modifier.fillMaxWidth()
717+
.clickable(enabled = link != null) { link?.let { openSentryLink(context, it) } }
718+
.padding(vertical = 7.dp),
719+
horizontalArrangement = Arrangement.spacedBy(10.dp),
720+
verticalAlignment = Alignment.CenterVertically,
714721
) {
715-
Row(
716-
modifier = Modifier.fillMaxWidth().padding(14.dp),
717-
horizontalArrangement = Arrangement.spacedBy(10.dp),
718-
verticalAlignment = Alignment.CenterVertically,
719-
) {
720-
LiveFeedCategoryPill(item.category.label, color)
721-
Text(
722-
item.title(),
723-
modifier = Modifier.weight(1f),
724-
color = BuddyInk,
725-
fontWeight = FontWeight.Bold,
726-
)
727-
Text(
728-
relativeTime(item.timestamp.time, nowMs),
729-
color = BuddyMuted,
730-
style = MaterialTheme.typography.labelMedium,
731-
)
732-
}
722+
LiveFeedCategoryPill(item.category.label, color)
723+
Text(
724+
item.title(),
725+
modifier = Modifier.weight(1f),
726+
color = BuddyInk,
727+
fontWeight = if (item.adverse) FontWeight.Bold else FontWeight.Normal,
728+
)
729+
Text(
730+
relativeTime(item.timestamp.time, nowMs),
731+
color = BuddyMuted,
732+
style = MaterialTheme.typography.labelMedium,
733+
)
733734
}
734735
}
735736
}
@@ -1035,45 +1036,21 @@ private fun BuddyLiveFeedItem.title(): String =
10351036
BuddyLiveFeedItem.Category.FAILED_SPAN -> timelineItem.name ?: "Failed span"
10361037
}
10371038

1038-
private fun BuddyLiveFeedItem.subtitle(): String? =
1039-
when (category) {
1040-
BuddyLiveFeedItem.Category.SCREEN -> null
1041-
BuddyLiveFeedItem.Category.STEP -> null
1042-
BuddyLiveFeedItem.Category.ERROR ->
1043-
listOfNotNull(
1044-
timelineItem.data.stringValue("message"),
1045-
timelineItem.data.stringValue("transaction")?.let { "in $it" },
1046-
)
1047-
.joinToString(" • ")
1048-
.ifBlank { null }
1049-
BuddyLiveFeedItem.Category.FAILED_HTTP -> httpSubtitle()
1050-
BuddyLiveFeedItem.Category.SLOW_SPAN,
1051-
BuddyLiveFeedItem.Category.FAILED_SPAN -> spanSubtitle()
1052-
}
1053-
10541039
private fun BuddyLiveFeedItem.httpTitle(): String {
10551040
val data = timelineItem.data.mapValue("data")
10561041
val method = data.stringValue("method") ?: data.stringValue("http.method")
10571042
val url = data.stringValue("url") ?: data.stringValue("http.url")
10581043
return listOfNotNull(method, url).joinToString(" ").ifBlank { timelineItem.name ?: "Failed HTTP" }
10591044
}
10601045

1061-
private fun BuddyLiveFeedItem.httpSubtitle(): String? {
1062-
val data = timelineItem.data.mapValue("data")
1063-
val statusCode = data.longValue("status_code") ?: timelineItem.data.longValue("status_code")
1064-
return statusCode?.let { "HTTP $it" }
1046+
private fun BuddyLiveFeedItem.screenContextText(): String? {
1047+
if (visibleScreens.isEmpty()) {
1048+
return null
1049+
}
1050+
val label = if (visibleScreens.size == 1) "Screen" else "Screens"
1051+
return "$label: ${visibleScreens.joinToString(" -> ")}"
10651052
}
10661053

1067-
private fun BuddyLiveFeedItem.spanSubtitle(): String? =
1068-
listOfNotNull(
1069-
timelineItem.data.stringValue("op"),
1070-
timelineItem.data.stringValue("status")?.let { "status $it" },
1071-
timelineItem.data.longValue("duration_ms")?.let { "${it}ms" },
1072-
timelineItem.data.stringValue("transaction")?.let { "in $it" },
1073-
)
1074-
.joinToString(" • ")
1075-
.ifBlank { null }
1076-
10771054
private fun relativeTime(timestampMs: Long, nowMs: Long): String {
10781055
val ageMs = (nowMs - timestampMs).coerceAtLeast(0)
10791056
if (ageMs < 1000) {
@@ -1400,6 +1377,7 @@ private val BuddyBubbleInitialTop = 96.dp
14001377
private val BuddyBubbleTouchPadding = 20.dp
14011378
private val BuddyTransientTextWidth = 190.dp
14021379
private val BuddyTransientTextHeight = 28.dp
1380+
private const val LIVE_FEED_VISIBLE_ITEM_LIMIT = 7
14031381
private const val ANALYSIS_POLL_INTERVAL_MS = 1000L
14041382
private const val ANALYSIS_TIMEOUT_MS = 30_000L
14051383

‎sentry-android-buddy/src/main/java/io/sentry/android/buddy/internal.kt‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,42 @@ internal class BuddyRecorder(
394394
severity: Severity,
395395
adverse: Boolean,
396396
) {
397-
val snapshot = liveFeed.add(item, category, severity, adverse)
397+
val snapshot =
398+
liveFeed.add(
399+
item = item,
400+
category = category,
401+
severity = severity,
402+
adverse = adverse,
403+
visibleScreens = activeRecording.visibleScreensFor(item),
404+
)
398405
liveFeedListeners.toList().forEach { it(snapshot) }
399406
}
400407

408+
private fun ActiveRecording?.visibleScreensFor(item: BuddyTimelineItem): List<String> {
409+
val recording = this ?: return emptyList()
410+
val startMs = item.timestamp.time
411+
val endMs = item.data.longValue(DATA_DURATION_MS)?.let { startMs + it } ?: startMs
412+
val screenItems =
413+
recording.timeline
414+
.filter { it.type == BuddyTimelineItem.Type.SCREEN && !it.name.isNullOrBlank() }
415+
.sortedBy { it.timestamp.time }
416+
val screenAtStart = screenItems.lastOrNull { it.timestamp.time <= startMs }
417+
val screensDuringItem = screenItems.filter { it.timestamp.time in (startMs + 1)..endMs }
418+
return (listOfNotNull(screenAtStart) + screensDuringItem)
419+
.mapNotNull { it.name }
420+
.dedupeConsecutive()
421+
}
422+
423+
private fun List<String>.dedupeConsecutive(): List<String> {
424+
val deduped = mutableListOf<String>()
425+
forEach { screen ->
426+
if (deduped.lastOrNull() != screen) {
427+
deduped += screen
428+
}
429+
}
430+
return deduped
431+
}
432+
401433
private fun ActiveRecording?.elapsedAt(timestamp: Date): Long =
402434
this?.let { timestamp.time - it.startedAt.time } ?: 0
403435

‎sentry-android-buddy/src/test/java/io/sentry/android/buddy/BuddyRecorderTest.kt‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,51 @@ class BuddyRecorderTest {
420420
assertThat(item.severity).isEqualTo(Severity.HIGH)
421421
}
422422

423+
@Test
424+
fun `live feed records visible screen for adverse event`() {
425+
val fixture = Fixture()
426+
fixture.recorder.start(BuddyFlowIntent("Checkout"))
427+
fixture.recorder.recordScreen("/home")
428+
429+
fixture.recorder.recordEvent(BuddyObservedEvent(Date(500), "Boom", emptyMap()))
430+
431+
val item = fixture.recorder.liveFeedSnapshot().latestAdverseItem
432+
433+
assertThat(item?.visibleScreens).containsExactly("/home")
434+
}
435+
436+
@Test
437+
fun `live feed records screen path for adverse span crossing screens`() {
438+
val fixture = Fixture()
439+
fixture.recorder.start(BuddyFlowIntent("Checkout"))
440+
fixture.recorder.recordScreen("/home")
441+
fixture.clock.advance(500)
442+
fixture.recorder.recordScreen("/profile")
443+
444+
fixture.recorder.recordTransaction(
445+
BuddyObservedTransaction(
446+
recordingId = "recording-1",
447+
operation = "ui.load",
448+
transactionName = "ProfileActivity",
449+
timestamp = Date(1000),
450+
spans =
451+
listOf(
452+
BuddyObservedSpan(
453+
id = "slow-span",
454+
timestamp = Date(100),
455+
operation = "ui.load",
456+
description = "full display",
457+
data = linkedMapOf("duration_ms" to 1000),
458+
)
459+
),
460+
)
461+
)
462+
463+
val item = fixture.recorder.liveFeedSnapshot().latestAdverseItem
464+
465+
assertThat(item?.visibleScreens).containsExactly("/home", "/profile").inOrder()
466+
}
467+
423468
@Test
424469
fun `mark live feed seen clears unviewed adverse count`() {
425470
val fixture = Fixture()

0 commit comments

Comments
 (0)