Skip to content

Commit 2826e76

Browse files
authored
Populate event.modules with dependencies metadata (#2324)
1 parent 34811f4 commit 2826e76

File tree

16 files changed

+535
-9
lines changed

16 files changed

+535
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
- Customizable fragment lifecycle breadcrumbs ([#2299](https://github.com/getsentry/sentry-java/pull/2299))
1313
- Provide hook for Jetpack Compose navigation instrumentation ([#2320](https://github.com/getsentry/sentry-java/pull/2320))
14+
- Populate `event.modules` with dependencies metadata ([#2324](https://github.com/getsentry/sentry-java/pull/2324))
1415

1516
### Dependencies
1617

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.sentry.SendFireAndForgetOutboxSender;
1313
import io.sentry.SentryLevel;
1414
import io.sentry.android.core.cache.AndroidEnvelopeCache;
15+
import io.sentry.android.core.internal.modules.AssetsModulesLoader;
1516
import io.sentry.android.fragment.FragmentLifecycleIntegration;
1617
import io.sentry.android.timber.SentryTimberIntegration;
1718
import io.sentry.util.Objects;
@@ -155,6 +156,7 @@ static void init(
155156
options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
156157
options.setTransactionProfiler(
157158
new AndroidTransactionProfiler(context, options, buildInfoProvider));
159+
options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
158160
}
159161

160162
private static void installDefaultIntegrations(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.sentry.android.core.internal.modules;
2+
3+
import android.content.Context;
4+
import io.sentry.ILogger;
5+
import io.sentry.SentryLevel;
6+
import io.sentry.internal.modules.ModulesLoader;
7+
import java.io.FileNotFoundException;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.util.Map;
11+
import java.util.TreeMap;
12+
import org.jetbrains.annotations.ApiStatus;
13+
import org.jetbrains.annotations.NotNull;
14+
15+
@ApiStatus.Internal
16+
public final class AssetsModulesLoader extends ModulesLoader {
17+
18+
private final @NotNull Context context;
19+
20+
public AssetsModulesLoader(final @NotNull Context context, final @NotNull ILogger logger) {
21+
super(logger);
22+
this.context = context;
23+
}
24+
25+
@Override
26+
protected Map<String, String> loadModules() {
27+
final Map<String, String> modules = new TreeMap<>();
28+
29+
try {
30+
final InputStream stream = context.getAssets().open(EXTERNAL_MODULES_FILENAME);
31+
return parseStream(stream);
32+
} catch (FileNotFoundException e) {
33+
logger.log(SentryLevel.INFO, "%s file was not found.", EXTERNAL_MODULES_FILENAME);
34+
} catch (IOException e) {
35+
logger.log(SentryLevel.ERROR, "Error extracting modules.", e);
36+
}
37+
return modules;
38+
}
39+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.sentry.ILogger
1111
import io.sentry.MainEventProcessor
1212
import io.sentry.SentryOptions
1313
import io.sentry.android.core.cache.AndroidEnvelopeCache
14+
import io.sentry.android.core.internal.modules.AssetsModulesLoader
1415
import io.sentry.android.fragment.FragmentLifecycleIntegration
1516
import io.sentry.android.timber.SentryTimberIntegration
1617
import org.junit.runner.RunWith
@@ -414,4 +415,11 @@ class AndroidOptionsInitializerTest {
414415
(activityLifeCycleIntegration as ActivityLifecycleIntegration).activityFramesTracker.isFrameMetricsAggregatorAvailable
415416
)
416417
}
418+
419+
@Test
420+
fun `AssetsModulesLoader is set to options`() {
421+
fixture.initSut()
422+
423+
assertTrue { fixture.sentryOptions.modulesLoader is AssetsModulesLoader }
424+
}
417425
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package io.sentry.android.core.internal.modules
2+
3+
import android.content.Context
4+
import android.content.res.AssetManager
5+
import com.nhaarman.mockitokotlin2.mock
6+
import com.nhaarman.mockitokotlin2.verify
7+
import com.nhaarman.mockitokotlin2.whenever
8+
import io.sentry.ILogger
9+
import java.io.FileNotFoundException
10+
import java.nio.charset.Charset
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
import kotlin.test.assertTrue
14+
15+
class AssetsModulesLoaderTest {
16+
17+
class Fixture {
18+
val context = mock<Context>()
19+
val assets = mock<AssetManager>()
20+
val logger = mock<ILogger>()
21+
22+
fun getSut(
23+
fileName: String = "sentry-external-modules.txt",
24+
content: String? = null,
25+
throws: Boolean = false
26+
): AssetsModulesLoader {
27+
if (content != null) {
28+
whenever(assets.open(fileName)).thenReturn(
29+
content.byteInputStream(Charset.defaultCharset())
30+
)
31+
}
32+
if (throws) {
33+
whenever(assets.open(fileName)).thenThrow(FileNotFoundException())
34+
}
35+
whenever(context.assets).thenReturn(assets)
36+
return AssetsModulesLoader(context, logger)
37+
}
38+
}
39+
40+
private val fixture = Fixture()
41+
42+
@Test
43+
fun `reads modules from assets into map`() {
44+
val sut = fixture.getSut(
45+
content =
46+
"""
47+
com.squareup.okhttp3:okhttp:3.14.9
48+
com.squareup.okio:okio:1.17.2
49+
""".trimIndent()
50+
)
51+
52+
assertEquals(
53+
mapOf(
54+
"com.squareup.okhttp3:okhttp" to "3.14.9",
55+
"com.squareup.okio:okio" to "1.17.2"
56+
),
57+
sut.orLoadModules
58+
)
59+
}
60+
61+
@Test
62+
fun `caches modules after first read`() {
63+
val sut = fixture.getSut(
64+
content =
65+
"""
66+
com.squareup.okhttp3:okhttp:3.14.9
67+
com.squareup.okio:okio:1.17.2
68+
""".trimIndent()
69+
)
70+
71+
// first, call method to get modules cached
72+
sut.orLoadModules
73+
74+
// then call it second time
75+
assertEquals(
76+
mapOf(
77+
"com.squareup.okhttp3:okhttp" to "3.14.9",
78+
"com.squareup.okio:okio" to "1.17.2"
79+
),
80+
sut.orLoadModules
81+
)
82+
// the context only called once when there's no in-memory cache
83+
verify(fixture.context).assets
84+
}
85+
86+
@Test
87+
fun `when file does not exist, swallows exception and returns empty map`() {
88+
val sut = fixture.getSut(throws = true)
89+
90+
assertTrue(sut.orLoadModules!!.isEmpty())
91+
}
92+
93+
@Test
94+
fun `when content is malformed, swallows exception and returns empty map`() {
95+
val sut = fixture.getSut(
96+
content =
97+
"""
98+
com.squareup.okhttp3;3.14.9
99+
""".trimIndent()
100+
)
101+
102+
assertTrue(sut.orLoadModules!!.isEmpty())
103+
}
104+
}

sentry/api/sentry.api

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,7 @@ public class io/sentry/SentryOptions {
13791379
public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize;
13801380
public fun getMaxSpans ()I
13811381
public fun getMaxTraceFileSize ()J
1382+
public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader;
13821383
public fun getOutboxPath ()Ljava/lang/String;
13831384
public fun getProfilesSampleRate ()Ljava/lang/Double;
13841385
public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback;
@@ -1457,6 +1458,7 @@ public class io/sentry/SentryOptions {
14571458
public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V
14581459
public fun setMaxSpans (I)V
14591460
public fun setMaxTraceFileSize (J)V
1461+
public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V
14601462
public fun setPrintUncaughtStackTrace (Z)V
14611463
public fun setProfilesSampleRate (Ljava/lang/Double;)V
14621464
public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V
@@ -2190,6 +2192,28 @@ public final class io/sentry/instrumentation/file/SentryFileWriter : java/io/Out
21902192
public fun <init> (Ljava/lang/String;Z)V
21912193
}
21922194

2195+
public abstract interface class io/sentry/internal/modules/IModulesLoader {
2196+
public abstract fun getOrLoadModules ()Ljava/util/Map;
2197+
}
2198+
2199+
public abstract class io/sentry/internal/modules/ModulesLoader : io/sentry/internal/modules/IModulesLoader {
2200+
public static final field EXTERNAL_MODULES_FILENAME Ljava/lang/String;
2201+
protected final field logger Lio/sentry/ILogger;
2202+
public fun <init> (Lio/sentry/ILogger;)V
2203+
public fun getOrLoadModules ()Ljava/util/Map;
2204+
protected abstract fun loadModules ()Ljava/util/Map;
2205+
protected fun parseStream (Ljava/io/InputStream;)Ljava/util/Map;
2206+
}
2207+
2208+
public final class io/sentry/internal/modules/NoOpModulesLoader : io/sentry/internal/modules/IModulesLoader {
2209+
public static fun getInstance ()Lio/sentry/internal/modules/NoOpModulesLoader;
2210+
public fun getOrLoadModules ()Ljava/util/Map;
2211+
}
2212+
2213+
public final class io/sentry/internal/modules/ResourcesModulesLoader : io/sentry/internal/modules/ModulesLoader {
2214+
public fun <init> (Lio/sentry/ILogger;)V
2215+
}
2216+
21932217
public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
21942218
public static final field TYPE Ljava/lang/String;
21952219
public fun <init> ()V

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public MainEventProcessor(final @NotNull SentryOptions options) {
6060
setCommons(event);
6161
setExceptions(event);
6262
setDebugMeta(event);
63+
setModules(event);
6364

6465
if (shouldApplyScopeData(event, hint)) {
6566
processNonCachedEvent(event);
@@ -90,6 +91,20 @@ private void setDebugMeta(final @NotNull SentryEvent event) {
9091
}
9192
}
9293

94+
private void setModules(final @NotNull SentryEvent event) {
95+
final Map<String, String> modules = options.getModulesLoader().getOrLoadModules();
96+
if (modules == null) {
97+
return;
98+
}
99+
100+
final Map<String, String> eventModules = event.getModules();
101+
if (eventModules == null) {
102+
event.setModules(modules);
103+
} else {
104+
eventModules.putAll(modules);
105+
}
106+
}
107+
93108
private boolean shouldApplyScopeData(
94109
final @NotNull SentryBaseEvent event, final @NotNull Hint hint) {
95110
if (HintUtils.shouldApplyScopeData(hint)) {

sentry/src/main/java/io/sentry/Sentry.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import io.sentry.cache.EnvelopeCache;
44
import io.sentry.cache.IEnvelopeCache;
55
import io.sentry.config.PropertiesProviderFactory;
6+
import io.sentry.internal.modules.IModulesLoader;
7+
import io.sentry.internal.modules.NoOpModulesLoader;
8+
import io.sentry.internal.modules.ResourcesModulesLoader;
69
import io.sentry.protocol.SentryId;
710
import io.sentry.protocol.User;
811
import io.sentry.transport.NoOpEnvelopeCache;
@@ -270,6 +273,12 @@ private static boolean initConfigurations(final @NotNull SentryOptions options)
270273
});
271274
}
272275

276+
final IModulesLoader modulesLoader = options.getModulesLoader();
277+
// only override the ModulesLoader if it's not already set by Android
278+
if (modulesLoader instanceof NoOpModulesLoader) {
279+
options.setModulesLoader(new ResourcesModulesLoader(options.getLogger()));
280+
}
281+
273282
return true;
274283
}
275284

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
import io.sentry.clientreport.ClientReportRecorder;
66
import io.sentry.clientreport.IClientReportRecorder;
77
import io.sentry.clientreport.NoOpClientReportRecorder;
8+
import io.sentry.internal.modules.IModulesLoader;
9+
import io.sentry.internal.modules.NoOpModulesLoader;
810
import io.sentry.protocol.SdkVersion;
11+
import io.sentry.transport.ITransport;
912
import io.sentry.transport.ITransportGate;
1013
import io.sentry.transport.NoOpEnvelopeCache;
1114
import io.sentry.transport.NoOpTransportGate;
@@ -181,8 +184,8 @@ public class SentryOptions {
181184
private final @NotNull List<String> inAppIncludes = new CopyOnWriteArrayList<>();
182185

183186
/**
184-
* The transport factory creates instances of {@link io.sentry.transport.ITransport} - internal
185-
* construct of the client that abstracts away the event sending.
187+
* The transport factory creates instances of {@link ITransport} - internal construct of the
188+
* client that abstracts away the event sending.
186189
*/
187190
private @NotNull ITransportFactory transportFactory = NoOpTransportFactory.getInstance();
188191

@@ -355,6 +358,9 @@ public class SentryOptions {
355358
/** ClientReportRecorder to track count of lost events / transactions / ... * */
356359
@NotNull IClientReportRecorder clientReportRecorder = new ClientReportRecorder(this);
357360

361+
/** Modules (dependencies, packages) that will be send along with each event. */
362+
private @NotNull IModulesLoader modulesLoader = NoOpModulesLoader.getInstance();
363+
358364
/**
359365
* Adds an event processor
360366
*
@@ -1745,6 +1751,21 @@ public void setSendClientReports(boolean sendClientReports) {
17451751
return clientReportRecorder;
17461752
}
17471753

1754+
/**
1755+
* Returns a ModulesLoader to load external modules (dependencies/packages) of the program.
1756+
*
1757+
* @return a modules loader or no-op
1758+
*/
1759+
@ApiStatus.Internal
1760+
public @NotNull IModulesLoader getModulesLoader() {
1761+
return modulesLoader;
1762+
}
1763+
1764+
@ApiStatus.Internal
1765+
public void setModulesLoader(final @Nullable IModulesLoader modulesLoader) {
1766+
this.modulesLoader = modulesLoader != null ? modulesLoader : NoOpModulesLoader.getInstance();
1767+
}
1768+
17481769
/** The BeforeSend callback */
17491770
public interface BeforeSendCallback {
17501771

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.sentry.internal.modules;
2+
3+
import java.util.Map;
4+
import org.jetbrains.annotations.ApiStatus;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
@ApiStatus.Internal
8+
public interface IModulesLoader {
9+
@Nullable
10+
Map<String, String> getOrLoadModules();
11+
}

0 commit comments

Comments
 (0)