From 241265de0dc818c6d5b50842049890239c18da53 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 6 May 2026 11:15:45 +0200 Subject: [PATCH 1/2] feat(source-context): Include sources from library module dependencies Source context was only collected from the app module, which meant stack traces from library module code had no source context in Sentry. This adds support for collecting sources from all transitive local project dependencies using Gradle's consumable/resolvable configuration pattern, which is Project Isolation compatible. Library modules publish their source directories via a consumable `sentrySourceElements` configuration with a custom attribute. The app module resolves these through a resolvable configuration that inherits the same dependency scopes as RuntimeClasspath, with lenient resolution to gracefully skip dependencies without the plugin. Two setup paths: apply `io.sentry.android.gradle` to library modules, or apply the new `io.sentry.android.gradle.settings` settings plugin to auto-configure all library modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugin-build/build.gradle.kts | 15 +++ .../android/gradle/AndroidComponentsConfig.kt | 16 ++- .../io/sentry/android/gradle/SentryPlugin.kt | 14 ++- .../android/gradle/SentrySettingsPlugin.kt | 17 +++ .../sourcecontext/SourceContextArtifacts.kt | 41 +++++++ .../SentryPluginSourceContextTest.kt | 110 ++++++++++++++++++ 6 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 9aaf9b856..7e9b87dea 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -168,6 +168,10 @@ gradlePlugin { implementationClass = "io.sentry.android.gradle.snapshot.metadata.SentrySnapshotMetadataPlugin" } + register("sentrySettingsPlugin") { + id = "io.sentry.android.gradle.settings" + implementationClass = "io.sentry.android.gradle.SentrySettingsPlugin" + } } } @@ -203,6 +207,9 @@ distributions { create("sentrySnapshotMetadataPluginMarker") { contents { from("build${sep}publications${sep}sentrySnapshotMetadataPluginPluginMarkerMaven") } } + create("sentrySettingsPluginMarker") { + contents { from("build${sep}publications${sep}sentrySettingsPluginPluginMarkerMaven") } + } } tasks.named("distZip") { @@ -257,6 +264,14 @@ tasks.named("sentrySnapshotMetadataPluginMarkerDistZip").configure { dependsOn("generatePomFileForSentrySnapshotMetadataPluginPluginMarkerMavenPublication") } +tasks.named("sentrySettingsPluginMarkerDistTar").configure { + dependsOn("generatePomFileForSentrySettingsPluginPluginMarkerMavenPublication") +} + +tasks.named("sentrySettingsPluginMarkerDistZip").configure { + dependsOn("generatePomFileForSentrySettingsPluginPluginMarkerMavenPublication") +} + tasks.withType().configureEach { testLogging { events = setOf(TestLogEvent.SKIPPED, TestLogEvent.PASSED, TestLogEvent.FAILED) 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 4fcb35d83..9d2d6c7e7 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 @@ -26,6 +26,7 @@ import io.sentry.android.gradle.services.SentryModulesService import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask import io.sentry.android.gradle.sourcecontext.OutputPaths import io.sentry.android.gradle.sourcecontext.SourceContext +import io.sentry.android.gradle.sourcecontext.resolveDependencySources import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask import io.sentry.android.gradle.tasks.InjectSentryMetaPropertiesIntoAssetsTask import io.sentry.android.gradle.tasks.PropertiesFileOutputTask @@ -44,6 +45,7 @@ import io.sentry.android.gradle.util.SentryPluginUtils.isMinificationEnabled import io.sentry.android.gradle.util.SentryPluginUtils.isVariantAllowed import io.sentry.android.gradle.util.collectModules import io.sentry.android.gradle.util.hookWithAssembleTasks +import io.sentry.gradle.common.filterBuildConfig import java.io.File import org.gradle.api.Project import org.gradle.api.file.Directory @@ -103,7 +105,19 @@ fun ApplicationAndroidComponentsExtension.configure( project.layout.projectDirectory.dir(it) } } - val sourceFiles = sentryVariant.sources(project, additionalSourcesProvider) + val moduleSourceFiles = sentryVariant.sources(project, additionalSourcesProvider) + + val sourceFiles = + if (extension.includeSourceContext.get()) { + val dependencySources = resolveDependencySources(project, variant.name) + moduleSourceFiles?.map { currentSources -> + val depDirs = + dependencySources.files.map { project.layout.projectDirectory.dir(it.absolutePath) } + (currentSources + depDirs).filterBuildConfig().toSet() + } + } else { + moduleSourceFiles + } val tasksGeneratingProperties = mutableListOf>() val sourceContextTasks = diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt index 2582c78f2..c0fc92f1e 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt @@ -4,6 +4,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import io.sentry.BuildConfig import io.sentry.android.gradle.autoinstall.installDependencies import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.sourcecontext.registerSentrySourceElements import io.sentry.android.gradle.util.AgpVersions import java.io.File import javax.inject.Inject @@ -30,11 +31,14 @@ constructor(private val buildEvents: BuildEventListenerRegistryInternal) : Plugi .trimIndent() ) } - if (!project.plugins.hasPlugin("com.android.application")) { + if ( + !project.plugins.hasPlugin("com.android.application") && + !project.plugins.hasPlugin("com.android.library") + ) { project.logger.warn( """ - WARNING: Using 'io.sentry.android.gradle' is only supported for the app module. - Please make sure that you apply the Sentry gradle plugin alongside 'com.android.application' on the _module_ level, and not on the root project level. + WARNING: Using 'io.sentry.android.gradle' is only supported for app and library modules. + Please make sure that you apply the Sentry gradle plugin alongside 'com.android.application' or 'com.android.library' on the _module_ level, and not on the root project level. https://docs.sentry.io/platforms/android/configuration/gradle/ """ .trimIndent() @@ -67,6 +71,10 @@ constructor(private val buildEvents: BuildEventListenerRegistryInternal) : Plugi project.installDependencies(extension, true) } + + project.pluginManager.withPlugin("com.android.library") { + registerSentrySourceElements(project) + } } companion object { diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt new file mode 100644 index 000000000..c96c9f4a4 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt @@ -0,0 +1,17 @@ +package io.sentry.android.gradle + +import io.sentry.android.gradle.sourcecontext.registerSentrySourceElements +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings + +class SentrySettingsPlugin : Plugin { + + override fun apply(settings: Settings) { + settings.gradle.beforeProject { project -> + project.pluginManager.withPlugin("com.android.library") { + registerSentrySourceElements(project) + } + project.pluginManager.withPlugin("java-library") { registerSentrySourceElements(project) } + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt new file mode 100644 index 000000000..f4f2837d5 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt @@ -0,0 +1,41 @@ +package io.sentry.android.gradle.sourcecontext + +import io.sentry.android.gradle.util.SentryPluginUtils.capitalizeUS +import org.gradle.api.Project +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.FileCollection + +val SENTRY_ARTIFACT_ATTR: Attribute = Attribute.of("io.sentry.artifact", String::class.java) + +const val SENTRY_SOURCES_VALUE = "sentry-sources" + +fun registerSentrySourceElements(project: Project) { + val config = + project.configurations.create("sentrySourceElements") { + it.isCanBeConsumed = true + it.isCanBeResolved = false + it.attributes { attrs -> attrs.attribute(SENTRY_ARTIFACT_ATTR, SENTRY_SOURCES_VALUE) } + } + listOf("src/main/java", "src/main/kotlin").forEach { path -> + val dir = project.file(path) + if (dir.isDirectory) { + config.outgoing.artifact(dir) + } + } +} + +fun resolveDependencySources(project: Project, variantName: String): FileCollection { + val sentrySourcesPath = + project.configurations.create("sentrySourcesFor${variantName.capitalizeUS()}") { + it.isCanBeConsumed = false + it.isCanBeResolved = true + it.attributes { attrs -> attrs.attribute(SENTRY_ARTIFACT_ATTR, SENTRY_SOURCES_VALUE) } + } + + val runtimeClasspath = project.configurations.getByName("${variantName}RuntimeClasspath") + runtimeClasspath.extendsFrom + .filter { !it.isCanBeResolved && !it.isCanBeConsumed } + .forEach { sentrySourcesPath.extendsFrom(it) } + + return sentrySourcesPath.incoming.artifactView { view -> view.lenient(true) }.files +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt index 4be98743b..4e4264037 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt @@ -348,6 +348,116 @@ class SentryPluginSourceContextTest : assertTrue(subsequentBuild.output) { "BUILD SUCCESSFUL" in subsequentBuild.output } } + @Test + fun `bundles source context from library module dependencies`() { + // Set up settings.gradle to include the :library module + File(testProjectDir.root, "settings.gradle") + .writeText( + // language=Groovy + """ + include ':app', ':module', ':library' + """ + .trimIndent() + ) + + // Create the library module with the sentry plugin to publish source elements + val libraryDir = File(testProjectDir.root, "library").apply { mkdirs() } + File(libraryDir, "build.gradle") + .writeText( + // language=Groovy + """ + plugins { + id 'com.android.library' + id 'io.sentry.android.gradle' + } + + android { + namespace 'com.example.library' + compileSdkVersion 35 + defaultConfig { + minSdkVersion 21 + } + } + + sentry { + autoInstallation.enabled = false + telemetry = false + } + """ + .trimIndent() + ) + + val libSrcDir = File(libraryDir, "src/main/java/com/example/library") + libSrcDir.mkdirs() + val libContents = + // language=java + """ + package com.example.library; + + public class LibHelper { + public static int add(int a, int b) { return a + b; } + } + """ + .trimIndent() + File(libSrcDir, "LibHelper.java").writeText(libContents) + + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + android { + namespace 'com.example' + + buildFeatures { + buildConfig false + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + } + + dependencies { + implementation project(':library') + } + + sentry { + debug = true + includeSourceContext = true + autoUploadSourceContext = false + autoUploadProguardMapping = false + org = "sentry-sdks" + projectName = "sentry-android" + } + """ + .trimIndent() + ) + + sentryPropertiesFile.writeText("") + val ktContents = testProjectDir.withDummyKtFile() + + val result = runner.appendArguments("app:assembleRelease").build() + + assertTrue(result.output) { "BUILD SUCCESSFUL" in result.output } + + // App module sources should be bundled + verifySourceBundleContents(testProjectDir.root, "files/_/_/com/example/Example.jvm", ktContents) + + // Library module sources should also be bundled + verifySourceBundleContents( + testProjectDir.root, + "files/_/_/com/example/library/LibHelper.jvm", + libContents, + ) + } + @Test fun `uploadSourceBundle task is not up-to-date on subsequent builds if cli path changes`() { val sentryCli = SentryCliProvider.getSentryCliPath(File(""), File("build"), File("")) From 37e4794302ce96ed2ddfb78350388fbe94c921e6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 6 May 2026 11:19:46 +0200 Subject: [PATCH 2/2] refactor: Settings plugin auto-applies project plugin per blog best practices Instead of the settings plugin performing build logic directly, it now auto-applies `io.sentry.android.gradle` to library modules. The project plugin handles all actual configuration. Follows the pattern from liutikas.net/2024/10/28/DRY-Gradle-Configuration-Values.html. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/android/gradle/SentrySettingsPlugin.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt index c96c9f4a4..84b8bafb8 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt @@ -1,6 +1,5 @@ package io.sentry.android.gradle -import io.sentry.android.gradle.sourcecontext.registerSentrySourceElements import org.gradle.api.Plugin import org.gradle.api.initialization.Settings @@ -9,9 +8,11 @@ class SentrySettingsPlugin : Plugin { override fun apply(settings: Settings) { settings.gradle.beforeProject { project -> project.pluginManager.withPlugin("com.android.library") { - registerSentrySourceElements(project) + project.pluginManager.apply("io.sentry.android.gradle") + } + project.pluginManager.withPlugin("java-library") { + project.pluginManager.apply("io.sentry.android.gradle") } - project.pluginManager.withPlugin("java-library") { registerSentrySourceElements(project) } } } }