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

Populate event.modules with dependencies metadata #2324

Merged
merged 13 commits into from
Nov 4, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

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

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.modules.AssetsModulesLoader;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.util.Objects;
Expand Down Expand Up @@ -155,6 +156,7 @@ static void init(
options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
options.setTransactionProfiler(
new AndroidTransactionProfiler(context, options, buildInfoProvider));
options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
}

private static void installDefaultIntegrations(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.sentry.android.core.internal.modules;

import android.content.Context;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.internal.modules.ModulesLoader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.TreeMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public final class AssetsModulesLoader extends ModulesLoader {
romtsn marked this conversation as resolved.
Show resolved Hide resolved

private final @NotNull Context context;

public AssetsModulesLoader(final @NotNull Context context, final @NotNull ILogger logger) {
super(logger);
this.context = context;
}

@Override
protected Map<String, String> loadModules() {
final Map<String, String> modules = new TreeMap<>();

try {
final InputStream stream = context.getAssets().open(EXTERNAL_MODULES_FILENAME);
return parseStream(stream);
} catch (FileNotFoundException e) {
logger.log(SentryLevel.INFO, "%s file was not found.", EXTERNAL_MODULES_FILENAME);
} catch (IOException e) {
logger.log(SentryLevel.ERROR, "Error extracting modules.", e);
}
return modules;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.sentry.ILogger
import io.sentry.MainEventProcessor
import io.sentry.SentryOptions
import io.sentry.android.core.cache.AndroidEnvelopeCache
import io.sentry.android.core.internal.modules.AssetsModulesLoader
import io.sentry.android.fragment.FragmentLifecycleIntegration
import io.sentry.android.timber.SentryTimberIntegration
import org.junit.runner.RunWith
Expand Down Expand Up @@ -414,4 +415,11 @@ class AndroidOptionsInitializerTest {
(activityLifeCycleIntegration as ActivityLifecycleIntegration).activityFramesTracker.isFrameMetricsAggregatorAvailable
)
}

@Test
fun `AssetsModulesLoader is set to options`() {
fixture.initSut()

assertTrue { fixture.sentryOptions.modulesLoader is AssetsModulesLoader }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.sentry.android.core.internal.modules

import android.content.Context
import android.content.res.AssetManager
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.sentry.ILogger
import java.io.FileNotFoundException
import java.nio.charset.Charset
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class AssetsModulesLoaderTest {

class Fixture {
val context = mock<Context>()
val assets = mock<AssetManager>()
val logger = mock<ILogger>()

fun getSut(
fileName: String = "sentry-external-modules.txt",
content: String? = null,
throws: Boolean = false
): AssetsModulesLoader {
if (content != null) {
whenever(assets.open(fileName)).thenReturn(
content.byteInputStream(Charset.defaultCharset())
)
}
if (throws) {
whenever(assets.open(fileName)).thenThrow(FileNotFoundException())
}
whenever(context.assets).thenReturn(assets)
return AssetsModulesLoader(context, logger)
}
}

private val fixture = Fixture()

@Test
fun `reads modules from assets into map`() {
val sut = fixture.getSut(
content =
"""
com.squareup.okhttp3:okhttp:3.14.9
com.squareup.okio:okio:1.17.2
""".trimIndent()
)

assertEquals(
mapOf(
"com.squareup.okhttp3:okhttp" to "3.14.9",
"com.squareup.okio:okio" to "1.17.2"
),
sut.orLoadModules
)
}

@Test
fun `caches modules after first read`() {
val sut = fixture.getSut(
content =
"""
com.squareup.okhttp3:okhttp:3.14.9
com.squareup.okio:okio:1.17.2
""".trimIndent()
)

// first, call method to get modules cached
sut.orLoadModules

// then call it second time
assertEquals(
mapOf(
"com.squareup.okhttp3:okhttp" to "3.14.9",
"com.squareup.okio:okio" to "1.17.2"
),
sut.orLoadModules
)
// the context only called once when there's no in-memory cache
verify(fixture.context).assets
}

@Test
fun `when file does not exist, swallows exception and returns empty map`() {
val sut = fixture.getSut(throws = true)

assertTrue(sut.orLoadModules!!.isEmpty())
}

@Test
fun `when content is malformed, swallows exception and returns empty map`() {
val sut = fixture.getSut(
content =
"""
com.squareup.okhttp3;3.14.9
""".trimIndent()
)

assertTrue(sut.orLoadModules!!.isEmpty())
}
}
24 changes: 24 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,7 @@ public class io/sentry/SentryOptions {
public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize;
public fun getMaxSpans ()I
public fun getMaxTraceFileSize ()J
public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader;
public fun getOutboxPath ()Ljava/lang/String;
public fun getProfilesSampleRate ()Ljava/lang/Double;
public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback;
Expand Down Expand Up @@ -1457,6 +1458,7 @@ public class io/sentry/SentryOptions {
public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V
public fun setMaxSpans (I)V
public fun setMaxTraceFileSize (J)V
public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V
public fun setPrintUncaughtStackTrace (Z)V
public fun setProfilesSampleRate (Ljava/lang/Double;)V
public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V
Expand Down Expand Up @@ -2190,6 +2192,28 @@ public final class io/sentry/instrumentation/file/SentryFileWriter : java/io/Out
public fun <init> (Ljava/lang/String;Z)V
}

public abstract interface class io/sentry/internal/modules/IModulesLoader {
public abstract fun getOrLoadModules ()Ljava/util/Map;
}

public abstract class io/sentry/internal/modules/ModulesLoader : io/sentry/internal/modules/IModulesLoader {
public static final field EXTERNAL_MODULES_FILENAME Ljava/lang/String;
protected final field logger Lio/sentry/ILogger;
public fun <init> (Lio/sentry/ILogger;)V
public fun getOrLoadModules ()Ljava/util/Map;
protected abstract fun loadModules ()Ljava/util/Map;
protected fun parseStream (Ljava/io/InputStream;)Ljava/util/Map;
}

public final class io/sentry/internal/modules/NoOpModulesLoader : io/sentry/internal/modules/IModulesLoader {
public static fun getInstance ()Lio/sentry/internal/modules/NoOpModulesLoader;
public fun getOrLoadModules ()Ljava/util/Map;
}

public final class io/sentry/internal/modules/ResourcesModulesLoader : io/sentry/internal/modules/ModulesLoader {
public fun <init> (Lio/sentry/ILogger;)V
}

public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public static final field TYPE Ljava/lang/String;
public fun <init> ()V
Expand Down
15 changes: 15 additions & 0 deletions sentry/src/main/java/io/sentry/MainEventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public MainEventProcessor(final @NotNull SentryOptions options) {
setCommons(event);
setExceptions(event);
setDebugMeta(event);
setModules(event);

if (shouldApplyScopeData(event, hint)) {
processNonCachedEvent(event);
Expand Down Expand Up @@ -90,6 +91,20 @@ private void setDebugMeta(final @NotNull SentryEvent event) {
}
}

private void setModules(final @NotNull SentryEvent event) {
final Map<String, String> modules = options.getModulesLoader().getOrLoadModules();
if (modules == null) {
return;
}

final Map<String, String> eventModules = event.getModules();
if (eventModules == null) {
event.setModules(modules);
} else {
eventModules.putAll(modules);
}
}

private boolean shouldApplyScopeData(
final @NotNull SentryBaseEvent event, final @NotNull Hint hint) {
if (HintUtils.shouldApplyScopeData(hint)) {
Expand Down
9 changes: 9 additions & 0 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import io.sentry.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.config.PropertiesProviderFactory;
import io.sentry.internal.modules.IModulesLoader;
import io.sentry.internal.modules.NoOpModulesLoader;
import io.sentry.internal.modules.ResourcesModulesLoader;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.transport.NoOpEnvelopeCache;
Expand Down Expand Up @@ -270,6 +273,12 @@ private static boolean initConfigurations(final @NotNull SentryOptions options)
});
}

final IModulesLoader modulesLoader = options.getModulesLoader();
// only override the ModulesLoader if it's not already set by Android
if (modulesLoader instanceof NoOpModulesLoader) {
options.setModulesLoader(new ResourcesModulesLoader(options.getLogger()));
}

return true;
}

Expand Down
25 changes: 23 additions & 2 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import io.sentry.clientreport.ClientReportRecorder;
import io.sentry.clientreport.IClientReportRecorder;
import io.sentry.clientreport.NoOpClientReportRecorder;
import io.sentry.internal.modules.IModulesLoader;
import io.sentry.internal.modules.NoOpModulesLoader;
import io.sentry.protocol.SdkVersion;
import io.sentry.transport.ITransport;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.transport.NoOpTransportGate;
Expand Down Expand Up @@ -181,8 +184,8 @@ public class SentryOptions {
private final @NotNull List<String> inAppIncludes = new CopyOnWriteArrayList<>();

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

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

/** Modules (dependencies, packages) that will be send along with each event. */
private @NotNull IModulesLoader modulesLoader = NoOpModulesLoader.getInstance();

/**
* Adds an event processor
*
Expand Down Expand Up @@ -1745,6 +1751,21 @@ public void setSendClientReports(boolean sendClientReports) {
return clientReportRecorder;
}

/**
* Returns a ModulesLoader to load external modules (dependencies/packages) of the program.
*
* @return a modules loader or no-op
*/
@ApiStatus.Internal
public @NotNull IModulesLoader getModulesLoader() {
return modulesLoader;
}

@ApiStatus.Internal
public void setModulesLoader(final @Nullable IModulesLoader modulesLoader) {
this.modulesLoader = modulesLoader != null ? modulesLoader : NoOpModulesLoader.getInstance();
}

/** The BeforeSend callback */
public interface BeforeSendCallback {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.sentry.internal.modules;

import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public interface IModulesLoader {
@Nullable
Map<String, String> getOrLoadModules();
}
Loading