diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index ef8fb70f8..fb628ebba 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -61,12 +61,27 @@ fun ApplicationAndroidComponentsExtension.configure( tmpDir.mkdirs() onVariants { variant -> - if (isVariantAllowed(extension, variant.name, variant.flavorName, variant.buildType)) { - val paths = OutputPaths(project, variant.name) - val sentryTelemetryProvider = + // Calculate feature enablement early + val sizeAnalysisEnabledForVariant = calculateSizeAnalysisEnabled(extension, variant.name) + val variantIsAllowed = + isVariantAllowed(extension, variant.name, variant.flavorName, variant.buildType) + val distributionEnabledForVariant = + variantIsAllowed && extension.distribution.enabledVariants.get().contains(variant.name) + + // Configure telemetry if ANY feature needs it (variant allowed OR size analysis enabled) + val sentryTelemetryProvider = + if (variantIsAllowed || sizeAnalysisEnabledForVariant) { variant.configureTelemetry(project, extension, cliExecutable, sentryOrg, buildEvents) + } else { + null + } + + if (variantIsAllowed) { + val paths = OutputPaths(project, variant.name) + // sentryTelemetryProvider is guaranteed to be non-null here since variantIsAllowed is true + val telemetryProvider = sentryTelemetryProvider!! - variant.configureDependenciesTask(project, extension, sentryTelemetryProvider) + variant.configureDependenciesTask(project, extension, telemetryProvider) // TODO: do this only once, and all other tasks should be SentryVariant.configureSomething val sentryVariant = AndroidVariant74(variant) @@ -84,7 +99,7 @@ fun ApplicationAndroidComponentsExtension.configure( variant.configureSourceBundleTasks( project, extension, - sentryTelemetryProvider, + telemetryProvider, paths, sourceFiles, cliExecutable, @@ -97,7 +112,7 @@ fun ApplicationAndroidComponentsExtension.configure( variant.configureProguardMappingsTasks( project, extension, - sentryTelemetryProvider, + telemetryProvider, paths, cliExecutable, sentryOrg, @@ -109,7 +124,7 @@ fun ApplicationAndroidComponentsExtension.configure( variant.configureDistributionPropertiesTask( project, extension, - sentryTelemetryProvider, + telemetryProvider, paths, sentryOrg, sentryProject, @@ -119,7 +134,7 @@ fun ApplicationAndroidComponentsExtension.configure( sentryVariant.configureNativeSymbolsTask( project, extension, - sentryTelemetryProvider, + telemetryProvider, cliExecutable, sentryOrg, sentryProject, @@ -135,7 +150,7 @@ fun ApplicationAndroidComponentsExtension.configure( InjectSentryMetaPropertiesIntoAssetsTask.register( project, extension, - sentryTelemetryProvider, + telemetryProvider, tasksGeneratingProperties, variant.name.capitalized, ) @@ -216,7 +231,7 @@ fun ApplicationAndroidComponentsExtension.configure( SentryGenerateIntegrationListTask.register( project, extension, - sentryTelemetryProvider, + telemetryProvider, sentryModulesService, variant.name, ) @@ -229,18 +244,18 @@ fun ApplicationAndroidComponentsExtension.configure( ) .toTransform(SingleArtifact.MERGED_MANIFEST) } - val sizeAnalysisEnabled = extension.sizeAnalysis.enabled.get() == true - val distributionEnabled = extension.distribution.enabledVariants.get().contains(variant.name) - if (sizeAnalysisEnabled || distributionEnabled) { - variant.configureUploadAppTasks( - project, - extension, - sentryTelemetryProvider, - cliExecutable, - sentryOrg, - sentryProject, - ) - } + } + + // Configure upload tasks if EITHER feature is enabled for this variant + if (sizeAnalysisEnabledForVariant || distributionEnabledForVariant) { + variant.configureUploadAppTasks( + project, + extension, + sentryTelemetryProvider!!, + cliExecutable, + sentryOrg, + sentryProject, + ) } } } @@ -499,3 +514,29 @@ private fun ApplicationVariant.getReleaseInfo(): ReleaseInfo { } return ReleaseInfo(applicationId, versionName, versionCode) } + +/** + * Calculates whether size analysis is enabled for a specific variant. + * + * Size analysis is enabled if: + * 1. The global enabled flag is true, AND + * 2. Either enabledVariants is empty (meaning all variants), OR the variant is in enabledVariants + * + * Note: This function BYPASSES isVariantAllowed. This means size analysis can run on variants + * that are globally ignored via ignoredVariants, ignoredBuildTypes, or ignoredFlavors. This is + * intentional to provide fine-grained control over which variants have size analysis enabled. + */ +private fun calculateSizeAnalysisEnabled( + extension: SentryPluginExtension, + variantName: String, +): Boolean { + // Master kill-switch check first + if (extension.sizeAnalysis.enabled.get() != true) { + return false + } + + val enabledVariants = extension.sizeAnalysis.enabledVariants.get() + + // Empty set = enable for all variants (backward compatibility) + return enabledVariants.isEmpty() || enabledVariants.contains(variantName) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtension.kt index d7bd6a802..9ed3f65da 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtension.kt @@ -5,6 +5,7 @@ import javax.inject.Inject import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.SetProperty import org.jetbrains.annotations.ApiStatus.Experimental @Experimental @@ -17,6 +18,31 @@ constructor(objects: ObjectFactory, providerFactory: ProviderFactory) { .property(Boolean::class.java) .convention(providerFactory.isCi() && false) // set to false for now otherwise upload fails CI + /** + * Set of Android build variants that should have size analysis enabled. + * + * When empty (default), size analysis runs on all variants (subject to the enabled flag). + * When populated, only the specified variants will have size analysis enabled. + * + * Note: This property BYPASSES the global ignore settings (ignoredVariants, ignoredBuildTypes, + * ignoredFlavors). This allows size analysis to run on specific variants even if they are + * globally ignored. This is different from most other Sentry features which respect the global + * ignore settings. + * + * Example: + * ``` + * sentry { + * ignoredVariants.set(["debug"]) // Most features ignore debug + * sizeAnalysis { + * enabled = true + * enabledVariants.set(["debug", "release"]) // Size analysis runs on both + * } + * } + * ``` + */ + val enabledVariants: SetProperty = + objects.setProperty(String::class.java).convention(emptySet()) + /** * The build configuration to use for the upload. This allows comparison between builds with the * same buildConfiguration. If not provided, the build variant will be used. diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtensionTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtensionTest.kt new file mode 100644 index 000000000..4c5531ffb --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/extensions/SizeAnalysisExtensionTest.kt @@ -0,0 +1,99 @@ +package io.sentry.android.gradle.extensions + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Test + +class SizeAnalysisExtensionTest { + + @Test + fun `enabledVariants is empty by default`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + assertTrue(extension.enabledVariants.get().isEmpty()) + } + + @Test + fun `enabledVariants can be configured with variant names`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + extension.enabledVariants.set(setOf("release", "staging", "debug")) + + assertEquals(setOf("release", "staging", "debug"), extension.enabledVariants.get()) + } + + @Test + fun `enabledVariants can be updated multiple times`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + extension.enabledVariants.set(setOf("release")) + assertEquals(setOf("release"), extension.enabledVariants.get()) + + extension.enabledVariants.set(setOf("debug", "staging")) + assertEquals(setOf("debug", "staging"), extension.enabledVariants.get()) + } + + @Test + fun `enabled is false by default in non-CI environment`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + // In test environment, CI is typically not set + assertFalse(extension.enabled.get()) + } + + @Test + fun `enabled can be configured`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + extension.enabled.set(true) + + assertTrue(extension.enabled.get()) + } + + @Test + fun `buildConfiguration can be configured`() { + val project = ProjectBuilder.builder().build() + val extension = + project.objects.newInstance( + SizeAnalysisExtension::class.java, + project.objects, + project.providers, + ) + + extension.buildConfiguration.set("custom-config") + + assertEquals("custom-config", extension.buildConfiguration.get()) + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginVariantTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginVariantTest.kt index 866f2a4e5..b5dcc5f19 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginVariantTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginVariantTest.kt @@ -83,6 +83,118 @@ class SentryPluginVariantTest : assertTrue(":app:uploadSentryProguardMappingsDemoRelease" in build.output) } + @Test + fun `size analysis bypasses ignoredVariants when enabledVariants is set`() { + appBuildFile.appendText( + // language=Groovy + """ + sentry { + autoUploadProguardMapping = false + ignoredVariants = ["fullRelease"] + sizeAnalysis { + enabled = true + enabledVariants = ["fullRelease"] + } + tracingInstrumentation { + enabled = false + } + } + """ + .trimIndent() + ) + + val build = runner.appendArguments(":app:assembleFullRelease", "--dry-run").build() + + // Size analysis upload task should be present despite variant being ignored + assertTrue(":app:uploadSentryBundleFullRelease" in build.output) + assertTrue(":app:uploadSentryApkFullRelease" in build.output) + // Proguard mapping task should still be skipped (respects ignoredVariants) + assertFalse(":app:uploadSentryProguardMappingsFullRelease" in build.output) + } + + @Test + fun `size analysis respects enabled flag even when variant is in enabledVariants`() { + appBuildFile.appendText( + // language=Groovy + """ + sentry { + autoUploadProguardMapping = false + sizeAnalysis { + enabled = false + enabledVariants = ["fullRelease"] + } + tracingInstrumentation { + enabled = false + } + } + """ + .trimIndent() + ) + + val build = runner.appendArguments(":app:assembleFullRelease", "--dry-run").build() + + // Size analysis should be disabled + assertFalse(":app:uploadSentryBundleFullRelease" in build.output) + assertFalse(":app:uploadSentryApkFullRelease" in build.output) + } + + @Test + fun `size analysis runs on all variants when enabledVariants is empty and enabled is true`() { + appBuildFile.appendText( + // language=Groovy + """ + sentry { + autoUploadProguardMapping = false + sizeAnalysis { + enabled = true + } + tracingInstrumentation { + enabled = false + } + } + """ + .trimIndent() + ) + + val buildRelease = runner.appendArguments(":app:assembleFullRelease", "--dry-run").build() + val buildDebug = runner.appendArguments(":app:assembleFullDebug", "--dry-run").build() + + // Both variants should have size analysis tasks + assertTrue(":app:uploadSentryBundleFullRelease" in buildRelease.output) + assertTrue(":app:uploadSentryApkFullRelease" in buildRelease.output) + assertTrue(":app:uploadSentryBundleFullDebug" in buildDebug.output) + assertTrue(":app:uploadSentryApkFullDebug" in buildDebug.output) + } + + @Test + fun `size analysis only runs on specified variants when enabledVariants is not empty`() { + appBuildFile.appendText( + // language=Groovy + """ + sentry { + autoUploadProguardMapping = false + sizeAnalysis { + enabled = true + enabledVariants = ["fullRelease"] + } + tracingInstrumentation { + enabled = false + } + } + """ + .trimIndent() + ) + + val buildRelease = runner.appendArguments(":app:assembleFullRelease", "--dry-run").build() + val buildDebug = runner.appendArguments(":app:assembleFullDebug", "--dry-run").build() + + // Only fullRelease should have size analysis tasks + assertTrue(":app:uploadSentryBundleFullRelease" in buildRelease.output) + assertTrue(":app:uploadSentryApkFullRelease" in buildRelease.output) + assertFalse(":app:uploadSentryBundleFullDebug" in buildDebug.output) + assertFalse(":app:uploadSentryApkFullDebug" in buildDebug.output) + } + private fun applyIgnores( ignoredVariants: Set = setOf(), ignoredBuildTypes: Set = setOf(),