Skip to content

Commit 70ef023

Browse files
adinauerromtsn
andauthored
Send source bundle IDs to Sentry to enable source context (#2663)
Co-authored-by: Roman Zavarnitsyn <[email protected]>
1 parent a78496a commit 70ef023

File tree

12 files changed

+240
-22
lines changed

12 files changed

+240
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- 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).
1616
- The old watchdog implementation will continue working for older API versions (Android < 11)
1717
- 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))
18+
- Send source bundle IDs to Sentry to enable source context ([#2663](https://github.com/getsentry/sentry-java/pull/2663))
19+
- For more information on how to enable source context, please refer to [#633](https://github.com/getsentry/sentry-java/issues/633#issuecomment-1465599120)
1820

1921
### Fixes
2022

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,32 @@ private static void readDefaultOptionValues(
287287
}
288288
}
289289

290-
if (options.getProguardUuid() == null) {
291-
options.setProguardUuid(getProguardUUID(context, options.getLogger()));
290+
final @Nullable Properties debugMetaProperties =
291+
loadDebugMetaProperties(context, options.getLogger());
292+
293+
if (debugMetaProperties != null) {
294+
if (options.getProguardUuid() == null) {
295+
final @Nullable String proguardUuid =
296+
debugMetaProperties.getProperty("io.sentry.ProguardUuids");
297+
options.getLogger().log(SentryLevel.DEBUG, "Proguard UUID found: %s", proguardUuid);
298+
options.setProguardUuid(proguardUuid);
299+
}
300+
301+
if (options.getBundleIds().isEmpty()) {
302+
final @Nullable String bundleIdStrings =
303+
debugMetaProperties.getProperty("io.sentry.bundle-ids");
304+
options.getLogger().log(SentryLevel.DEBUG, "Bundle IDs found: %s", bundleIdStrings);
305+
if (bundleIdStrings != null) {
306+
final @NotNull String[] bundleIds = bundleIdStrings.split(",", -1);
307+
for (final String bundleId : bundleIds) {
308+
options.addBundleId(bundleId);
309+
}
310+
}
311+
}
292312
}
293313
}
294314

295-
private static @Nullable String getProguardUUID(
315+
private static @Nullable Properties loadDebugMetaProperties(
296316
final @NotNull Context context, final @NotNull ILogger logger) {
297317
final AssetManager assets = context.getAssets();
298318
// 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(
302322
new BufferedInputStream(assets.open("sentry-debug-meta.properties"))) {
303323
final Properties properties = new Properties();
304324
properties.load(is);
305-
306-
final String uuid = properties.getProperty("io.sentry.ProguardUuids");
307-
logger.log(SentryLevel.DEBUG, "Proguard UUID found: %s", uuid);
308-
return uuid;
325+
return properties;
309326
} catch (FileNotFoundException e) {
310327
logger.log(SentryLevel.INFO, "sentry-debug-meta.properties file was not found.");
311328
} catch (IOException e) {

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.android.core
22

33
import android.content.Context
4+
import android.content.res.AssetManager
45
import android.os.Bundle
56
import androidx.test.core.app.ApplicationProvider
67
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -46,12 +47,14 @@ class AndroidOptionsInitializerTest {
4647
hasAppContext: Boolean = true,
4748
useRealContext: Boolean = false,
4849
configureOptions: SentryAndroidOptions.() -> Unit = {},
49-
configureContext: Context.() -> Unit = {}
50+
configureContext: Context.() -> Unit = {},
51+
assets: AssetManager? = null
5052
) {
5153
mockContext = if (metadata != null) {
5254
ContextUtilsTest.mockMetaData(
5355
mockContext = ContextUtilsTest.createMockContext(hasAppContext),
54-
metaData = metadata
56+
metaData = metadata,
57+
assets = assets
5558
)
5659
} else {
5760
ContextUtilsTest.createMockContext(hasAppContext)
@@ -277,6 +280,46 @@ class AndroidOptionsInitializerTest {
277280
assertEquals("proguard-uuid", fixture.sentryOptions.proguardUuid)
278281
}
279282

283+
@Test
284+
fun `init should set proguard uuid from properties id on start`() {
285+
val assets = mock<AssetManager>()
286+
287+
whenever(assets.open("sentry-debug-meta.properties")).thenReturn(
288+
"""
289+
io.sentry.ProguardUuids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411
290+
""".trimIndent().byteInputStream()
291+
)
292+
293+
fixture.initSut(
294+
Bundle(),
295+
hasAppContext = false,
296+
assets = assets
297+
)
298+
299+
assertNotNull(fixture.sentryOptions.proguardUuid)
300+
assertEquals("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", fixture.sentryOptions.proguardUuid)
301+
}
302+
303+
@Test
304+
fun `init should set bundle IDs id on start`() {
305+
val assets = mock<AssetManager>()
306+
307+
whenever(assets.open("sentry-debug-meta.properties")).thenReturn(
308+
"""
309+
io.sentry.bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411, faa3ab42-b1bd-4659-af8e-1682324aa744
310+
""".trimIndent().byteInputStream()
311+
)
312+
313+
fixture.initSut(
314+
Bundle(),
315+
hasAppContext = false,
316+
assets = assets
317+
)
318+
319+
assertTrue(fixture.sentryOptions.bundleIds.size == 2)
320+
assertTrue(fixture.sentryOptions.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", "faa3ab42-b1bd-4659-af8e-1682324aa744")))
321+
}
322+
280323
@Test
281324
fun `init should set Android transport gate`() {
282325
fixture.initSut()

sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,22 @@ import org.mockito.kotlin.whenever
1212
import java.io.FileNotFoundException
1313

1414
object ContextUtilsTest {
15-
fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle): Context {
15+
fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle, assets: AssetManager? = null): Context {
1616
val mockPackageManager = mock<PackageManager>()
1717
val mockApplicationInfo = mock<ApplicationInfo>()
18-
val assets = mock<AssetManager>()
1918

2019
whenever(mockContext.packageName).thenReturn("io.sentry.sample.test")
2120
whenever(mockContext.packageManager).thenReturn(mockPackageManager)
2221
whenever(mockPackageManager.getApplicationInfo(mockContext.packageName, PackageManager.GET_META_DATA))
2322
.thenReturn(mockApplicationInfo)
24-
whenever(assets.open(any())).thenThrow(FileNotFoundException())
25-
whenever(mockContext.assets).thenReturn(assets)
23+
24+
if (assets == null) {
25+
val mockAssets = mock<AssetManager>()
26+
whenever(mockAssets.open(any())).thenThrow(FileNotFoundException())
27+
whenever(mockContext.assets).thenReturn(mockAssets)
28+
} else {
29+
whenever(mockContext.assets).thenReturn(assets)
30+
}
2631

2732
mockApplicationInfo.metaData = metaData
2833
return mockContext

sentry/api/sentry.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,15 @@ public abstract interface class io/sentry/EventProcessor {
235235

236236
public final class io/sentry/ExternalOptions {
237237
public fun <init> ()V
238+
public fun addBundleId (Ljava/lang/String;)V
238239
public fun addContextTag (Ljava/lang/String;)V
239240
public fun addIgnoredExceptionForType (Ljava/lang/Class;)V
240241
public fun addInAppExclude (Ljava/lang/String;)V
241242
public fun addInAppInclude (Ljava/lang/String;)V
242243
public fun addTracePropagationTarget (Ljava/lang/String;)V
243244
public fun addTracingOrigin (Ljava/lang/String;)V
244245
public static fun from (Lio/sentry/config/PropertiesProvider;Lio/sentry/ILogger;)Lio/sentry/ExternalOptions;
246+
public fun getBundleIds ()Ljava/util/Set;
245247
public fun getContextTags ()Ljava/util/List;
246248
public fun getDebug ()Ljava/lang/Boolean;
247249
public fun getDist ()Ljava/lang/String;
@@ -1625,6 +1627,7 @@ public final class io/sentry/SentryNanotimeDateProvider : io/sentry/SentryDatePr
16251627

16261628
public class io/sentry/SentryOptions {
16271629
public fun <init> ()V
1630+
public fun addBundleId (Ljava/lang/String;)V
16281631
public fun addCollector (Lio/sentry/ICollector;)V
16291632
public fun addContextTag (Ljava/lang/String;)V
16301633
public fun addEventProcessor (Lio/sentry/EventProcessor;)V
@@ -1638,6 +1641,7 @@ public class io/sentry/SentryOptions {
16381641
public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback;
16391642
public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback;
16401643
public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback;
1644+
public fun getBundleIds ()Ljava/util/Set;
16411645
public fun getCacheDirPath ()Ljava/lang/String;
16421646
public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder;
16431647
public fun getCollectors ()Ljava/util/List;
@@ -2857,6 +2861,7 @@ public final class io/sentry/protocol/Contexts$Deserializer : io/sentry/JsonDese
28572861
}
28582862

28592863
public final class io/sentry/protocol/DebugImage : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
2864+
public static final field JVM Ljava/lang/String;
28602865
public static final field PROGUARD Ljava/lang/String;
28612866
public fun <init> ()V
28622867
public fun getArch ()Ljava/lang/String;

sentry/src/main/java/io/sentry/ExternalOptions.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public final class ExternalOptions {
4141
new CopyOnWriteArraySet<>();
4242
private @Nullable Boolean printUncaughtStackTrace;
4343
private @Nullable Boolean sendClientReports;
44+
private @NotNull Set<String> bundleIds = new CopyOnWriteArraySet<>();
4445

4546
@SuppressWarnings("unchecked")
4647
public static @NotNull ExternalOptions from(
@@ -109,6 +110,9 @@ public final class ExternalOptions {
109110
options.addContextTag(contextTag);
110111
}
111112
options.setProguardUuid(propertiesProvider.getProperty("proguard-uuid"));
113+
for (final String bundleId : propertiesProvider.getList("bundle-ids")) {
114+
options.addBundleId(bundleId);
115+
}
112116
options.setIdleTimeout(propertiesProvider.getLongProperty("idle-timeout"));
113117

114118
for (final String ignoredExceptionType :
@@ -335,4 +339,12 @@ public void setIdleTimeout(final @Nullable Long idleTimeout) {
335339
public void setSendClientReports(final @Nullable Boolean sendClientReports) {
336340
this.sendClientReports = sendClientReports;
337341
}
342+
343+
public @NotNull Set<String> getBundleIds() {
344+
return bundleIds;
345+
}
346+
347+
public void addBundleId(final @NotNull String bundleId) {
348+
bundleIds.add(bundleId);
349+
}
338350
}

sentry/src/main/java/io/sentry/MainEventProcessor.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,35 @@ public MainEventProcessor(final @NotNull SentryOptions options) {
6565
}
6666

6767
private void setDebugMeta(final @NotNull SentryBaseEvent event) {
68+
final @NotNull List<DebugImage> debugImages = new ArrayList<>();
69+
6870
if (options.getProguardUuid() != null) {
71+
final DebugImage proguardMappingImage = new DebugImage();
72+
proguardMappingImage.setType(DebugImage.PROGUARD);
73+
proguardMappingImage.setUuid(options.getProguardUuid());
74+
debugImages.add(proguardMappingImage);
75+
}
76+
77+
for (final @NotNull String bundleId : options.getBundleIds()) {
78+
final DebugImage sourceBundleImage = new DebugImage();
79+
sourceBundleImage.setType(DebugImage.JVM);
80+
sourceBundleImage.setDebugId(bundleId);
81+
debugImages.add(sourceBundleImage);
82+
}
83+
84+
if (!debugImages.isEmpty()) {
6985
DebugMeta debugMeta = event.getDebugMeta();
7086

7187
if (debugMeta == null) {
7288
debugMeta = new DebugMeta();
7389
}
7490
if (debugMeta.getImages() == null) {
75-
debugMeta.setImages(new ArrayList<>());
76-
}
77-
List<DebugImage> images = debugMeta.getImages();
78-
if (images != null) {
79-
final DebugImage debugImage = new DebugImage();
80-
debugImage.setType(DebugImage.PROGUARD);
81-
debugImage.setUuid(options.getProguardUuid());
82-
images.add(debugImage);
83-
event.setDebugMeta(debugMeta);
91+
debugMeta.setImages(debugImages);
92+
} else {
93+
debugMeta.getImages().addAll(debugImages);
8494
}
95+
96+
event.setDebugMeta(debugMeta);
8597
}
8698
}
8799

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public class SentryOptions {
6666
*/
6767
private final @NotNull List<Integration> integrations = new CopyOnWriteArrayList<>();
6868

69+
/** List of bundle IDs representing source bundles. */
70+
private final @NotNull Set<String> bundleIds = new CopyOnWriteArraySet<>();
71+
6972
/**
7073
* The DSN tells the SDK where to send the events to. If this value is not provided, the SDK will
7174
* just not send any events.
@@ -1797,6 +1800,31 @@ public void setProguardUuid(final @Nullable String proguardUuid) {
17971800
this.proguardUuid = proguardUuid;
17981801
}
17991802

1803+
/**
1804+
* Adds a bundle ID (also known as debugId) representing a source bundle that contains sources for
1805+
* this application. These sources will be used to source code for frames of an exceptions stack
1806+
* trace.
1807+
*
1808+
* @param bundleId Bundle ID generated by sentry-cli or the sentry-android-gradle-plugin
1809+
*/
1810+
public void addBundleId(final @Nullable String bundleId) {
1811+
if (bundleId != null) {
1812+
final @NotNull String trimmedBundleId = bundleId.trim();
1813+
if (!trimmedBundleId.isEmpty()) {
1814+
this.bundleIds.add(trimmedBundleId);
1815+
}
1816+
}
1817+
}
1818+
1819+
/**
1820+
* Returns all configured bundle IDs referencing source code bundles.
1821+
*
1822+
* @return list of bundle IDs
1823+
*/
1824+
public @NotNull Set<String> getBundleIds() {
1825+
return bundleIds;
1826+
}
1827+
18001828
/**
18011829
* Returns Context tags names applied to Sentry events as Sentry tags.
18021830
*
@@ -2250,6 +2278,9 @@ public void merge(final @NotNull ExternalOptions options) {
22502278
if (options.getIdleTimeout() != null) {
22512279
setIdleTimeout(options.getIdleTimeout());
22522280
}
2281+
for (String bundleId : options.getBundleIds()) {
2282+
addBundleId(bundleId);
2283+
}
22532284
}
22542285

22552286
private @NotNull SdkVersion createSdkVersion() {

sentry/src/main/java/io/sentry/protocol/DebugImage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
*/
5151
public final class DebugImage implements JsonUnknown, JsonSerializable {
5252
public static final String PROGUARD = "proguard";
53+
public static final String JVM = "jvm";
5354

5455
/**
5556
* The unique UUID of the image.

sentry/src/test/java/io/sentry/ExternalOptionsTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,37 @@ class ExternalOptionsTest {
216216
}
217217
}
218218

219+
@Test
220+
fun `creates options with single bundle ID using external properties`() {
221+
withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411") { options ->
222+
assertTrue(options.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411")))
223+
}
224+
}
225+
226+
@Test
227+
fun `creates options with multiple bundle IDs using external properties`() {
228+
withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411,faa3ab42-b1bd-4659-af8e-1682324aa744") { options ->
229+
assertTrue(options.bundleIds.containsAll(listOf("12ea7a02-46ac-44c0-a5bb-6d1fd9586411", "faa3ab42-b1bd-4659-af8e-1682324aa744")))
230+
}
231+
}
232+
233+
@Test
234+
fun `creates options with empty bundle IDs using external properties`() {
235+
withPropertiesFile("bundle-ids=") { options ->
236+
assertTrue(options.bundleIds.size == 1)
237+
// trimming is tested in SentryOptionsTest so even though there's an empty string here
238+
// it will be filtered when being merged with SentryOptions
239+
assertTrue(options.bundleIds.containsAll(listOf("")))
240+
}
241+
}
242+
243+
@Test
244+
fun `creates options with missing bundle IDs using external properties`() {
245+
withPropertiesFile("") { options ->
246+
assertTrue(options.bundleIds.isEmpty())
247+
}
248+
}
249+
219250
private fun withPropertiesFile(textLines: List<String> = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) {
220251
// create a sentry.properties file in temporary folder
221252
val temporaryFolder = TemporaryFolder()

0 commit comments

Comments
 (0)