Skip to content

fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict#5714

Open
antonis wants to merge 13 commits intomainfrom
antonis/issue-5698
Open

fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict#5714
antonis wants to merge 13 commits intomainfrom
antonis/issue-5698

Conversation

@antonis
Copy link
Contributor

@antonis antonis commented Feb 24, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

tasks.findAll iterates the entire task container, realizing every lazily-registered task regardless of whether it matches the predicate. This causes the Fullstory / AGP Artifacts API issue (#5698): plugins that pre-register a transform task and wire it via toTransformMany() inside onVariants can have that task configured before AGP injects its artifact paths, causing the APK to land in build/intermediates/ instead of build/outputs/.

Why the predicate doesn't help: tasks.findAll { predicate } must yield configured Task objects. Realizing a TaskProvider into a Task happens before any predicate check — there is no short-circuiting on task name.

Fix: predict the bundle task name from the variant (createBundle${Variant}JsAndAssets / bundle${Variant}JsAndAssets), check existence with tasks.names.contains() (no realization), then obtain the task with tasks.named().get(). A warn() is emitted if neither task is found. All task registration logic is unchanged from main.

Changeset: 13 lines in sentry.gradle — only the task lookup preamble changes.

💡 Motivation and Context

Fixes #5698

This is a follow up to #5690 which fixed #5698 but introduced a regression in #5236. This PR avoids that regression by building on main's onVariants approach rather than moving to afterEvaluate.

💚 How did you test it?

Verified with a local Android test-repro project containing two canary checks:

CANARY 1 — react-native-legal (#5236)
Registers sentryCanaryTask lazily. Checks that sentry does not iterate the task container and realize unrelated tasks.

CANARY 2 — Fullstory / AGP Artifacts API (#5698)
Pre-registers fullstoryTransformRelease lazily and wires it into the APK artifact chain via toTransformMany() inside a second onVariants callback registered after sentry's. Checks that sentry does not configure the task before toTransformMany() fires — if it does, the APK lands in build/intermediates/ instead of build/outputs/.

Approach CANARY 1 CANARY 2 APK location
No-op baseline (no sentry code) build/outputs/
tasks.findAll (main branch) build/intermediates/ ← wrong
tasks.named() (this PR) build/outputs/

📝 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

…ifacts 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>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 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).


  • fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict by antonis in #5714
  • feat(tooling): add Cursor BUGBOT.md review guidelines by antonis in #5716
  • chore(ci): Use runner group 10 by itaybre in #5717
  • chore(deps): bump qs to ^6.14.2 by antonis in #5707
  • chore(deps): bump lodash to ^4.17.23 by antonis in #5702
  • chore(deps): bump getsentry/craft from 2.21.4 to 2.21.7 by dependabot in #5694
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.21.4 to 2.21.7 by dependabot in #5695
  • chore(deps): update CLI to v3.2.2 by github-actions in #5692
  • chore(deps): bump github/codeql-action from 4.32.3 to 4.32.4 by dependabot in #5693
  • chore(deps): update Maestro to v2.2.0 by github-actions in #5691
  • 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
Copy link
Contributor Author

antonis commented Feb 24, 2026

@sentry review

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Feb 24, 2026
…adle

- 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>
antonis and others added 2 commits February 24, 2026 16:33
…eady

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>
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>
…aluate

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>
@antonis
Copy link
Contributor Author

antonis commented Feb 24, 2026

@cursor review

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

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 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1213.26 ms 1215.63 ms 2.37 ms
Size 3.38 MiB 4.78 MiB 1.40 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
ea3e26e+dirty 1229.13 ms 1228.46 ms -0.67 ms
80e4616+dirty 1221.32 ms 1225.64 ms 4.32 ms
818a608+dirty 1205.76 ms 1208.00 ms 2.24 ms
77061ed+dirty 1233.16 ms 1234.88 ms 1.71 ms
bef3709+dirty 1222.07 ms 1220.24 ms -1.83 ms
a206511+dirty 1185.00 ms 1186.35 ms 1.35 ms
74979ac+dirty 1210.49 ms 1213.31 ms 2.82 ms
a2bb688+dirty 1223.53 ms 1232.90 ms 9.37 ms
8a868fe+dirty 1221.50 ms 1230.78 ms 9.28 ms
d590428+dirty 1211.77 ms 1220.51 ms 8.75 ms

App size

Revision Plain With Sentry Diff
ea3e26e+dirty 3.41 MiB 4.58 MiB 1.17 MiB
80e4616+dirty 3.38 MiB 4.60 MiB 1.22 MiB
818a608+dirty 2.63 MiB 3.91 MiB 1.28 MiB
77061ed+dirty 2.63 MiB 3.98 MiB 1.34 MiB
bef3709+dirty 3.38 MiB 4.78 MiB 1.40 MiB
a206511+dirty 3.41 MiB 4.67 MiB 1.25 MiB
74979ac+dirty 3.38 MiB 4.60 MiB 1.22 MiB
a2bb688+dirty 2.63 MiB 3.99 MiB 1.36 MiB
8a868fe+dirty 3.38 MiB 4.60 MiB 1.22 MiB
d590428+dirty 3.38 MiB 4.78 MiB 1.39 MiB

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1217.02 ms 1223.40 ms 6.37 ms
Size 3.38 MiB 4.78 MiB 1.40 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
ea3e26e+dirty 1216.61 ms 1214.15 ms -2.47 ms
80e4616+dirty 1206.90 ms 1205.94 ms -0.96 ms
818a608+dirty 1218.84 ms 1223.18 ms 4.34 ms
77061ed+dirty 1210.77 ms 1218.45 ms 7.68 ms
bef3709+dirty 1217.79 ms 1225.33 ms 7.54 ms
a206511+dirty 1225.02 ms 1223.74 ms -1.28 ms
74979ac+dirty 1212.33 ms 1212.54 ms 0.21 ms
a2bb688+dirty 1244.82 ms 1238.60 ms -6.22 ms
8a868fe+dirty 1206.85 ms 1215.04 ms 8.19 ms
d590428+dirty 1221.23 ms 1225.27 ms 4.03 ms

App size

Revision Plain With Sentry Diff
ea3e26e+dirty 3.41 MiB 4.58 MiB 1.17 MiB
80e4616+dirty 3.38 MiB 4.60 MiB 1.22 MiB
818a608+dirty 3.19 MiB 4.48 MiB 1.29 MiB
77061ed+dirty 3.19 MiB 4.54 MiB 1.36 MiB
bef3709+dirty 3.38 MiB 4.78 MiB 1.40 MiB
a206511+dirty 3.41 MiB 4.67 MiB 1.25 MiB
74979ac+dirty 3.38 MiB 4.60 MiB 1.22 MiB
a2bb688+dirty 3.19 MiB 4.56 MiB 1.37 MiB
8a868fe+dirty 3.38 MiB 4.60 MiB 1.22 MiB
d590428+dirty 3.38 MiB 4.78 MiB 1.39 MiB

def variantOutputsData = v.outputs.collect { output ->
[baseName: output.baseName, versionCode: output.versionCode.getOrElse(0), versionName: output.versionName.getOrElse('')]
}
project.afterEvaluate {

Choose a reason for hiding this comment

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

This project.afterEvaluate is what was causing the regression mentioned on #5690. It should be removed. You should be able to determine the presence of the bundle tasks immediately as that was happening before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for looking itno this @ReeceLaF 🙇 Good catch 👍
Updated with 5b6b7c0

[baseName: output.baseName, versionCode: output.versionCode.getOrElse(0), versionName: output.versionName.getOrElse('')]
}
project.afterEvaluate {
def sentryBundleTaskName = ["createBundle${variantCapitalized}JsAndAssets", "bundle${variantCapitalized}JsAndAssets"].find { tasks.names.contains(it) }

Choose a reason for hiding this comment

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

Nice! This matches task names without realizing them. This does assume only one or the other of these tasks will exist though, whereas before it did not. Is that correct, or should it be a list of matches instead?

With the bundle task names, you should be able to immediately register the other tasks and then configure them if the bundleTask is configured. Something like this ought to work, I think:

def cliTaskProvider = tasks.register(nameCliTask) {
    dependsOn sentryBundleTaskName // Ensure bundle task is included and configured if this task runs
    onlyIf { shouldSentryAutoUploadGeneral() }
    description = 'upload debug symbols to sentry'
    group = 'sentry.io'
}
tasks.named(sentryBundleTaskName).configure { bundleTask ->
    def cliTask = cliTaskProvider.get()
    cliTask.enabled = bundleTask.enabled
    // Rest of configuration
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point — but in practice this isn't an issue. The two names are the same conceptual task renamed between React Native versions: bundle${Variant}JsAndAssets (pre-0.71) and createBundle${Variant}JsAndAssets (0.71+). The react-native-gradle-plugin registers exactly one per variant depending on the RN version in use, so both can never coexist for the same variant. .find {} correctly returns whichever one is present, which is the same effective behavior as the old .each {} loop (which always iterated exactly one).

antonis and others added 7 commits February 25, 2026 11:06
….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>
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>
…igure 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>
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>
…acts 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 474.96 ms 513.11 ms 38.15 ms
Size 43.75 MiB 48.46 MiB 4.71 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
c7f264b 434.98 ms 452.96 ms 17.98 ms
9f211e3 451.50 ms 500.00 ms 48.50 ms
9ced351+dirty 405.40 ms 419.39 ms 13.98 ms
f70acbf+dirty 373.39 ms 382.81 ms 9.43 ms
f234eb4+dirty 407.62 ms 429.64 ms 22.02 ms
2adbd1e+dirty 433.98 ms 427.96 ms -6.02 ms
7886639+dirty 425.10 ms 477.73 ms 52.63 ms
a206511+dirty 424.28 ms 474.82 ms 50.54 ms
98f632c 424.25 ms 435.48 ms 11.23 ms
46da307 455.92 ms 443.79 ms -12.13 ms

App size

Revision Plain With Sentry Diff
c7f264b 17.75 MiB 19.68 MiB 1.94 MiB
9f211e3 17.75 MiB 19.68 MiB 1.94 MiB
9ced351+dirty 43.75 MiB 48.41 MiB 4.66 MiB
f70acbf+dirty 17.75 MiB 19.68 MiB 1.94 MiB
f234eb4+dirty 17.75 MiB 19.74 MiB 1.99 MiB
2adbd1e+dirty 17.75 MiB 19.70 MiB 1.96 MiB
7886639+dirty 43.75 MiB 48.42 MiB 4.67 MiB
a206511+dirty 43.75 MiB 48.07 MiB 4.32 MiB
98f632c 17.75 MiB 20.15 MiB 2.41 MiB
46da307 17.75 MiB 19.68 MiB 1.93 MiB

@github-actions
Copy link
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 453.41 ms 489.61 ms 36.20 ms
Size 43.94 MiB 49.33 MiB 5.39 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
664c66f+dirty 376.23 ms 389.51 ms 13.28 ms
d73150f+dirty 424.60 ms 454.35 ms 29.75 ms
4a17c8f+dirty 368.54 ms 381.43 ms 12.89 ms
b3b5b0d+dirty 361.42 ms 403.90 ms 42.48 ms
9ced351+dirty 361.74 ms 411.45 ms 49.70 ms
7886639+dirty 530.30 ms 571.34 ms 41.04 ms
c08359e+dirty 406.04 ms 428.87 ms 22.83 ms
3099014+dirty 344.58 ms 404.21 ms 59.63 ms
d751a5d+dirty 341.61 ms 403.06 ms 61.45 ms
682f0f5+dirty 402.33 ms 440.61 ms 38.28 ms

App size

Revision Plain With Sentry Diff
664c66f+dirty 43.94 MiB 49.38 MiB 5.44 MiB
d73150f+dirty 43.94 MiB 49.38 MiB 5.44 MiB
4a17c8f+dirty 43.94 MiB 48.82 MiB 4.88 MiB
b3b5b0d+dirty 7.15 MiB 8.41 MiB 1.26 MiB
9ced351+dirty 43.94 MiB 49.27 MiB 5.33 MiB
7886639+dirty 43.94 MiB 49.28 MiB 5.34 MiB
c08359e+dirty 7.15 MiB 8.42 MiB 1.27 MiB
3099014+dirty 7.15 MiB 8.43 MiB 1.27 MiB
d751a5d+dirty 7.15 MiB 8.41 MiB 1.26 MiB
682f0f5+dirty 43.94 MiB 48.91 MiB 4.97 MiB

@antonis antonis marked this pull request as ready for review February 25, 2026 15:13
@antonis antonis requested a review from alwx as a code owner February 25, 2026 15:13
@antonis
Copy link
Contributor Author

antonis commented Feb 25, 2026

Thank you again for all your work on #5690 and the early feedback on this PR @ReeceLaF 🙇
Could you check if the current minimal changes solve the issue for your use case? 🙏

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

Labels

ready-to-merge Triggers the full CI test suite

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

2 participants