Skip to content

Move task operations out of onVariants to fix AGP Artifacts API confl…#5690

Open
RyanCommits wants to merge 5 commits intogetsentry:mainfrom
RyanCommits:fix/gradle-onvariants-task-realization
Open

Move task operations out of onVariants to fix AGP Artifacts API confl…#5690
RyanCommits wants to merge 5 commits intogetsentry:mainfrom
RyanCommits:fix/gradle-onvariants-task-realization

Conversation

@RyanCommits
Copy link

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 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:

  • Collect only variant metadata (name, outputs) in onVariants callback
  • Move all task-level operations (tasks.matching, tasks.register, etc.) into
    project.afterEvaluate within the plugins.withId block
  • Update extractCurrentVariants to accept pre-collected data instead of live variant

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

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

@github-actions
Copy link
Contributor

github-actions bot commented Feb 20, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • Move task operations out of onVariants to fix AGP Artifacts API confl… by RyanCommits in #5690
  • chore(deps): update Cocoa SDK to v9.5.0 by github-actions in #5685
  • chore(deps): update Android SDK Stubs to v8.33.0 by github-actions in #5697
  • chore(deps): update Android SDK to v8.33.0 by github-actions in #5684
  • chore(deps): update Sentry Android Gradle Plugin to v6.1.0 by github-actions in #5687
  • Ref(CI): Add android sdk version check by lucas-zimerman in #5686

🤖 This preview updates automatically when you update the PR.

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Feb 23, 2026
@antonis
Copy link
Contributor

antonis commented Feb 23, 2026

@sentry review

Copy link
Contributor

@antonis antonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

RyanCommits and others added 3 commits February 23, 2026 10:37
Co-authored-by: Antonis Lilis <antonis.lilis@gmail.com>
Co-authored-by: Antonis Lilis <antonis.lilis@gmail.com>
@RyanCommits RyanCommits marked this pull request as ready for review February 23, 2026 16:46
@RyanCommits
Copy link
Author

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:
https://github.com/RyanCommits/sentry-fullstory-issue-repro/tree/master
You'll need to configure your own sentry account and EAS build settings in your environment, but the issue should show with build command eas build --local --platform android

This other branch has the fix implemented:
https://github.com/RyanCommits/sentry-fullstory-issue-repro/tree/issue-fix

@RyanCommits RyanCommits requested a review from antonis February 23, 2026 17:48
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

task.name.endsWith("JsAndAssets") &&
!task.name.contains("Debug")
}.each { bundleTask ->
if (!bundleTask.enabled) return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Copy link

@ReeceLaF ReeceLaF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt of simplifying the message to [sentry] No release variants found. Source map upload tasks will not be registered.?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great. The one you've got in #5714 looks good too!

@lucas-zimerman
Copy link
Collaborator

The PR looks good! Would you be able to edit the changelog and add the following snippet?


## Unreleased

### Fixes

- Android: defer Sentry sourcemap upload task wiring to afterEvaluate to avoid configuring Gradle tasks inside onVariants and improve AGP compatibility ([#5690](https://github.com/getsentry/sentry-react-native/pull/5690))

Copy link
Contributor

@antonis antonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

antonis added a commit that referenced this pull request Feb 25, 2026
….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>
antonis added a commit that referenced this pull request Feb 26, 2026
…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>
@antonis antonis removed the ready-to-merge Triggers the full CI test suite label Feb 26, 2026
Copy link
Contributor

@antonis antonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up that we've proceeded with the alternative implementation in #5714 due to the regression mentioned here. The fix is now shipped in 8.2.0 🚢

Thank you again for taking the effort to prepare this PR and pushing us forward to fix the issue 🙇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sentry.gradle task realization in onVariants breaks AGP Artifacts API transform chain

4 participants