From a74ff1b65b9b6fa77eebe76d7971639853b78a81 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 13 Aug 2026 12:47:22 +0200 Subject: [PATCH 1/3] 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 41a5f4ef507bac50b91af1007ac8b02b1e5babd9 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 14 Aug 2026 12:57:36 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83df10a9871..7af095cfd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 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)) + ## 8.53.0 ### Features From 889ddc43c3925ee98a079e2de4393629d1b2b506 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 14 Aug 2026 20:36:20 +0200 Subject: [PATCH 3/3] ref(android): Add build-time metadata holder Keep the Gradle plugin bytecode contract separate from manifest reader implementation details. Co-Authored-By: OpenAI Codex --- .../sentry/android/core/ManifestMetadataReader.java | 6 +----- .../sentry/android/core/SentryBuildTimeMetadata.java | 12 ++++++++++++ .../android/core/ManifestMetadataReaderTest.kt | 8 ++++---- 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/SentryBuildTimeMetadata.java 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 7ce083df5b4..d53b9abc3a7 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 @@ -26,10 +26,6 @@ /** 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"; @@ -930,7 +926,7 @@ static boolean isAutoInit(final @NotNull Context context, final @NotNull ILogger final @NotNull Context context, final @NotNull ILogger logger, final @Nullable BuildInfoProvider buildInfoProvider) { - final @Nullable Map injected = buildTimeMetadata; + final @Nullable Map injected = SentryBuildTimeMetadata.manifestMetadata; if (injected != null) { return injected; } 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 00000000000..ba745d9ce56 --- /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 159714cb778..76c813f92ca 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 @@ -47,13 +47,13 @@ class ManifestMetadataReaderTest { @AfterTest fun `tear down`() { - ManifestMetadataReader.buildTimeMetadata = null + SentryBuildTimeMetadata.manifestMetadata = null } @Test fun `applyMetadata reads typed build-time metadata without querying context`() { val context = mock() - ManifestMetadataReader.buildTimeMetadata = + SentryBuildTimeMetadata.manifestMetadata = mapOf( ManifestMetadataReader.DEBUG to true, ManifestMetadataReader.DIST to "dist", @@ -74,7 +74,7 @@ class ManifestMetadataReaderTest { fun `build-time metadata is authoritative when a key is absent`() { val context = mock() fixture.options.dist = "configured" - ManifestMetadataReader.buildTimeMetadata = emptyMap() + SentryBuildTimeMetadata.manifestMetadata = emptyMap() ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) @@ -85,7 +85,7 @@ class ManifestMetadataReaderTest { @Test fun `isAutoInit reads build-time metadata without querying context`() { val context = mock() - ManifestMetadataReader.buildTimeMetadata = mapOf(ManifestMetadataReader.AUTO_INIT to false) + SentryBuildTimeMetadata.manifestMetadata = mapOf(ManifestMetadataReader.AUTO_INIT to false) assertThat(ManifestMetadataReader.isAutoInit(context, fixture.logger)).isFalse() verifyNoInteractions(context)