Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send source bundle IDs to Sentry to enable source context #2663

Merged
merged 11 commits into from
May 15, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
adinauer marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,53 @@ 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 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.
// quite a bit, for this reason, we try to open the file directly and take care of errors
// like FileNotFoundException
try (final InputStream is =
new BufferedInputStream(assets.open("sentry-debug-meta.properties"))) {
final Properties properties = new Properties();
properties.load(is);
return properties;
} catch (FileNotFoundException e) {
logger.log(SentryLevel.INFO, "sentry-debug-meta.properties file was not found.");
} catch (IOException e) {
logger.log(SentryLevel.ERROR, "Error getting Proguard UUIDs.", e);
} catch (RuntimeException e) {
logger.log(SentryLevel.ERROR, "sentry-debug-meta.properties file is malformed.", e);
}

return null;
}

private static @Nullable String getProguardUUID(
adinauer marked this conversation as resolved.
Show resolved Hide resolved
final @NotNull Context context, final @NotNull ILogger logger) {
final AssetManager assets = context.getAssets();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<AssetManager>()

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<AssetManager>()

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PackageManager>()
val mockApplicationInfo = mock<ApplicationInfo>()
val assets = mock<AssetManager>()

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<AssetManager>()
whenever(mockAssets.open(any())).thenThrow(FileNotFoundException())
whenever(mockContext.assets).thenReturn(mockAssets)
} else {
whenever(mockContext.assets).thenReturn(assets)
}

mockApplicationInfo.metaData = metaData
return mockContext
Expand Down
5 changes: 5 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,15 @@ public abstract interface class io/sentry/EventProcessor {

public final class io/sentry/ExternalOptions {
public fun <init> ()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
public fun addInAppInclude (Ljava/lang/String;)V
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;
Expand Down Expand Up @@ -1625,6 +1627,7 @@ public final class io/sentry/SentryNanotimeDateProvider : io/sentry/SentryDatePr

public class io/sentry/SentryOptions {
public fun <init> ()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
Expand All @@ -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;
Expand Down Expand Up @@ -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 <init> ()V
public fun getArch ()Ljava/lang/String;
Expand Down
12 changes: 12 additions & 0 deletions sentry/src/main/java/io/sentry/ExternalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public final class ExternalOptions {
new CopyOnWriteArraySet<>();
private @Nullable Boolean printUncaughtStackTrace;
private @Nullable Boolean sendClientReports;
private @NotNull Set<String> bundleIds = new CopyOnWriteArraySet<>();

@SuppressWarnings("unchecked")
public static @NotNull ExternalOptions from(
Expand Down Expand Up @@ -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 :
Expand Down Expand Up @@ -335,4 +339,12 @@ public void setIdleTimeout(final @Nullable Long idleTimeout) {
public void setSendClientReports(final @Nullable Boolean sendClientReports) {
this.sendClientReports = sendClientReports;
}

public @NotNull Set<String> getBundleIds() {
return bundleIds;
}

public void addBundleId(final @NotNull String bundleId) {
bundleIds.add(bundleId);
}
}
30 changes: 21 additions & 9 deletions sentry/src/main/java/io/sentry/MainEventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,35 @@ public MainEventProcessor(final @NotNull SentryOptions options) {
}

private void setDebugMeta(final @NotNull SentryBaseEvent event) {
final @NotNull List<DebugImage> 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<DebugImage> 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);
}
}

Expand Down
31 changes: 31 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public class SentryOptions {
*/
private final @NotNull List<Integration> integrations = new CopyOnWriteArrayList<>();

/** List of bundle IDs representing source bundles. */
private final @NotNull Set<String> 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.
Expand Down Expand Up @@ -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<String> getBundleIds() {
return bundleIds;
}

/**
* Returns Context tags names applied to Sentry events as Sentry tags.
*
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions sentry/src/main/java/io/sentry/protocol/DebugImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions sentry/src/test/java/io/sentry/ExternalOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) {
// create a sentry.properties file in temporary folder
val temporaryFolder = TemporaryFolder()
Expand Down