diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a45ec3fe3..8948599b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Performance + +- Use manifest metadata resolved at build time, reducing Android SDK initialization time by 6.5% ([#5969](https://github.com/getsentry/sentry-java/pull/5969)) + ### Dependencies - Bump Native SDK from v0.16.2 to v0.16.3 ([#5962](https://github.com/getsentry/sentry-java/pull/5962)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index f21d4c801a..d53b9abc3a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -217,7 +218,7 @@ static void applyMetadata( Objects.requireNonNull(options, "The options object is required."); try { - final Bundle metadata = getMetadata(context, options.getLogger(), buildInfoProvider); + final Object metadata = getMetadata(context, options.getLogger(), buildInfoProvider); final ILogger logger = options.getLogger(); if (metadata != null) { @@ -491,7 +492,7 @@ static void applyMetadata( List tracePropagationTargets = readList(metadata, logger, TRACE_PROPAGATION_TARGETS); - if (metadata.containsKey(TRACE_PROPAGATION_TARGETS) && tracePropagationTargets == null) { + if (containsKey(metadata, TRACE_PROPAGATION_TARGETS) && tracePropagationTargets == null) { options.setTracePropagationTargets(Collections.emptyList()); } else if (tracePropagationTargets != null) { options.setTracePropagationTargets(tracePropagationTargets); @@ -779,11 +780,17 @@ static void applyMetadata( } private static boolean readBool( - final @NotNull Bundle metadata, + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key, final boolean defaultValue) { - final boolean value = metadata.getBoolean(key, defaultValue); + final boolean value; + if (metadata instanceof Bundle) { + value = ((Bundle) metadata).getBoolean(key, defaultValue); + } else { + final Object raw = ((Map) metadata).get(key); + value = raw instanceof Boolean ? (Boolean) raw : defaultValue; + } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); } @@ -791,11 +798,17 @@ private static boolean readBool( } private static @Nullable String readString( - final @NotNull Bundle metadata, + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key, final @Nullable String defaultValue) { - final String value = metadata.getString(key, defaultValue); + final String value; + if (metadata instanceof Bundle) { + value = ((Bundle) metadata).getString(key, defaultValue); + } else { + final Object raw = ((Map) metadata).get(key); + value = raw instanceof String ? (String) raw : defaultValue; + } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); } @@ -803,11 +816,17 @@ private static boolean readBool( } private static @NotNull String readStringNotNull( - final @NotNull Bundle metadata, + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key, final @NotNull String defaultValue) { - final String value = metadata.getString(key, defaultValue); + final String value; + if (metadata instanceof Bundle) { + value = ((Bundle) metadata).getString(key, defaultValue); + } else { + final Object raw = ((Map) metadata).get(key); + value = raw instanceof String ? (String) raw : defaultValue; + } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); } @@ -815,8 +834,14 @@ private static boolean readBool( } private static @Nullable List readList( - final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { - final String value = metadata.getString(key); + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key) { + final String value; + if (metadata instanceof Bundle) { + value = ((Bundle) metadata).getString(key); + } else { + final Object raw = ((Map) metadata).get(key); + value = raw instanceof String ? (String) raw : null; + } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); } @@ -828,11 +853,21 @@ private static boolean readBool( } private static double readDouble( - final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { - // manifest meta-data only reads float - double value = ((Float) metadata.getFloat(key, -1)).doubleValue(); - if (value == -1) { - value = ((Integer) metadata.getInt(key, -1)).doubleValue(); + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key) { + final double value; + if (metadata instanceof Bundle) { + // manifest meta-data only reads float + double bundleValue = ((Float) ((Bundle) metadata).getFloat(key, -1)).doubleValue(); + if (bundleValue == -1) { + bundleValue = ((Integer) ((Bundle) metadata).getInt(key, -1)).doubleValue(); + } + value = bundleValue; + } else { + final Object raw = ((Map) metadata).get(key); + value = + raw instanceof Float + ? ((Float) raw).doubleValue() + : raw instanceof Integer ? ((Integer) raw).doubleValue() : -1; } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); @@ -841,12 +876,18 @@ private static double readDouble( } private static long readLong( - final @NotNull Bundle metadata, + final @NotNull Object metadata, final @NotNull ILogger logger, final @NotNull String key, final long defaultValue) { - // manifest meta-data only reads int if the value is not big enough - final long value = metadata.getInt(key, (int) defaultValue); + final long value; + if (metadata instanceof Bundle) { + // manifest meta-data only reads int if the value is not big enough + value = ((Bundle) metadata).getInt(key, (int) defaultValue); + } else { + final Object raw = ((Map) metadata).get(key); + value = raw instanceof Integer ? (Integer) raw : defaultValue; + } if (logger.isEnabled(SentryLevel.DEBUG)) { logger.log(SentryLevel.DEBUG, key + " read: " + value); } @@ -865,7 +906,7 @@ static boolean isAutoInit(final @NotNull Context context, final @NotNull ILogger boolean autoInit = true; try { - final Bundle metadata = getMetadata(context, logger, null); + final Object metadata = getMetadata(context, logger, null); if (metadata != null) { autoInit = readBool(metadata, logger, AUTO_INIT, true); } @@ -876,18 +917,28 @@ static boolean isAutoInit(final @NotNull Context context, final @NotNull ILogger } /** - * Returns the Bundle attached from the given Context + * Returns build-time metadata when available, otherwise metadata attached to the given Context. * * @param context the application context - * @return the Bundle attached to the PackageManager + * @return metadata as a Map or PackageManager Bundle */ - private static @Nullable Bundle getMetadata( + private static @Nullable Object getMetadata( final @NotNull Context context, final @NotNull ILogger logger, final @Nullable BuildInfoProvider buildInfoProvider) { + final @Nullable Map injected = SentryBuildTimeMetadata.manifestMetadata; + if (injected != null) { + return injected; + } final ApplicationInfo app = ContextUtils.getApplicationInfo( context, buildInfoProvider != null ? buildInfoProvider : new BuildInfoProvider(logger)); return app != null ? app.metaData : null; } + + private static boolean containsKey(final @NotNull Object metadata, final @NotNull String key) { + return metadata instanceof Bundle + ? ((Bundle) metadata).containsKey(key) + : ((Map) metadata).containsKey(key); + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryBuildTimeMetadata.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryBuildTimeMetadata.java new file mode 100644 index 0000000000..ba745d9ce5 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryBuildTimeMetadata.java @@ -0,0 +1,12 @@ +package io.sentry.android.core; + +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +/** Manifest metadata populated by the Sentry Android Gradle plugin at build time. */ +final class SentryBuildTimeMetadata { + + static @Nullable Map manifestMetadata; + + private SentryBuildTimeMetadata() {} +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index d67a869eff..76c813f92c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -4,12 +4,14 @@ import android.content.Context import android.os.Bundle import androidx.core.os.bundleOf import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import io.sentry.FilterString import io.sentry.ILogger import io.sentry.ProfileLifecycle import io.sentry.SentryLevel import io.sentry.SentryReplayOptions import io.sentry.TransactionOptions +import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -23,6 +25,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions @RunWith(AndroidJUnit4::class) class ManifestMetadataReaderTest { @@ -42,6 +45,52 @@ class ManifestMetadataReaderTest { ContextUtils.resetInstance() } + @AfterTest + fun `tear down`() { + SentryBuildTimeMetadata.manifestMetadata = null + } + + @Test + fun `applyMetadata reads typed build-time metadata without querying context`() { + val context = mock() + SentryBuildTimeMetadata.manifestMetadata = + mapOf( + ManifestMetadataReader.DEBUG to true, + ManifestMetadataReader.DIST to "dist", + ManifestMetadataReader.SAMPLE_RATE to 0.5f, + ManifestMetadataReader.MAX_BREADCRUMBS to 42, + ) + + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + assertThat(fixture.options.isDebug).isTrue() + assertThat(fixture.options.dist).isEqualTo("dist") + assertThat(fixture.options.sampleRate).isEqualTo(0.5) + assertThat(fixture.options.maxBreadcrumbs).isEqualTo(42) + verifyNoInteractions(context) + } + + @Test + fun `build-time metadata is authoritative when a key is absent`() { + val context = mock() + fixture.options.dist = "configured" + SentryBuildTimeMetadata.manifestMetadata = emptyMap() + + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + assertThat(fixture.options.dist).isEqualTo("configured") + verifyNoInteractions(context) + } + + @Test + fun `isAutoInit reads build-time metadata without querying context`() { + val context = mock() + SentryBuildTimeMetadata.manifestMetadata = mapOf(ManifestMetadataReader.AUTO_INIT to false) + + assertThat(ManifestMetadataReader.isAutoInit(context, fixture.logger)).isFalse() + verifyNoInteractions(context) + } + @Test fun `isAutoInit won't throw exception and is enabled by default`() { fixture.options.setDebug(true)