diff --git a/CHANGELOG.md b/CHANGELOG.md index 4054f490ef..7325160718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - If you would like us to provide support for the old approach working alongside the new one on Android 11 and above (e.g. for raising events for slow code on main thread), consider upvoting [this issue](https://github.com/getsentry/sentry-java/issues/2693). - The old watchdog implementation will continue working for older API versions (Android < 11) - Open up `TransactionOptions`, `ITransaction` and `IHub` methods allowing consumers modify start/end timestamp of transactions and spans ([#2701](https://github.com/getsentry/sentry-java/pull/2701)) +- Send source bundle IDs to Sentry to enable source context ([#2663](https://github.com/getsentry/sentry-java/pull/2663)) + - For more information on how to enable source context, please refer to [#633](https://github.com/getsentry/sentry-java/issues/633#issuecomment-1465599120) ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 66d83cd971..67101e3655 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -287,12 +287,32 @@ private static void readDefaultOptionValues( } } - if (options.getProguardUuid() == null) { - options.setProguardUuid(getProguardUUID(context, options.getLogger())); + final @Nullable Properties debugMetaProperties = + loadDebugMetaProperties(context, options.getLogger()); + + if (debugMetaProperties != null) { + if (options.getProguardUuid() == null) { + final @Nullable String proguardUuid = + debugMetaProperties.getProperty("io.sentry.ProguardUuids"); + options.getLogger().log(SentryLevel.DEBUG, "Proguard UUID found: %s", proguardUuid); + options.setProguardUuid(proguardUuid); + } + + if (options.getBundleIds().isEmpty()) { + final @Nullable String bundleIdStrings = + debugMetaProperties.getProperty("io.sentry.bundle-ids"); + options.getLogger().log(SentryLevel.DEBUG, "Bundle IDs found: %s", bundleIdStrings); + if (bundleIdStrings != null) { + final @NotNull String[] bundleIds = bundleIdStrings.split(",", -1); + for (final String bundleId : bundleIds) { + options.addBundleId(bundleId); + } + } + } } } - private static @Nullable String getProguardUUID( + private static @Nullable Properties loadDebugMetaProperties( final @NotNull Context context, final @NotNull ILogger logger) { final AssetManager assets = context.getAssets(); // one may have thousands of asset files and looking up this list might slow down the SDK init. @@ -302,10 +322,7 @@ private static void readDefaultOptionValues( new BufferedInputStream(assets.open("sentry-debug-meta.properties"))) { final Properties properties = new Properties(); properties.load(is); - - final String uuid = properties.getProperty("io.sentry.ProguardUuids"); - logger.log(SentryLevel.DEBUG, "Proguard UUID found: %s", uuid); - return uuid; + return properties; } catch (FileNotFoundException e) { logger.log(SentryLevel.INFO, "sentry-debug-meta.properties file was not found."); } catch (IOException e) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 762777ce68..18bd55bba0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core import android.content.Context +import android.content.res.AssetManager import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,12 +47,14 @@ class AndroidOptionsInitializerTest { hasAppContext: Boolean = true, useRealContext: Boolean = false, configureOptions: SentryAndroidOptions.() -> Unit = {}, - configureContext: Context.() -> Unit = {} + configureContext: Context.() -> Unit = {}, + assets: AssetManager? = null ) { mockContext = if (metadata != null) { ContextUtilsTest.mockMetaData( mockContext = ContextUtilsTest.createMockContext(hasAppContext), - metaData = metadata + metaData = metadata, + assets = assets ) } else { ContextUtilsTest.createMockContext(hasAppContext) @@ -277,6 +280,46 @@ class AndroidOptionsInitializerTest { assertEquals("proguard-uuid", fixture.sentryOptions.proguardUuid) } + @Test + fun `init should set proguard uuid from properties id on start`() { + val assets = mock() + + whenever(assets.open("sentry-debug-meta.properties")).thenReturn( + """ + io.sentry.ProguardUuids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411 + """.trimIndent().byteInputStream() + ) + + fixture.initSut( + Bundle(), + hasAppContext = false, + assets = assets + ) + + assertNotNull(fixture.sentryOptions.proguardUuid) + assertEquals("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", fixture.sentryOptions.proguardUuid) + } + + @Test + fun `init should set bundle IDs id on start`() { + val assets = mock() + + whenever(assets.open("sentry-debug-meta.properties")).thenReturn( + """ + io.sentry.bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411, faa3ab42-b1bd-4659-af8e-1682324aa744 + """.trimIndent().byteInputStream() + ) + + fixture.initSut( + Bundle(), + hasAppContext = false, + assets = assets + ) + + assertTrue(fixture.sentryOptions.bundleIds.size == 2) + assertTrue(fixture.sentryOptions.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", "faa3ab42-b1bd-4659-af8e-1682324aa744"))) + } + @Test fun `init should set Android transport gate`() { fixture.initSut() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index b0a5e60685..c05d71e2b4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -12,17 +12,22 @@ import org.mockito.kotlin.whenever import java.io.FileNotFoundException object ContextUtilsTest { - fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle): Context { + fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle, assets: AssetManager? = null): Context { val mockPackageManager = mock() val mockApplicationInfo = mock() - val assets = mock() whenever(mockContext.packageName).thenReturn("io.sentry.sample.test") whenever(mockContext.packageManager).thenReturn(mockPackageManager) whenever(mockPackageManager.getApplicationInfo(mockContext.packageName, PackageManager.GET_META_DATA)) .thenReturn(mockApplicationInfo) - whenever(assets.open(any())).thenThrow(FileNotFoundException()) - whenever(mockContext.assets).thenReturn(assets) + + if (assets == null) { + val mockAssets = mock() + whenever(mockAssets.open(any())).thenThrow(FileNotFoundException()) + whenever(mockContext.assets).thenReturn(mockAssets) + } else { + whenever(mockContext.assets).thenReturn(assets) + } mockApplicationInfo.metaData = metaData return mockContext diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 197cedc540..c15750ead0 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -235,6 +235,7 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExternalOptions { public fun ()V + public fun addBundleId (Ljava/lang/String;)V public fun addContextTag (Ljava/lang/String;)V public fun addIgnoredExceptionForType (Ljava/lang/Class;)V public fun addInAppExclude (Ljava/lang/String;)V @@ -242,6 +243,7 @@ public final class io/sentry/ExternalOptions { public fun addTracePropagationTarget (Ljava/lang/String;)V public fun addTracingOrigin (Ljava/lang/String;)V public static fun from (Lio/sentry/config/PropertiesProvider;Lio/sentry/ILogger;)Lio/sentry/ExternalOptions; + public fun getBundleIds ()Ljava/util/Set; public fun getContextTags ()Ljava/util/List; public fun getDebug ()Ljava/lang/Boolean; public fun getDist ()Ljava/lang/String; @@ -1625,6 +1627,7 @@ public final class io/sentry/SentryNanotimeDateProvider : io/sentry/SentryDatePr public class io/sentry/SentryOptions { public fun ()V + public fun addBundleId (Ljava/lang/String;)V public fun addCollector (Lio/sentry/ICollector;)V public fun addContextTag (Ljava/lang/String;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V @@ -1638,6 +1641,7 @@ public class io/sentry/SentryOptions { public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback; public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback; public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback; + public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; public fun getCollectors ()Ljava/util/List; @@ -2857,6 +2861,7 @@ public final class io/sentry/protocol/Contexts$Deserializer : io/sentry/JsonDese } public final class io/sentry/protocol/DebugImage : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field JVM Ljava/lang/String; public static final field PROGUARD Ljava/lang/String; public fun ()V public fun getArch ()Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 7c1fafe97e..4ef3f31d89 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -41,6 +41,7 @@ public final class ExternalOptions { new CopyOnWriteArraySet<>(); private @Nullable Boolean printUncaughtStackTrace; private @Nullable Boolean sendClientReports; + private @NotNull Set bundleIds = new CopyOnWriteArraySet<>(); @SuppressWarnings("unchecked") public static @NotNull ExternalOptions from( @@ -109,6 +110,9 @@ public final class ExternalOptions { options.addContextTag(contextTag); } options.setProguardUuid(propertiesProvider.getProperty("proguard-uuid")); + for (final String bundleId : propertiesProvider.getList("bundle-ids")) { + options.addBundleId(bundleId); + } options.setIdleTimeout(propertiesProvider.getLongProperty("idle-timeout")); for (final String ignoredExceptionType : @@ -335,4 +339,12 @@ public void setIdleTimeout(final @Nullable Long idleTimeout) { public void setSendClientReports(final @Nullable Boolean sendClientReports) { this.sendClientReports = sendClientReports; } + + public @NotNull Set getBundleIds() { + return bundleIds; + } + + public void addBundleId(final @NotNull String bundleId) { + bundleIds.add(bundleId); + } } diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index eaacc5fd64..d046855958 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -65,23 +65,35 @@ public MainEventProcessor(final @NotNull SentryOptions options) { } private void setDebugMeta(final @NotNull SentryBaseEvent event) { + final @NotNull List debugImages = new ArrayList<>(); + if (options.getProguardUuid() != null) { + final DebugImage proguardMappingImage = new DebugImage(); + proguardMappingImage.setType(DebugImage.PROGUARD); + proguardMappingImage.setUuid(options.getProguardUuid()); + debugImages.add(proguardMappingImage); + } + + for (final @NotNull String bundleId : options.getBundleIds()) { + final DebugImage sourceBundleImage = new DebugImage(); + sourceBundleImage.setType(DebugImage.JVM); + sourceBundleImage.setDebugId(bundleId); + debugImages.add(sourceBundleImage); + } + + if (!debugImages.isEmpty()) { DebugMeta debugMeta = event.getDebugMeta(); if (debugMeta == null) { debugMeta = new DebugMeta(); } if (debugMeta.getImages() == null) { - debugMeta.setImages(new ArrayList<>()); - } - List images = debugMeta.getImages(); - if (images != null) { - final DebugImage debugImage = new DebugImage(); - debugImage.setType(DebugImage.PROGUARD); - debugImage.setUuid(options.getProguardUuid()); - images.add(debugImage); - event.setDebugMeta(debugMeta); + debugMeta.setImages(debugImages); + } else { + debugMeta.getImages().addAll(debugImages); } + + event.setDebugMeta(debugMeta); } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 830fa116e9..27bb827447 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -66,6 +66,9 @@ public class SentryOptions { */ private final @NotNull List integrations = new CopyOnWriteArrayList<>(); + /** List of bundle IDs representing source bundles. */ + private final @NotNull Set bundleIds = new CopyOnWriteArraySet<>(); + /** * The DSN tells the SDK where to send the events to. If this value is not provided, the SDK will * just not send any events. @@ -1797,6 +1800,31 @@ public void setProguardUuid(final @Nullable String proguardUuid) { this.proguardUuid = proguardUuid; } + /** + * Adds a bundle ID (also known as debugId) representing a source bundle that contains sources for + * this application. These sources will be used to source code for frames of an exceptions stack + * trace. + * + * @param bundleId Bundle ID generated by sentry-cli or the sentry-android-gradle-plugin + */ + public void addBundleId(final @Nullable String bundleId) { + if (bundleId != null) { + final @NotNull String trimmedBundleId = bundleId.trim(); + if (!trimmedBundleId.isEmpty()) { + this.bundleIds.add(trimmedBundleId); + } + } + } + + /** + * Returns all configured bundle IDs referencing source code bundles. + * + * @return list of bundle IDs + */ + public @NotNull Set getBundleIds() { + return bundleIds; + } + /** * Returns Context tags names applied to Sentry events as Sentry tags. * @@ -2250,6 +2278,9 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getIdleTimeout() != null) { setIdleTimeout(options.getIdleTimeout()); } + for (String bundleId : options.getBundleIds()) { + addBundleId(bundleId); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/main/java/io/sentry/protocol/DebugImage.java b/sentry/src/main/java/io/sentry/protocol/DebugImage.java index 128eabd53f..20d81143db 100644 --- a/sentry/src/main/java/io/sentry/protocol/DebugImage.java +++ b/sentry/src/main/java/io/sentry/protocol/DebugImage.java @@ -50,6 +50,7 @@ */ public final class DebugImage implements JsonUnknown, JsonSerializable { public static final String PROGUARD = "proguard"; + public static final String JVM = "jvm"; /** * The unique UUID of the image. diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index f2adefc972..57be223218 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -216,6 +216,37 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with single bundle ID using external properties`() { + withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411") { options -> + assertTrue(options.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411"))) + } + } + + @Test + fun `creates options with multiple bundle IDs using external properties`() { + withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411,faa3ab42-b1bd-4659-af8e-1682324aa744") { options -> + assertTrue(options.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", "faa3ab42-b1bd-4659-af8e-1682324aa744"))) + } + } + + @Test + fun `creates options with empty bundle IDs using external properties`() { + withPropertiesFile("bundle-ids=") { options -> + assertTrue(options.bundleIds.size == 1) + // trimming is tested in SentryOptionsTest so even though there's an empty string here + // it will be filtered when being merged with SentryOptions + assertTrue(options.bundleIds.containsAll(listOf(""))) + } + } + + @Test + fun `creates options with missing bundle IDs using external properties`() { + withPropertiesFile("") { options -> + assertTrue(options.bundleIds.isEmpty()) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index cc2774e323..4e6b4f3d99 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -49,6 +49,7 @@ class MainEventProcessorTest { resolveHostDelay: Long? = null, hostnameCacheDuration: Long = 10, proguardUuid: String? = null, + bundleIds: List? = null, modules: Map? = null ): MainEventProcessor { sentryOptions.isAttachThreads = attachThreads @@ -63,6 +64,7 @@ class MainEventProcessorTest { if (proguardUuid != null) { sentryOptions.proguardUuid = proguardUuid } + bundleIds?.let { it.forEach { sentryOptions.addBundleId(it) } } tags.forEach { sentryOptions.setTag(it.key, it.value) } whenever(getLocalhost.canonicalHostName).thenAnswer { if (resolveHostDelay != null) { @@ -488,6 +490,23 @@ class MainEventProcessorTest { } } + @Test + fun `when event does not have debug meta and bundle ids are set, attaches debug information`() { + val sut = fixture.getSut(bundleIds = listOf("id1", "id2")) + + var event = SentryEvent() + event = sut.process(event, Hint()) + + assertNotNull(event.debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + @Test fun `when event has debug meta and proguard uuids are set, attaches debug information`() { val sut = fixture.getSut(proguardUuid = "id1") @@ -504,6 +523,44 @@ class MainEventProcessorTest { } } + @Test + fun `when event has debug meta and bundle ids are set, attaches debug information`() { + val sut = fixture.getSut(bundleIds = listOf("id1", "id2")) + + var event = SentryEvent() + event.debugMeta = DebugMeta() + event = sut.process(event, Hint()) + + assertNotNull(event.debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta as well as images and bundle ids are set, attaches debug information`() { + val sut = fixture.getSut(bundleIds = listOf("id1", "id2")) + + var event = SentryEvent() + event.debugMeta = DebugMeta().also { + it.images = listOf() + } + event = sut.process(event, Hint()) + + assertNotNull(event.debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + @Test fun `when processor is closed, closes hostname cache`() { val sut = fixture.getSut(serverName = null) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 57927840b0..3d3b654dc6 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -367,6 +367,7 @@ class SentryOptionsTest { externalOptions.addContextTag("requestId") externalOptions.proguardUuid = "1234" externalOptions.idleTimeout = 1500L + externalOptions.bundleIds.addAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411 ", " faa3ab42-b1bd-4659-af8e-1682324aa744")) val options = SentryOptions() options.merge(externalOptions) @@ -390,6 +391,7 @@ class SentryOptionsTest { assertEquals(listOf("userId", "requestId"), options.contextTags) assertEquals("1234", options.proguardUuid) assertEquals(1500L, options.idleTimeout) + assertEquals(setOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", "faa3ab42-b1bd-4659-af8e-1682324aa744"), options.bundleIds) } @Test