fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict#5714
fix(android): replace tasks.findAll with tasks.named() to fix AGP Artifacts API conflict#5714
Conversation
…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>
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 |
…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>
…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>
|
@cursor review |
There was a problem hiding this comment.
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.
iOS (legacy) Performance metrics 🚀
|
| 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 |
iOS (new) Performance metrics 🚀
|
| 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 |
packages/core/sentry.gradle
Outdated
| def variantOutputsData = v.outputs.collect { output -> | ||
| [baseName: output.baseName, versionCode: output.versionCode.getOrElse(0), versionName: output.versionName.getOrElse('')] | ||
| } | ||
| project.afterEvaluate { |
There was a problem hiding this comment.
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.
packages/core/sentry.gradle
Outdated
| [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) } |
There was a problem hiding this comment.
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
}There was a problem hiding this comment.
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).
….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>
…ressions" This reverts commit 513a738.
…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>
Android (legacy) Performance metrics 🚀
|
| 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 |
Android (new) Performance metrics 🚀
|
| 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 |
📢 Type of change
📜 Description
tasks.findAlliterates 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 viatoTransformMany()insideonVariantscan have that task configured before AGP injects its artifact paths, causing the APK to land inbuild/intermediates/instead ofbuild/outputs/.Why the predicate doesn't help:
tasks.findAll { predicate }must yield configuredTaskobjects. Realizing aTaskProviderinto aTaskhappens 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 withtasks.names.contains()(no realization), then obtain the task withtasks.named().get(). Awarn()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
onVariantsapproach rather than moving toafterEvaluate.💚 How did you test it?
Verified with a local Android test-repro project containing two canary checks:
CANARY 1 — react-native-legal (#5236)
Registers
sentryCanaryTasklazily. Checks that sentry does not iterate the task container and realize unrelated tasks.CANARY 2 — Fullstory / AGP Artifacts API (#5698)
Pre-registers
fullstoryTransformReleaselazily and wires it into the APK artifact chain viatoTransformMany()inside a secondonVariantscallback registered after sentry's. Checks that sentry does not configure the task beforetoTransformMany()fires — if it does, the APK lands inbuild/intermediates/instead ofbuild/outputs/.build/outputs/tasks.findAll(main branch)build/intermediates/← wrongtasks.named()(this PR)build/outputs/✓📝 Checklist
sendDefaultPIIis enabled🔮 Next steps