From ed4b35d6037da17510df249d4099d05afc75cedf Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 13 Aug 2026 12:47:22 +0200 Subject: [PATCH 1/2] perf(android): Use build-time manifest metadata Allow the Android Gradle plugin to provide authoritative manifest metadata so SDK initialization can skip PackageManager and Bundle unparceling. Read the injected map directly to avoid conversion overhead. Refs JAVA-531 Co-Authored-By: Codex --- .../android/core/ManifestMetadataReader.java | 99 ++++++++++++++----- .../core/ManifestMetadataReaderTest.kt | 49 +++++++++ 2 files changed, 126 insertions(+), 22 deletions(-) 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 f21d4c801a3..7ce083df5b4 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; @@ -25,6 +26,10 @@ /** Class responsible for reading values from manifest and setting them to the options */ final class ManifestMetadataReader { + // Populated by the Sentry Android Gradle plugin with manifest metadata resolved at build time. + // When set, it is authoritative: the manifest is not read at runtime. + static @Nullable Map buildTimeMetadata; + static final String DSN = "io.sentry.dsn"; static final String DEBUG = "io.sentry.debug"; static final String DEBUG_LEVEL = "io.sentry.debug.level"; @@ -217,7 +222,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 +496,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 +784,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 +802,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 +820,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 +838,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 +857,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 +880,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 +910,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 +921,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 = buildTimeMetadata; + 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/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index d67a869eff0..159714cb778 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`() { + ManifestMetadataReader.buildTimeMetadata = null + } + + @Test + fun `applyMetadata reads typed build-time metadata without querying context`() { + val context = mock() + ManifestMetadataReader.buildTimeMetadata = + 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" + ManifestMetadataReader.buildTimeMetadata = 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() + ManifestMetadataReader.buildTimeMetadata = 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) From a1698abeb996d9d7f0b210c50dc56e8842edca99 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 13 Aug 2026 13:45:26 +0200 Subject: [PATCH 2/2] docs(changelog): Add build-time metadata optimization Refs JAVA-531 Co-Authored-By: Codex --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 247a5e96d31..3129aa336ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Performance + +- Use Android manifest metadata resolved by the Sentry Android Gradle plugin at build time, avoiding `PackageManager` and `Bundle` parsing and reducing median SDK initialization time by 6.5% in a cold-start benchmark ([#5963](https://github.com/getsentry/sentry-java/pull/5963)) + ## 8.53.0 ### Features