Move task operations out of onVariants to fix AGP Artifacts API confl…#5690
Move task operations out of onVariants to fix AGP Artifacts API confl…#5690RyanCommits wants to merge 5 commits intogetsentry:mainfrom
Conversation
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
|
@sentry review |
There was a problem hiding this comment.
Thank you for your contribution @RyanCommits! We really appreciate this🙇
I understand that the PR is still draft but since it came to our attention I've added some early feedback. We would also appreciate a small repro to help us test the fix.
Co-authored-by: Antonis Lilis <antonis.lilis@gmail.com>
Co-authored-by: Antonis Lilis <antonis.lilis@gmail.com>
|
Hi @antonis, thank you for the feedback. I've updated the branch with your feedback, as well as simplified what I was doing. I have a repro of it issue here: This other branch has the fix implemented: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| // callbacks and the AGP Artifacts API transform chain is fully established. | ||
| project.afterEvaluate { | ||
| if (releaseVariants.isEmpty()) { | ||
| project.logger.warn("[sentry] No release variants collected, onVariants may have run after afterEvaluate. Sourcemap upload tasks will not be registered.") |
There was a problem hiding this comment.
Misleading warning for projects with only debug variants
Low Severity
The warning assumes releaseVariants is empty because onVariants ran after afterEvaluate, but it could also be empty because the project legitimately has no release variants (only debug). The warning message "onVariants may have run after afterEvaluate" is misleading in this case and could confuse developers working on debug-only configurations or sample projects. The code cannot distinguish between timing issues and the legitimate absence of release variants.
| task.name.endsWith("JsAndAssets") && | ||
| !task.name.contains("Debug") | ||
| }.each { bundleTask -> | ||
| if (!bundleTask.enabled) return |
There was a problem hiding this comment.
Inefficient nested loop processes each bundle task multiple times
Medium Severity
The nested loop structure iterates over all release variants in the outer loop and all bundle tasks in the inner loop, causing each bundle task to be processed N times (once per variant). Lines 98-112 execute for every (variant, bundleTask) combination, but extractCurrentVariants only returns non-null for one matching variant per bundle task. This means (N-1)×M iterations do unnecessary work (extracting task properties, calling forceSourceMapOutputFromBundleTask) before returning early on line 122. The loops should be restructured to process each bundle task once with its matching variant.
There was a problem hiding this comment.
Thank you for this fix, Ryan. Also thanks to the Sentry folks taking a quick look at this. I have some additional code that can be used to reproduce and test the issue. This is all Groovy code that can be added to an Android app module's build.gradle.
Note that while this suggested fix does address the current issue, it's still possible to run into similar issues in the future if other tasks are configured in afterEvaluate. Ideally this file would be converted to using lazy evaluation (API's like configureEach -- tasks.matching {}.each {} resolves immediately), but that will take a larger effort since tasks are currently being created for matched tasks. Those can't be put into configureEach, because the tasks won't be registered unless the "bundle" tasks are resolved. Instead the cli and module tasks would need to be known and created ahead of resolving the "bundle" tasks.
import com.android.build.api.artifact.ArtifactTransformationRequest
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationVariant
import com.android.build.api.variant.BuiltArtifact
import javax.inject.Inject
import java.nio.file.Files
import java.nio.file.StandardCopyOption
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle-api:9.0.1"
}
}
androidComponents.onVariants(androidComponents.selector().all()) { ApplicationVariant variant ->
def variantName = variant.name.capitalize()
// Early task resolution cause incorrect output destination
// tasks.matching { task -> task.name.startsWith("assemble") || task.name.endsWith("bundle") }.forEach {}
// Configure artifact transformations. This can be put in an `afterEvaluate` block and still be valid.
// APK ends up in intermediates directory
def copyApkTask = project.tasks.register("copy${variantName}Apks", CopyApkTask)
def transformationRequest = variant.artifacts.use(copyApkTask)
.wiredWithDirectories(CopyApkTask::getInput, CopyApkTask::getOutput)
.toTransformMany(SingleArtifact.APK.INSTANCE)
copyApkTask.configure { CopyApkTask task ->
task.transformationRequest.set(transformationRequest)
}
// Bundle ends up with wrong extension
def copyBundleTask = project.tasks.register("copy${variantName}Bundle", CopyBundleTask)
variant.artifacts.use(copyBundleTask)
.wiredWithFiles(CopyBundleTask::getInput, CopyBundleTask::getOutput)
.toTransform(SingleArtifact.BUNDLE.INSTANCE)
}
abstract class CopyApkTask extends DefaultTask {
private final WorkerExecutor workers
@Inject
CopyApkTask(WorkerExecutor workers) {
this.workers = workers
}
@InputFiles
abstract DirectoryProperty getInput();
@OutputDirectory
abstract DirectoryProperty getOutput();
@Internal
abstract Property<ArtifactTransformationRequest<CopyApkTask>> getTransformationRequest();
@TaskAction
void copyApk() {
transformationRequest.get().submit(this, workers.noIsolation(), CopyApksWorkItem) {
BuiltArtifact builtArtifact, Directory outputLocation, CopyApksWorkItemParameters params ->
def inputFile = new File(builtArtifact.outputFile)
def outputFile = new File(outputLocation.asFile, inputFile.name)
params.inputApkFile.set(inputFile)
params.outputApkFile.set(outputFile)
outputFile
}
}
}
interface CopyApksWorkItemParameters extends WorkParameters, Serializable {
RegularFileProperty getInputApkFile()
RegularFileProperty getOutputApkFile()
}
abstract class CopyApksWorkItem implements WorkAction<CopyApksWorkItemParameters> {
final CopyApksWorkItemParameters workItemParameters
@Inject
CopyApksWorkItem(CopyApksWorkItemParameters workItemParameters) {
this.workItemParameters = workItemParameters
}
@Override
void execute() {
def input = workItemParameters.inputApkFile.get().asFile
def output = workItemParameters.outputApkFile.get().asFile
FileUtil.copy(input, output)
}
}
class FileUtil {
static void copy(File src, File dst) {
println "Copying $src to $dst"
dst.parentFile.mkdirs()
dst.delete()
Files.copy(src.toPath(), dst.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES)
}
}
abstract class CopyBundleTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getInput();
@OutputFile
abstract RegularFileProperty getOutput();
@TaskAction
void copyBundle() {
def input = getInput().get().asFile
def output = getOutput().get().asFile
FileUtil.copy(input, output)
}
}| // callbacks and the AGP Artifacts API transform chain is fully established. | ||
| project.afterEvaluate { | ||
| if (releaseVariants.isEmpty()) { | ||
| project.logger.warn("[sentry] No release variants collected, onVariants may have run after afterEvaluate. Sourcemap upload tasks will not be registered.") |
There was a problem hiding this comment.
This warning is inaccurate. It should not be possible for onVariants to have been invoked afterEvaluate as it is called when the plugin is applied, which should not be in or after afterEvaluate in this case. Though it could indicate no release variants configured, as the AI review stated.
There was a problem hiding this comment.
wdyt of simplifying the message to [sentry] No release variants found. Source map upload tasks will not be registered.?
There was a problem hiding this comment.
Sounds great. The one you've got in #5714 looks good too!
|
The PR looks good! Would you be able to edit the changelog and add the following snippet? |
antonis
left a comment
There was a problem hiding this comment.
Thank you for iterating with the fixes @RyanCommits 🙇
The PR looks overall good but we realized that it reverts the changes in #5253 and would cause a regression of #5236
We will investigate this further and iterate back.
….gradle
project.afterEvaluate{} is not needed: bundle tasks are already registered
by the time onVariants fires, matching the timing of the original tasks.findAll.
Moving the tasks.names.contains() check and tasks.named().configure{} directly
into onVariants keeps the fix simple and avoids the regression risk that
afterEvaluate introduced in the earlier PR #5690.
Also fixes the indentation of the ~240-line configure{} closure body so it
is visually distinct from the enclosing onVariants block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ifacts API conflict (#5714) * fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict tasks.findAll iterates the entire task container, realizing every lazily-registered task as a side effect. This broke two distinct scenarios: - react-native-legal (issue #5236): AboutLibraries registers tasks lazily via tasks.register(); eager realization during onVariants caused a build crash. - Fullstory / AGP Artifacts API (issue #5698): AGP Artifacts API transforms (e.g. variant.artifacts.use().wiredWithDirectories().toTransformMany()) must be registered before the artifact chain is finalized. Realizing AGP's internal tasks inside onVariants locks the APK artifact prematurely, causing the APK to land in build/intermediates/ instead of build/outputs/. Fix: predict the two known RN bundle task names from the variant name (createBundle${Variant}JsAndAssets / bundle${Variant}JsAndAssets), check existence with tasks.names.contains() (no realization), then wire lazily via tasks.named(). A warn() is emitted when neither task is found so the skip is observable. Additional changes: - Add || currentVariants.isEmpty() guard to prevent orphan upload-task registration - Remove redundant bundleTask.configure { finalizedBy } nesting (already inside configure) Fixes #5698 Related: #5236, #5253 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Adds changelog * fix(android): fix variant capitalization and task timing in sentry.gradle - Replace v.name.capitalize() with substring(0,1).toUpperCase()+substring(1) so that flavored variants like freeRelease produce FreeRelease (not Freerelease), matching React Native's bundle task naming convention. - Replace tasks.named() with tasks.configureEach + name-set filter to handle bundle tasks registered after sentry's onVariants callback fires (e.g. when sentry.gradle is applied before the React Native plugin). configureEach does not iterate or realize the task container so the Fullstory AGP Artifacts API fix (#5698) and react-native-legal fix (#5236) are preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): restore missing bundle task warning via taskGraph.whenReady Re-adds the diagnostic warn() that was lost when switching from tasks.named() to tasks.configureEach. The check is deferred to gradle.taskGraph.whenReady so all plugins' onVariants callbacks (including the RN plugin's) have completed and tasks.names reflects the full set of registered tasks before we decide to emit the warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): move bundle task lookup to afterEvaluate, restore warning Follow the SAGP pattern (sentry-android-gradle-plugin/util/tasks.kt): register project.afterEvaluate{} inside onVariants{} so that task lookup is deferred until after all plugins have registered their tasks. onVariants fires during project evaluation — before the task container is complete — so tasks.configureEach registered there could miss late-registered bundle tasks. afterEvaluate runs after all onVariants callbacks (including the React Native plugin's) have completed, making tasks.names reliable. Replaces tasks.configureEach + gradle.taskGraph.whenReady with: - project.afterEvaluate for timing - tasks.names.contains() guard with inline warn() for missing tasks - tasks.named() for a targeted lazy reference (no container iteration) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): resolve AGP variant data inside onVariants, not afterEvaluate AGP Variant objects (outputs, applicationId, versionCode/versionName providers) are only valid inside the onVariants callback. Using them inside project.afterEvaluate{} can trigger late variant API access errors. Pre-extract all AGP-dependent data as plain values before registering the afterEvaluate block: - variantName (String) from v.name - variantApplicationId (String) from v.applicationId.get() - variantOutputsData (List<Map>) from v.outputs with all providers resolved Update extractCurrentVariants() to accept these plain values instead of the AGP Variant object so no AGP API is called outside onVariants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): remove afterEvaluate wrapper, fix indentation in sentry.gradle project.afterEvaluate{} is not needed: bundle tasks are already registered by the time onVariants fires, matching the timing of the original tasks.findAll. Moving the tasks.names.contains() check and tasks.named().configure{} directly into onVariants keeps the fix simple and avoids the regression risk that afterEvaluate introduced in the earlier PR #5690. Also fixes the indentation of the ~240-line configure{} closure body so it is visually distinct from the enclosing onVariants block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): pre-register sentry tasks in onVariants to fix Gradle 8.x Gradle 8.x forbids tasks.register() inside a task configuration action (the closure passed to tasks.named().configure {}). The previous change wrapped the entire sentry task setup in tasks.named(bundleTaskName).configure {}, which triggered the restriction when the RN bundle task was being created: DefaultTaskContainer#register(String, Action) on task set cannot be executed in the current context. Fix: pre-register all sentry task stubs (cliTask, modulesTask, cleanup tasks) directly in onVariants where task registration is always allowed. The tasks.named().configure {} block now only calls .configure {} on already-registered tasks and wires finalizedBy/dependsOn — both of which are allowed inside configuration actions. extractCurrentVariants() is now called in onVariants using the bundle task name as a proxy (the helper only reads bundleTask.name), so currentVariants is available before tasks.named().configure {} is reached. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): use ctx map to avoid TaskProvider.configure inside configure action Gradle 8.14.3 also forbids TaskProvider.configure(Action) inside a task configuration action, not just tasks.register(). The previous fix moved tasks.register() out but left tasks.named(other).configure {} calls inside bundleTask.configure {}, which triggered: DefaultTaskContainer#NamedDomainObjectProvider.configure(Action) on task set cannot be executed in the current context. Fix: introduce a shared mutable context map (ctx) that task action closures (doFirst/doLast/onlyIf/delete) capture by reference. The tasks are fully registered and wired in onVariants — including their complete doFirst/doLast logic referencing ctx. bundleTask.configure {} now does exactly two things: populate ctx from the bundle task's properties, and call bundleTask.finalizedBy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(android): add Gradle test-repro for task-realization regressions Adds a self-contained Android project under packages/core/test-repro/ that verifies two canary regressions: - CANARY 1 (#5236, react-native-legal): sentry.gradle must not realize lazily-registered tasks by iterating the task container (tasks.findAll). - CANARY 2 (#5698, Fullstory): sentry.gradle must not configure the fullstoryTransformRelease task before AGP's onVariants wires it via toTransformMany(), otherwise the APK lands in build/intermediates/ instead of build/outputs/. Includes stubs for multiple approaches under test: - sentry-main.gradle → tasks.findAll in onVariants (❌ both canaries fail) - sentry-noop.gradle → baseline no-op (✅ both canaries pass) - sentry-named.gradle → tasks.names.contains + tasks.named (✅ our fix) - sentry-configureEach.gradle → tasks.configureEach alternative (✅) - sentry-afterEvaluate.gradle → afterEvaluate + tasks.named (✅) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert "test(android): add Gradle test-repro for task-realization regressions" This reverts commit 513a738. * fix(android): replace tasks.findAll with tasks.named to fix AGP Artifacts API conflict tasks.findAll iterates the entire task container, realizing every lazily- registered task regardless of whether it matches the predicate. This caused two distinct issues: - react-native-legal (#5236): AboutLibraries tasks were realized as a side-effect of container iteration. - Fullstory / AGP Artifacts API (#5698): fullstoryTransformRelease was configured before AGP's toTransformMany() wired its artifact paths, causing the APK to land in build/intermediates/ instead of build/outputs/. Fix: predict the bundle task name from the variant name and use tasks.names.contains() (no realization) to check existence, then tasks.named().get() to obtain only that specific task. The rest of the task registration logic is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(android): simplify variant capitalization using Groovy capitalize() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>


📢 Type of change
📜 Description
Fixes issue where calling tasks.matching{}.each{} inside androidComponents.onVariants()
forces task realization during AGP's variant configuration phase, disrupting the
Artifacts API transform chain used by other plugins (e.g. Fullstory).
This caused APK transforms from other plugins to output to build/intermediates/
instead of build/outputs/, breaking downstream tooling that expects APKs in the
standard location.
The fix:
project.afterEvaluate within the plugins.withId block
This ensures the AGP Artifacts API transform chain is fully established before
Sentry accesses any tasks, preventing interference with other plugins.
💡 Motivation and Context
This is currently conflicting with the Fullstory plugin
💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled🔮 Next steps