Skip to content

Commit fcf3c3a

Browse files
open URLs on host machine (#5977)
* open URLs on host machine * Merge branch 'hackweek/sentry-buddy' into feat/hackweek/sentry-buddy-open-urls-on-host Also fixes an unrelated regression from the previous commit where the Sentry issue search query used "id:" instead of "event.id:". --------- Co-authored-by: Adam Brown <adam.brown@sentry.io>
1 parent 7c72994 commit fcf3c3a

10 files changed

Lines changed: 249 additions & 24 deletions

File tree

‎sentry-android-buddy/api/sentry-android-buddy.api‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,11 @@ public final class io/sentry/android/buddy/ComposableSingletons$SentryBuddyUiKt
314314
public final fun getLambda$-1944467248$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function2;
315315
public final fun getLambda$-218216137$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
316316
public final fun getLambda$-682238859$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
317-
public final fun getLambda$-90711693$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
318317
public final fun getLambda$1215034671$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
319318
public final fun getLambda$1525882558$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
320319
public final fun getLambda$1884831281$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
321320
public final fun getLambda$1892283088$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
321+
public final fun getLambda$342625061$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
322322
public final fun getLambda$356797945$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function3;
323323
public final fun getLambda$567083575$sentry_android_buddy_release ()Lkotlin/jvm/functions/Function2;
324324
}
@@ -331,6 +331,12 @@ public final class io/sentry/android/buddy/DummySentryBuddyFlowAnalysesApi : io/
331331
public fun submit (Lio/sentry/android/buddy/FlowAnalysisRequest;)Lio/sentry/android/buddy/FlowAnalysisResponse;
332332
}
333333

334+
public final class io/sentry/android/buddy/DummySentryBuddyOpenUrlApi : io/sentry/android/buddy/SentryBuddyOpenUrlApi {
335+
public static final field $stable I
336+
public static final field INSTANCE Lio/sentry/android/buddy/DummySentryBuddyOpenUrlApi;
337+
public fun open (Landroid/content/Context;Ljava/lang/String;)V
338+
}
339+
334340
public final class io/sentry/android/buddy/FlowAnalysisEvent : io/sentry/JsonSerializable {
335341
public static final field $stable I
336342
public fun <init> (Ljava/lang/String;JLjava/util/Map;)V
@@ -480,24 +486,39 @@ public final class io/sentry/android/buddy/SentryBuddyHttpFlowAnalysesApi : io/s
480486
public fun submit (Lio/sentry/android/buddy/FlowAnalysisRequest;)Lio/sentry/android/buddy/FlowAnalysisResponse;
481487
}
482488

489+
public final class io/sentry/android/buddy/SentryBuddyHttpOpenUrlApi : io/sentry/android/buddy/SentryBuddyOpenUrlApi {
490+
public static final field $stable I
491+
public fun <init> (Ljava/lang/String;)V
492+
public fun <init> (Ljava/lang/String;Lokhttp3/OkHttpClient;)V
493+
public synthetic fun <init> (Ljava/lang/String;Lokhttp3/OkHttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
494+
public fun open (Landroid/content/Context;Ljava/lang/String;)V
495+
}
496+
497+
public abstract interface class io/sentry/android/buddy/SentryBuddyOpenUrlApi {
498+
public abstract fun open (Landroid/content/Context;Ljava/lang/String;)V
499+
}
500+
483501
public final class io/sentry/android/buddy/SentryBuddyOptions {
484502
public static final field $stable I
485503
public fun <init> ()V
486504
public fun <init> (Z)V
487505
public fun <init> (ZZ)V
488506
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;)V
489-
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Ljava/lang/String;)V
490-
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Ljava/lang/String;Ljava/lang/String;)V
491-
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
492-
public synthetic fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
507+
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;)V
508+
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Ljava/lang/String;)V
509+
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Ljava/lang/String;Ljava/lang/String;)V
510+
public fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
511+
public synthetic fun <init> (ZZLio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
493512
public final fun getEnabled ()Z
494513
public final fun getFlowAnalysesApi ()Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;
514+
public final fun getOpenUrlApi ()Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;
495515
public final fun getSentryUiBaseUrl ()Ljava/lang/String;
496516
public final fun getSentryUiOrganizationSlug ()Ljava/lang/String;
497517
public final fun getSentryUiProjectId ()Ljava/lang/String;
498518
public final fun getShowOverlay ()Z
499519
public final fun setEnabled (Z)V
500520
public final fun setFlowAnalysesApi (Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;)V
521+
public final fun setOpenUrlApi (Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;)V
501522
public final fun setSentryUiBaseUrl (Ljava/lang/String;)V
502523
public final fun setSentryUiOrganizationSlug (Ljava/lang/String;)V
503524
public final fun setSentryUiProjectId (Ljava/lang/String;)V
@@ -515,13 +536,15 @@ public final class io/sentry/android/buddy/SentryBuddySessionController {
515536
public fun <init> ()V
516537
public fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;)V
517538
public fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;)V
518-
public fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lkotlin/jvm/functions/Function0;)V
519-
public synthetic fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
539+
public fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;)V
540+
public fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Lkotlin/jvm/functions/Function0;)V
541+
public synthetic fun <init> (Lio/sentry/android/buddy/SentryBuddyRecorderFacade;Lio/sentry/android/buddy/SentryBuddyFlowAnalysesApi;Lio/sentry/android/buddy/SentryBuddyOpenUrlApi;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
520542
public final fun analyze ()V
521543
public final fun briefRecording ()V
522544
public final fun close ()V
523545
public final fun getState ()Lio/sentry/android/buddy/SentryBuddySessionState;
524546
public final fun open ()V
547+
public final fun openUrl (Landroid/content/Context;Ljava/lang/String;)V
525548
public final fun pollFlowAnalysis ()V
526549
public final fun recordAgain ()V
527550
public final fun startRecording ()V

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,10 @@ public object SentryBuddy {
194194
return null
195195
}
196196
return BuddyOverlayManager(
197-
SentryBuddySessionController(flowAnalysesApi = options.flowAnalysesApi)
197+
SentryBuddySessionController(
198+
flowAnalysesApi = options.flowAnalysesApi,
199+
openUrlApi = options.openUrlApi,
200+
)
198201
)
199202
.also { it.updateOptions(options) }
200203
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.sentry.android.buddy
2+
3+
import android.content.Context
4+
import io.sentry.ILogger
5+
import io.sentry.JsonObjectReader
6+
import io.sentry.JsonSerializable
7+
import io.sentry.JsonSerializer
8+
import io.sentry.ObjectWriter
9+
import io.sentry.SentryOptions
10+
import java.io.IOException
11+
import java.io.StringReader
12+
import java.io.StringWriter
13+
import okhttp3.HttpUrl.Companion.toHttpUrl
14+
import okhttp3.MediaType.Companion.toMediaType
15+
import okhttp3.OkHttpClient
16+
import okhttp3.Request
17+
import okhttp3.RequestBody.Companion.toRequestBody
18+
import okhttp3.Response
19+
import org.jetbrains.annotations.ApiStatus
20+
21+
@ApiStatus.Experimental
22+
public class SentryBuddyHttpOpenUrlApi
23+
@JvmOverloads
24+
public constructor(
25+
private val baseUrl: String,
26+
private val client: OkHttpClient = OkHttpClient(),
27+
) : SentryBuddyOpenUrlApi {
28+
private val json = JsonSerializer(SentryOptions())
29+
30+
override fun open(context: Context, url: String) {
31+
val httpRequest =
32+
Request.Builder()
33+
.url(baseUrl.toHttpUrl().newBuilder().addPathSegments("v1/open-url").build())
34+
.post(serialize(OpenUrlRequest(url)).toRequestBody(JSON_MEDIA_TYPE))
35+
.build()
36+
execute(httpRequest)
37+
}
38+
39+
private fun serialize(request: OpenUrlRequest): String {
40+
val writer = StringWriter()
41+
json.serialize(request, writer)
42+
return writer.toString()
43+
}
44+
45+
private fun execute(request: Request) {
46+
try {
47+
client.newCall(request).execute().use { response ->
48+
if (!response.isSuccessful) {
49+
throw IllegalStateException(response.errorMessage())
50+
}
51+
}
52+
} catch (exception: IOException) {
53+
throw IllegalStateException(
54+
"Failed to call open URL bridge: ${exception.message}",
55+
exception,
56+
)
57+
}
58+
}
59+
60+
private fun Response.errorMessage(): String {
61+
val error = extractError(body?.string().orEmpty())
62+
return buildString {
63+
append("Open URL bridge request failed with HTTP ").append(code)
64+
error?.let { append(": ").append(it) }
65+
}
66+
}
67+
68+
private fun extractError(body: String): String? {
69+
if (body.isBlank()) {
70+
return null
71+
}
72+
return try {
73+
(JsonObjectReader(StringReader(body)).use { it.nextObjectOrNull() } as? Map<*, *>)
74+
?.get("error")
75+
?.toString()
76+
} catch (_: Exception) {
77+
body.take(200)
78+
}
79+
}
80+
81+
private companion object {
82+
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
83+
}
84+
}
85+
86+
private data class OpenUrlRequest(val url: String) : JsonSerializable {
87+
@Throws(IOException::class)
88+
override fun serialize(writer: ObjectWriter, logger: ILogger) {
89+
writer.beginObject()
90+
writer.name("url").value(url)
91+
writer.endObject()
92+
}
93+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.sentry.android.buddy
2+
3+
import android.content.ActivityNotFoundException
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.net.Uri
7+
import android.os.Handler
8+
import android.os.Looper
9+
import org.jetbrains.annotations.ApiStatus
10+
11+
@ApiStatus.Experimental
12+
public interface SentryBuddyOpenUrlApi {
13+
public fun open(context: Context, url: String)
14+
}
15+
16+
@ApiStatus.Experimental
17+
public object DummySentryBuddyOpenUrlApi : SentryBuddyOpenUrlApi {
18+
override fun open(context: Context, url: String) {
19+
if (Looper.myLooper() != Looper.getMainLooper()) {
20+
Handler(Looper.getMainLooper()).post { open(context, url) }
21+
return
22+
}
23+
try {
24+
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
25+
} catch (_: ActivityNotFoundException) {
26+
// A debug overlay should not crash the app when no browser can handle the link.
27+
}
28+
}
29+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public constructor(
99
public var enabled: Boolean = true,
1010
public var showOverlay: Boolean = true,
1111
public var flowAnalysesApi: SentryBuddyFlowAnalysesApi = DummySentryBuddyFlowAnalysesApi,
12+
public var openUrlApi: SentryBuddyOpenUrlApi = DummySentryBuddyOpenUrlApi,
1213
public var sentryUiBaseUrl: String? = null,
1314
public var sentryUiOrganizationSlug: String? = null,
1415
public var sentryUiProjectId: String? = null,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.android.buddy
22

3+
import android.content.Context
34
import java.util.Locale
45
import org.jetbrains.annotations.ApiStatus
56

@@ -189,6 +190,7 @@ public class SentryBuddySessionController
189190
public constructor(
190191
private val recorderFacade: SentryBuddyRecorderFacade = RealSentryBuddyRecorderFacade,
191192
private val flowAnalysesApi: SentryBuddyFlowAnalysesApi = DummySentryBuddyFlowAnalysesApi,
193+
private val openUrlApi: SentryBuddyOpenUrlApi = DummySentryBuddyOpenUrlApi,
192194
private val clock: () -> Long = { System.currentTimeMillis() },
193195
) {
194196
public var state: SentryBuddySessionState = SentryBuddySessionState.Closed
@@ -347,6 +349,14 @@ public constructor(
347349
state = SentryBuddySessionState.Intro
348350
}
349351

352+
public fun openUrl(context: Context, url: String) {
353+
try {
354+
openUrlApi.open(context, url)
355+
} catch (_: IllegalStateException) {
356+
// A debug overlay should not disrupt the current session when the bridge is unreachable.
357+
}
358+
}
359+
350360
internal fun recordTransientEvent(text: String) {
351361
val event: TransientRecordingEvent
352362
val listeners: List<(TransientRecordingEvent) -> Unit>

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package io.sentry.android.buddy
22

3-
import android.content.ActivityNotFoundException
43
import android.content.Context
5-
import android.content.Intent
64
import android.graphics.Rect
7-
import android.net.Uri
85
import androidx.compose.animation.AnimatedVisibility
96
import androidx.compose.animation.core.Animatable
107
import androidx.compose.animation.core.FastOutSlowInEasing
@@ -176,6 +173,10 @@ private fun SentryBuddyOverlayContent(
176173
}
177174
}
178175

176+
fun openUrl(context: Context, url: String) {
177+
analysisScope.launch { withContext(Dispatchers.IO) { controller.openUrl(context, url) } }
178+
}
179+
179180
LaunchedEffect(state) {
180181
if (state !is SentryBuddySessionState.Closed) {
181182
while (true) {
@@ -255,6 +256,7 @@ private fun SentryBuddyOverlayContent(
255256
nowMs = nowMs,
256257
onDispatch = { dispatch(it) },
257258
onAnalyze = { dispatchAnalysis { analyze() } },
259+
onOpenUrl = { context, url -> openUrl(context, url) },
258260
)
259261
}
260262
}
@@ -792,6 +794,7 @@ private fun BuddySheet(
792794
nowMs: Long,
793795
onDispatch: (SentryBuddySessionController.() -> Unit) -> Unit,
794796
onAnalyze: () -> Unit,
797+
onOpenUrl: (Context, String) -> Unit,
795798
) {
796799
if (state is SentryBuddySessionState.Closed || state is SentryBuddySessionState.Recording) {
797800
return
@@ -823,7 +826,14 @@ private fun BuddySheet(
823826
) {
824827
when (state) {
825828
SentryBuddySessionState.LiveFeed ->
826-
LiveFeedSheet(liveFeed, sentryUiLinks, nowMs, onDispatch, ::startRecordingAfterSheetExit)
829+
LiveFeedSheet(
830+
liveFeed,
831+
sentryUiLinks,
832+
nowMs,
833+
onDispatch,
834+
::startRecordingAfterSheetExit,
835+
onOpenUrl,
836+
)
827837
SentryBuddySessionState.Intro -> IntroSheet(::startRecordingAfterSheetExit)
828838
is SentryBuddySessionState.StoppedSummary -> StoppedSummarySheet(state, onDispatch)
829839
is SentryBuddySessionState.Briefing -> BriefingSheet(state, onDispatch, onAnalyze)
@@ -892,6 +902,7 @@ private fun LiveFeedSheet(
892902
nowMs: Long,
893903
onDispatch: (SentryBuddySessionController.() -> Unit) -> Unit,
894904
onStartRecording: () -> Unit,
905+
onOpenUrl: (Context, String) -> Unit,
895906
) {
896907
SheetTitle("Sentry Buddy", "Live Feed")
897908
val emptyAttentionArtIndex = remember { EmptyAttentionArtIndex.next() }
@@ -902,6 +913,7 @@ private fun LiveFeedSheet(
902913
nowMs = nowMs,
903914
emptyArtIndex = emptyAttentionArtIndex,
904915
onDismiss = { onDispatch { dismissLiveFeedAttention() } },
916+
onOpenUrl = onOpenUrl,
905917
)
906918
Spacer(Modifier.height(12.dp))
907919
Button(
@@ -921,7 +933,7 @@ private fun LiveFeedSheet(
921933
if (liveFeed.items.isEmpty()) {
922934
EmptyLiveFeedCard()
923935
} else {
924-
LiveFeedRows(liveFeed.items.take(LIVE_FEED_VISIBLE_ITEM_LIMIT), sentryUiLinks, nowMs)
936+
LiveFeedRows(liveFeed.items.take(LIVE_FEED_VISIBLE_ITEM_LIMIT), sentryUiLinks, nowMs, onOpenUrl)
925937
}
926938
}
927939

@@ -932,6 +944,7 @@ private fun AttentionCard(
932944
nowMs: Long,
933945
emptyArtIndex: Int,
934946
onDismiss: () -> Unit,
947+
onOpenUrl: (Context, String) -> Unit,
935948
) {
936949
val item = liveFeed.latestUnviewedAdverseItem
937950
val dismissOffset = remember(item?.id) { Animatable(0f) }
@@ -1002,7 +1015,7 @@ private fun AttentionCard(
10021015
modifier =
10031016
Modifier.matchParentSize()
10041017
.offset { IntOffset(dismissOffset.value.roundToInt(), 0) }
1005-
.clickable(enabled = link != null) { link?.let { openSentryLink(context, it) } }
1018+
.clickable(enabled = link != null) { link?.let { onOpenUrl(context, it) } }
10061019
) {
10071020
AttentionItemContent(
10081021
item = item,
@@ -1125,6 +1138,7 @@ private fun LiveFeedRows(
11251138
items: List<BuddyLiveFeedItem>,
11261139
sentryUiLinks: BuddySentryUiLinks,
11271140
nowMs: Long,
1141+
onOpenUrl: (Context, String) -> Unit,
11281142
) {
11291143
val context = LocalContext.current
11301144
Surface(
@@ -1141,7 +1155,7 @@ private fun LiveFeedRows(
11411155
Row(
11421156
modifier =
11431157
Modifier.fillMaxWidth()
1144-
.clickable(enabled = link != null) { link?.let { openSentryLink(context, it) } }
1158+
.clickable(enabled = link != null) { link?.let { onOpenUrl(context, it) } }
11451159
.padding(vertical = 7.dp),
11461160
horizontalArrangement = Arrangement.spacedBy(10.dp),
11471161
verticalAlignment = Alignment.CenterVertically,
@@ -1413,14 +1427,6 @@ private fun relativeTime(timestampMs: Long, nowMs: Long): String {
14131427
return "${ageSeconds / 60}m ago"
14141428
}
14151429

1416-
private fun openSentryLink(context: Context, link: String) {
1417-
try {
1418-
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
1419-
} catch (_: ActivityNotFoundException) {
1420-
// A debug overlay should not crash the app when no browser can handle the link.
1421-
}
1422-
}
1423-
14241430
private fun Map<String, Any?>.mapValue(key: String): Map<*, *> =
14251431
this[key] as? Map<*, *> ?: emptyMap<Any, Any>()
14261432

0 commit comments

Comments
 (0)