diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce997f3b1..77c28a927b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - Report process init time as a span for app start performance ([#3159](https://github.com/getsentry/sentry-java/pull/3159)) - (perf-v2): Calculate frame delay on a span level ([#3197](https://github.com/getsentry/sentry-java/pull/3197)) - Resolve spring properties in @SentryCheckIn annotation ([#3194](https://github.com/getsentry/sentry-java/pull/3194)) +- Experimental: Add Spotlight integration ([#3166](https://github.com/getsentry/sentry-java/pull/3166)) + - For more details about Spotlight head over to https://spotlightjs.com/ + - Set `options.isEnableSpotlight = true` to enable Spotlight ### Fixes diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index bd5b3695fb..cd0f8ed8c0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -21,6 +21,7 @@ import io.sentry.SentryOptions import io.sentry.SentryOptions.BeforeSendCallback import io.sentry.Session import io.sentry.ShutdownHookIntegration +import io.sentry.SpotlightIntegration import io.sentry.SystemOutLogger import io.sentry.UncaughtExceptionHandlerIntegration import io.sentry.android.core.cache.AndroidEnvelopeCache @@ -412,7 +413,7 @@ class SentryAndroidTest { fixture.initSut(context = mock()) { options -> optionsRef = options options.dsn = "https://key@sentry.io/123" - assertEquals(19, options.integrations.size) + assertEquals(20, options.integrations.size) options.integrations.removeAll { it is UncaughtExceptionHandlerIntegration || it is ShutdownHookIntegration || @@ -431,7 +432,8 @@ class SentryAndroidTest { it is SystemEventsBreadcrumbsIntegration || it is NetworkBreadcrumbsIntegration || it is TempSensorBreadcrumbsIntegration || - it is PhoneStateBreadcrumbsIntegration + it is PhoneStateBreadcrumbsIntegration || + it is SpotlightIntegration } } assertEquals(0, optionsRef.integrations.size) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index f8609c1c29..4b4ed4dc58 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network" tools:ignore="GoogleAppIndexingWarning, UnusedAttribute"> + + + + + 10.0.2.2 + + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 040a9bb208..416ea954bb 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -14,6 +14,8 @@ sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR sentry.enable-backpressure-handling=true +sentry.enable-spotlight=true +sentry.enablePrettySerializationOutput=false in-app-includes="io.sentry.samples" # Uncomment and set to true to enable aot compatibility diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index da727346a8..b46483457d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2159,6 +2159,7 @@ public class io/sentry/SentryOptions { public static fun empty ()Lio/sentry/SentryOptions; public fun getBackpressureMonitor ()Lio/sentry/backpressure/IBackpressureMonitor; public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback; + public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback; public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback; public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback; public fun getBundleIds ()Ljava/util/Set; @@ -2221,6 +2222,7 @@ public class io/sentry/SentryOptions { public fun getSessionTrackingIntervalMillis ()J public fun getShutdownTimeout ()J public fun getShutdownTimeoutMillis ()J + public fun getSpotlightConnectionUrl ()Ljava/lang/String; public fun getSslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public fun getTags ()Ljava/util/Map; public fun getTracePropagationTargets ()Ljava/util/List; @@ -2243,6 +2245,7 @@ public class io/sentry/SentryOptions { public fun isEnableExternalConfiguration ()Z public fun isEnablePrettySerializationOutput ()Z public fun isEnableShutdownHook ()Z + public fun isEnableSpotlight ()Z public fun isEnableTimeToFullDisplayTracing ()Z public fun isEnableUncaughtExceptionHandler ()Z public fun isEnableUserInteractionBreadcrumbs ()Z @@ -2262,6 +2265,7 @@ public class io/sentry/SentryOptions { public fun setAttachThreads (Z)V public fun setBackpressureMonitor (Lio/sentry/backpressure/IBackpressureMonitor;)V public fun setBeforeBreadcrumb (Lio/sentry/SentryOptions$BeforeBreadcrumbCallback;)V + public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V @@ -2281,6 +2285,7 @@ public class io/sentry/SentryOptions { public fun setEnableExternalConfiguration (Z)V public fun setEnablePrettySerializationOutput (Z)V public fun setEnableShutdownHook (Z)V + public fun setEnableSpotlight (Z)V public fun setEnableTimeToFullDisplayTracing (Z)V public fun setEnableTracing (Ljava/lang/Boolean;)V public fun setEnableUncaughtExceptionHandler (Z)V @@ -2328,6 +2333,7 @@ public class io/sentry/SentryOptions { public fun setSessionTrackingIntervalMillis (J)V public fun setShutdownTimeout (J)V public fun setShutdownTimeoutMillis (J)V + public fun setSpotlightConnectionUrl (Ljava/lang/String;)V public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTraceOptionsRequests (Z)V @@ -2347,6 +2353,10 @@ public abstract interface class io/sentry/SentryOptions$BeforeBreadcrumbCallback public abstract fun execute (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)Lio/sentry/Breadcrumb; } +public abstract interface class io/sentry/SentryOptions$BeforeEnvelopeCallback { + public abstract fun execute (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V +} + public abstract interface class io/sentry/SentryOptions$BeforeSendCallback { public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } @@ -2721,6 +2731,14 @@ public final class io/sentry/SpanStatus$Deserializer : io/sentry/JsonDeserialize public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } +public final class io/sentry/SpotlightIntegration : io/sentry/Integration, io/sentry/SentryOptions$BeforeEnvelopeCallback, java/io/Closeable { + public fun ()V + public fun close ()V + public fun execute (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V + public fun getSpotlightConnectionUrl ()Ljava/lang/String; + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V +} + public final class io/sentry/SystemOutLogger : io/sentry/ILogger { public fun ()V public fun isEnabled (Lio/sentry/SentryLevel;)Z diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 57f4559ab3..43236d6345 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -212,12 +212,12 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul final boolean shouldSendAttachments = event != null; List attachments = shouldSendAttachments ? getAttachments(hint) : null; - final SentryEnvelope envelope = + final @Nullable SentryEnvelope envelope = buildEnvelope(event, attachments, session, traceContext, null); hint.clear(); if (envelope != null) { - transport.send(envelope, hint); + sentryId = sendEnvelope(envelope, hint); } } catch (IOException | SentryEnvelopeException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing event %s failed.", sentryId); @@ -445,8 +445,8 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { .log(SentryLevel.DEBUG, "Capturing userFeedback: %s", userFeedback.getEventId()); try { - final SentryEnvelope envelope = buildEnvelope(userFeedback); - transport.send(envelope); + final @NotNull SentryEnvelope envelope = buildEnvelope(userFeedback); + sendEnvelope(envelope, null); } catch (IOException e) { options .getLogger() @@ -582,17 +582,33 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint try { hint.clear(); - transport.send(envelope, hint); + return sendEnvelope(envelope, hint); } catch (IOException e) { options.getLogger().log(SentryLevel.ERROR, "Failed to capture envelope.", e); - return SentryId.EMPTY_ID; } - final SentryId eventId = envelope.getHeader().getEventId(); - if (eventId != null) { - return eventId; + return SentryId.EMPTY_ID; + } + + private @NotNull SentryId sendEnvelope( + @NotNull final SentryEnvelope envelope, @Nullable final Hint hint) throws IOException { + final @Nullable SentryOptions.BeforeEnvelopeCallback beforeEnvelopeCallback = + options.getBeforeEnvelopeCallback(); + if (beforeEnvelopeCallback != null) { + try { + beforeEnvelopeCallback.execute(envelope, hint); + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.ERROR, "The BeforeEnvelope callback threw an exception.", e); + } + } + if (hint == null) { + transport.send(envelope); } else { - return SentryId.EMPTY_ID; + transport.send(envelope, hint); } + final @Nullable SentryId id = envelope.getHeader().getEventId(); + return id != null ? id : SentryId.EMPTY_ID; } @Override @@ -665,9 +681,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint hint.clear(); if (envelope != null) { - transport.send(envelope, hint); - } else { - sentryId = SentryId.EMPTY_ID; + sentryId = sendEnvelope(envelope, hint); } } catch (IOException | SentryEnvelopeException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing transaction %s failed.", sentryId); @@ -729,10 +743,10 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint } } - final SentryEnvelope envelope = buildEnvelope(checkIn, traceContext); + final @NotNull SentryEnvelope envelope = buildEnvelope(checkIn, traceContext); hint.clear(); - transport.send(envelope, hint); + sentryId = sendEnvelope(envelope, hint); } catch (IOException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing check-in %s failed.", sentryId); // if there was an error capturing the event, we return an emptyId diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d97b8c79d1..bfc51d9218 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -442,6 +442,12 @@ public class SentryOptions { /** Whether to send modules containing information about versions. */ private boolean sendModules = true; + private @Nullable BeforeEnvelopeCallback beforeEnvelopeCallback; + + private boolean enableSpotlight = false; + + private @Nullable String spotlightConnectionUrl; + /** Contains a list of monitor slugs for which check-ins should not be sent. */ @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; @@ -2274,6 +2280,39 @@ public void setSessionFlushTimeoutMillis(final long sessionFlushTimeoutMillis) { this.sessionFlushTimeoutMillis = sessionFlushTimeoutMillis; } + @ApiStatus.Internal + @Nullable + public BeforeEnvelopeCallback getBeforeEnvelopeCallback() { + return beforeEnvelopeCallback; + } + + @ApiStatus.Internal + public void setBeforeEnvelopeCallback( + @Nullable final BeforeEnvelopeCallback beforeEnvelopeCallback) { + this.beforeEnvelopeCallback = beforeEnvelopeCallback; + } + + @ApiStatus.Experimental + @Nullable + public String getSpotlightConnectionUrl() { + return spotlightConnectionUrl; + } + + @ApiStatus.Experimental + public void setSpotlightConnectionUrl(final @Nullable String spotlightConnectionUrl) { + this.spotlightConnectionUrl = spotlightConnectionUrl; + } + + @ApiStatus.Experimental + public boolean isEnableSpotlight() { + return enableSpotlight; + } + + @ApiStatus.Experimental + public void setEnableSpotlight(final boolean enableSpotlight) { + this.enableSpotlight = enableSpotlight; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { @@ -2345,6 +2384,19 @@ public interface ProfilesSamplerCallback { Double sample(@NotNull SamplingContext samplingContext); } + /** The BeforeEnvelope callback */ + @ApiStatus.Internal + public interface BeforeEnvelopeCallback { + + /** + * A callback which gets called right before an envelope is about to be sent + * + * @param envelope the envelope + * @param hint the hints + */ + void execute(@NotNull SentryEnvelope envelope, @Nullable Hint hint); + } + /** * Creates SentryOptions instance without initializing any of the internal parts. * @@ -2378,6 +2430,7 @@ private SentryOptions(final boolean empty) { integrations.add(new UncaughtExceptionHandlerIntegration()); integrations.add(new ShutdownHookIntegration()); + integrations.add(new SpotlightIntegration()); eventProcessors.add(new MainEventProcessor(this)); eventProcessors.add(new DuplicateEventDetectionEventProcessor(this)); diff --git a/sentry/src/main/java/io/sentry/SpotlightIntegration.java b/sentry/src/main/java/io/sentry/SpotlightIntegration.java new file mode 100644 index 0000000000..6d488bcbce --- /dev/null +++ b/sentry/src/main/java/io/sentry/SpotlightIntegration.java @@ -0,0 +1,136 @@ +package io.sentry; + +import static io.sentry.SentryLevel.DEBUG; +import static io.sentry.SentryLevel.ERROR; +import static io.sentry.SentryLevel.WARNING; + +import io.sentry.util.Platform; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.concurrent.RejectedExecutionException; +import java.util.zip.GZIPOutputStream; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +@ApiStatus.Internal +public final class SpotlightIntegration + implements Integration, SentryOptions.BeforeEnvelopeCallback, Closeable { + + private @Nullable SentryOptions options; + private @NotNull ILogger logger = NoOpLogger.getInstance(); + private @NotNull ISentryExecutorService executorService = NoOpSentryExecutorService.getInstance(); + + @Override + public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + this.options = options; + this.logger = options.getLogger(); + + if (options.getBeforeEnvelopeCallback() == null && options.isEnableSpotlight()) { + executorService = new SentryExecutorService(); + options.setBeforeEnvelopeCallback(this); + logger.log(DEBUG, "SpotlightIntegration enabled."); + } else { + logger.log( + DEBUG, + "SpotlightIntegration is not enabled. " + + "BeforeEnvelopeCallback is already set or spotlight is not enabled."); + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void execute(final @NotNull SentryEnvelope envelope, final @Nullable Hint hint) { + try { + executorService.submit(() -> sendEnvelope(envelope)); + } catch (RejectedExecutionException e) { + logger.log(WARNING, "Spotlight envelope submission rejected.", e); + } + } + + private void sendEnvelope(final @NotNull SentryEnvelope envelope) { + try { + if (options == null) { + throw new IllegalArgumentException("SentryOptions are required to send envelopes."); + } + final String spotlightConnectionUrl = getSpotlightConnectionUrl(); + + final HttpURLConnection connection = createConnection(spotlightConnectionUrl); + try (final OutputStream outputStream = connection.getOutputStream(); + final GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + options.getSerializer().serialize(envelope, gzip); + } catch (Throwable e) { + logger.log( + ERROR, "An exception occurred while submitting the envelope to the Sentry server.", e); + } finally { + final int responseCode = connection.getResponseCode(); + logger.log(DEBUG, "Envelope sent to spotlight: %d", responseCode); + closeAndDisconnect(connection); + } + } catch (final Exception e) { + logger.log(ERROR, "An exception occurred while creating the connection to spotlight.", e); + } + } + + @TestOnly + public String getSpotlightConnectionUrl() { + if (options != null && options.getSpotlightConnectionUrl() != null) { + return options.getSpotlightConnectionUrl(); + } + if (Platform.isAndroid()) { + // developer machine should be the same across emulators + // see https://developer.android.com/studio/run/emulator-networking.html + return "http://10.0.2.2:8969/stream"; + } else { + return "http://localhost:8969/stream"; + } + } + + private @NotNull HttpURLConnection createConnection(final @NotNull String url) throws Exception { + + final @NotNull HttpURLConnection connection = + (HttpURLConnection) URI.create(url).toURL().openConnection(); + + connection.setReadTimeout(1000); + connection.setConnectTimeout(1000); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + connection.setRequestProperty("Content-Encoding", "gzip"); + connection.setRequestProperty("Content-Type", "application/x-sentry-envelope"); + connection.setRequestProperty("Accept", "application/json"); + + // https://stackoverflow.com/questions/52726909/java-io-ioexception-unexpected-end-of-stream-on-connection/53089882 + connection.setRequestProperty("Connection", "close"); + + connection.connect(); + return connection; + } + + /** + * Closes the Response stream and disconnect the connection + * + * @param connection the HttpURLConnection + */ + private void closeAndDisconnect(final @NotNull HttpURLConnection connection) { + try { + connection.getInputStream().close(); + } catch (IOException ignored) { + // connection is already closed + } finally { + connection.disconnect(); + } + } + + @Override + public void close() throws IOException { + executorService.close(0); + if (options != null && options.getBeforeEnvelopeCallback() == this) { + options.setBeforeEnvelopeCallback(null); + } + } +} diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index d5c7a54845..23a7c0bcaf 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2499,6 +2499,32 @@ class SentryClientTest { ) } + @Test + fun `beforeEnvelopeCallback is executed`() { + var beforeEnvelopeCalled = false + val sut = fixture.getSut { options -> + options.beforeEnvelopeCallback = + SentryOptions.BeforeEnvelopeCallback { _, _ -> beforeEnvelopeCalled = true } + } + + sut.captureEvent(SentryEvent(), Hint()) + + assertTrue(beforeEnvelopeCalled) + } + + @Test + fun `beforeEnvelopeCallback may fail, but the transport is still sends the envelope `() { + val sut = fixture.getSut { options -> + options.beforeEnvelopeCallback = + SentryOptions.BeforeEnvelopeCallback { _, _ -> + RuntimeException("hook failed") + } + } + + sut.captureEvent(SentryEvent(), Hint()) + verify(fixture.transport).send(anyOrNull(), anyOrNull()) + } + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope { val scope = createScope(fixture.sentryOptions) scope.startSession() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index d0b648bb60..213ce69b56 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -586,4 +586,21 @@ class SentryOptionsTest { options.profilingTracesHz = 13 assertEquals(13, options.profilingTracesHz) } + + @Test + fun `when options are initialized, spotlight is disabled by default and no url is set`() { + val options = SentryOptions() + assertFalse(options.isEnableSpotlight) + assertNull(options.spotlightConnectionUrl) + } + + @Test + fun `when spotlight is configured, getters reflect that`() { + val options = SentryOptions().apply { + isEnableSpotlight = true + spotlightConnectionUrl = "http://localhost:8080" + } + assertTrue(options.isEnableSpotlight) + assertEquals("http://localhost:8080", options.spotlightConnectionUrl) + } } diff --git a/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt b/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt new file mode 100644 index 0000000000..73aa339f26 --- /dev/null +++ b/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt @@ -0,0 +1,78 @@ +package io.sentry.internal + +import io.sentry.IHub +import io.sentry.SentryOptions +import io.sentry.SentryOptions.BeforeEnvelopeCallback +import io.sentry.SpotlightIntegration +import io.sentry.util.PlatformTestManipulator +import org.mockito.kotlin.mock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SpotlightIntegrationTest { + + @Test + fun `Integration does not register before-envelope callback when disabled`() { + val options = SentryOptions().apply { + isEnableSpotlight = false + } + + val spotlight = SpotlightIntegration() + spotlight.register(mock(), options) + + assertNull(options.beforeEnvelopeCallback) + } + + @Test + fun `Integration does not register before-envelope callback when before-envelope is already set`() { + val envelopeCallback = mock() + val options = SentryOptions().apply { + isEnableSpotlight = true + beforeEnvelopeCallback = envelopeCallback + } + + val spotlight = SpotlightIntegration() + spotlight.register(mock(), options) + + assertEquals(envelopeCallback, options.beforeEnvelopeCallback) + } + + @Test + fun `Integration does register and un-register before-envelope callback`() { + val options = SentryOptions().apply { + isEnableSpotlight = true + } + + val spotlight = SpotlightIntegration() + spotlight.register(mock(), options) + + assertEquals(options.beforeEnvelopeCallback, spotlight) + spotlight.close() + assertNull(options.beforeEnvelopeCallback) + } + + @Test + fun `spotlight connection url falls back to platform defaults`() { + val spotlight = SpotlightIntegration() + + PlatformTestManipulator.pretendIsAndroid(true) + assertEquals("http://10.0.2.2:8969/stream", spotlight.spotlightConnectionUrl) + + PlatformTestManipulator.pretendIsAndroid(false) + assertEquals("http://localhost:8969/stream", spotlight.spotlightConnectionUrl) + } + + @Test + fun `respects spotlight connection url set via options`() { + val options = SentryOptions().apply { + isEnableSpotlight = true + spotlightConnectionUrl = "http://example.com:1234/stream" + } + + val spotlight = SpotlightIntegration() + spotlight.register(mock(), options) + + assertEquals("http://example.com:1234/stream", spotlight.spotlightConnectionUrl) + } +}