Skip to content

Commit

Permalink
Populate event.modules with dependencies metadata (#2324)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Nov 4, 2022
1 parent 34811f4 commit 2826e76
Show file tree
Hide file tree
Showing 16 changed files with 535 additions and 9 deletions.
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 {

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

0 comments on commit 2826e76

Please sign in to comment.