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
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,57 @@ private static void readDefaultOptionValues(
}
}

if (options.getProguardUuid() == null) {
options.setProguardUuid(getProguardUUID(context, options.getLogger()));
// TODO when to parse, when not?
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) {
// TODO really nullable?
final @Nullable String[] bundleIds = bundleIdStrings.split(",", -1);
if (bundleIds != null) {
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
Expand Up @@ -266,6 +266,19 @@ class AndroidOptionsInitializerTest {
assertEquals("proguard-uuid", fixture.sentryOptions.proguardUuid)
}

@Test
fun `init should set bundle IDs id on start`() {
fixture.initSut(
Bundle().apply {
putString("io.sentry.bundle-ids", "12ea7a02-46ac-44c0-a5bb-6d1fd9586411, faa3ab42-b1bd-4659-af8e-1682324aa744")
},
hasAppContext = false
)

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
5 changes: 5 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,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 @@ -1541,6 +1543,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 @@ -1553,6 +1556,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 @@ -2688,6 +2692,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);
}
}
31 changes: 22 additions & 9 deletions sentry/src/main/java/io/sentry/MainEventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -66,23 +67,35 @@ public MainEventProcessor(final @NotNull SentryOptions options) {
}

private void setDebugMeta(final @NotNull SentryBaseEvent event) {
final @NotNull List<DebugImage> debugImages = new CopyOnWriteArrayList<>();
adinauer marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -1776,6 +1779,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 @@ -2229,6 +2257,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
2 changes: 2 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,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)
Expand All @@ -380,6 +381,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
Expand Down