From f42787cfd17bbdf6edf1e858652b91a5b58e78de Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Oct 2025 11:33:04 +0200 Subject: [PATCH 1/4] Add support for Gradle 9 --- packages/core/sentry.gradle | 369 ++++++++++++++++++------------------ 1 file changed, 188 insertions(+), 181 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index a461813e8f..16d3a650c9 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -4,15 +4,20 @@ import java.util.regex.Matcher import java.util.regex.Pattern project.ext.shouldSentryAutoUploadNative = { -> - return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true' + return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true' } project.ext.shouldSentryAutoUploadGeneral = { -> - return System.getenv('SENTRY_DISABLE_AUTO_UPLOAD') != 'true' + return System.getenv('SENTRY_DISABLE_AUTO_UPLOAD') != 'true' } project.ext.shouldSentryAutoUpload = { -> - return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() + return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() +} + +interface InjectedExecOps { + @Inject //@javax.inject.Inject + ExecOperations getExecOps() } def config = project.hasProperty("sentryCli") ? project.sentryCli : []; @@ -82,179 +87,181 @@ project.afterEvaluate { def nameModulesCleanup = "${bundleTask.name}_SentryCollectModulesCleanUp" // Upload the source map several times if necessary: once for each release and versionCode. currentVariants.each { key, currentVariant -> - def variant = currentVariant[0] - def releaseName = currentVariant[1] - def versionCode = currentVariant[2] - applicationVariant = currentVariant[3] - - try { - if (versionCode instanceof String) { - versionCode = Integer.parseInt(versionCode) - versionCode = Math.abs(versionCode) + def variant = currentVariant[0] + def releaseName = currentVariant[1] + def versionCode = currentVariant[2] + applicationVariant = currentVariant[3] + + try { + if (versionCode instanceof String) { + versionCode = Integer.parseInt(versionCode) + versionCode = Math.abs(versionCode) + } + } catch (NumberFormatException e) { + project.logger.info("versionCode: '$versionCode' isn't an Integer, using the plain value.") + } + + // The Sentry server distinguishes source maps by release (`--release` in the command + // below) and distribution identifier (`--dist` below). Give the task a unique name + // based on where we're uploading to. + def nameCliTask = "${bundleTask.name}_SentryUpload_${releaseName}_${versionCode}" + def nameModulesTask = "${bundleTask.name}_SentryCollectModules_${releaseName}_${versionCode}" + + // If several outputs have the same releaseName and versionCode, we'd do the exact same + // upload for each of them. No need to repeat. + try { tasks.named(nameCliTask); return } catch (Exception e) {} + + /** Upload source map file to the sentry server via CLI call. */ + def cliTask = tasks.create(nameCliTask) { + onlyIf { shouldSentryAutoUploadGeneral() } + description = "upload debug symbols to sentry" + group = 'sentry.io' + + def extraArgs = [] + + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + def copyDebugIdScript = config.copyDebugIdScript + ? file(config.copyDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/copy-debugid.js" + def hasSourceMapDebugIdScript = config.hasSourceMapDebugIdScript + ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/has-sourcemap-debugid.js" + + doFirst { + def injected = project.objects.newInstance(InjectedExecOps) + // Copy Debug ID from packager source map to Hermes composed source map + injected.execOps.exec { + def args = ["node", + copyDebugIdScript, + packagerSourcemapOutput, + sourcemapOutput] + def osCompatibilityCopyCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : [] + commandLine(*osCompatibilityCopyCommand, *args) + } + + // Add release and dist for backward compatibility if no Debug ID detected in output soruce map + def process = ["node", hasSourceMapDebugIdScript, sourcemapOutput].execute(null, new File("$reactRoot")) + def exitValue = process.waitFor() + project.logger.lifecycle("Check generated source map for Debug ID: ${process.text}") + + project.logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") + extraArgs.addAll([ + "--release", releaseName, + "--dist", versionCode + ]) + } + + doLast { + def injected = project.objects.newInstance(InjectedExecOps) + injected.execOps.exec { + workingDir reactRoot + + def propertiesFile = config.sentryProperties + ? config.sentryProperties + : "$reactRoot/android/sentry.properties" + + if (config.flavorAware) { + propertiesFile = "$reactRoot/android/sentry-${variant}.properties" + project.logger.info("For $variant using: $propertiesFile") + } else { + environment("SENTRY_PROPERTIES", propertiesFile) + } + + Properties sentryProps = new Properties() + try { + sentryProps.load(new FileInputStream(propertiesFile)) + } catch (FileNotFoundException e) { + project.logger.info("file not found '$propertiesFile' for '$variant'") + } + + def cliPackage = resolveSentryCliPackagePath(reactRoot) + def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") + + // fix path separator for Windows + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cliExecutable = cliExecutable.replaceAll("/", "\\\\") + } + + // + // based on: + // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs + // + def args = [cliExecutable] + + args.addAll(!config.logLevel ? [] : [ + "--log-level", config.logLevel // control verbosity of the output + ]) + args.addAll(!config.flavorAware ? [] : [ + "--url", sentryProps.get("defaults.url"), + "--auth-token", sentryProps.get("auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN") + ]) + args.addAll(["react-native", "gradle", + "--bundle", bundleOutput, // The path to a bundle that should be uploaded. + "--sourcemap", sourcemapOutput // The path to a sourcemap that should be uploaded. + ]) + args.addAll(!config.flavorAware ? [] : [ + "--org", sentryProps.get("defaults.org"), + "--project", sentryProps.get("defaults.project") + ]) + + args.addAll(extraArgs) + + project.logger.lifecycle("Sentry-CLI arguments: ${args}") + def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] + if (!System.getenv('SENTRY_DOTENV_PATH') && file("$reactRoot/.env.sentry-build-plugin").exists()) { + environment('SENTRY_DOTENV_PATH', "$reactRoot/.env.sentry-build-plugin") + } + commandLine(*osCompatibility, *args) + } + } + + enabled true + } + + modulesTask = tasks.create(nameModulesTask, Exec) { + description = "collect javascript modules from bundle source map" + group = 'sentry.io' + + workingDir reactRoot + + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + + def collectModulesScript = config.collectModulesScript + ? file(config.collectModulesScript).getAbsolutePath() + : "$sentryPackage/dist/js/tools/collectModules.js" + def modulesPaths = config.modulesPaths + ? config.modulesPaths.join(',') + : "$reactRoot/node_modules" + def args = ["node", + collectModulesScript, + sourcemapOutput, + modulesOutput, + modulesPaths + ] + + if ((new File(collectModulesScript)).exists()) { + project.logger.info("Sentry-CollectModules arguments: ${args}") + commandLine(*args) + + def skip = config.skipCollectModules + ? config.skipCollectModules == true + : false + enabled !skip + } else { + project.logger.info("collectModulesScript not found: $collectModulesScript") + enabled false + } + } + + // chain the upload tasks so they run sequentially in order to run + // the cliCleanUpTask after the final upload task is run + if (previousCliTask != null) { + previousCliTask.finalizedBy cliTask + } else { + bundleTask.finalizedBy cliTask } - } catch (NumberFormatException e) { - project.logger.info("versionCode: '$versionCode' isn't an Integer, using the plain value.") - } - - // The Sentry server distinguishes source maps by release (`--release` in the command - // below) and distribution identifier (`--dist` below). Give the task a unique name - // based on where we're uploading to. - def nameCliTask = "${bundleTask.name}_SentryUpload_${releaseName}_${versionCode}" - def nameModulesTask = "${bundleTask.name}_SentryCollectModules_${releaseName}_${versionCode}" - - // If several outputs have the same releaseName and versionCode, we'd do the exact same - // upload for each of them. No need to repeat. - try { tasks.named(nameCliTask); return } catch (Exception e) {} - - /** Upload source map file to the sentry server via CLI call. */ - def cliTask = tasks.create(nameCliTask) { - onlyIf { shouldSentryAutoUploadGeneral() } - description = "upload debug symbols to sentry" - group = 'sentry.io' - - def extraArgs = [] - - def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) - def copyDebugIdScript = config.copyDebugIdScript - ? file(config.copyDebugIdScript).getAbsolutePath() - : "$sentryPackage/scripts/copy-debugid.js" - def hasSourceMapDebugIdScript = config.hasSourceMapDebugIdScript - ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() - : "$sentryPackage/scripts/has-sourcemap-debugid.js" - - doFirst { - // Copy Debug ID from packager source map to Hermes composed source map - exec { - def args = ["node", - copyDebugIdScript, - packagerSourcemapOutput, - sourcemapOutput] - def osCompatibilityCopyCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : [] - commandLine(*osCompatibilityCopyCommand, *args) - } - - // Add release and dist for backward compatibility if no Debug ID detected in output soruce map - def process = ["node", hasSourceMapDebugIdScript, sourcemapOutput].execute(null, new File("$reactRoot")) - def exitValue = process.waitFor() - project.logger.lifecycle("Check generated source map for Debug ID: ${process.text}") - - project.logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") - extraArgs.addAll([ - "--release", releaseName, - "--dist", versionCode - ]) - } - - doLast { - exec { - workingDir reactRoot - - def propertiesFile = config.sentryProperties - ? config.sentryProperties - : "$reactRoot/android/sentry.properties" - - if (config.flavorAware) { - propertiesFile = "$reactRoot/android/sentry-${variant}.properties" - project.logger.info("For $variant using: $propertiesFile") - } else { - environment("SENTRY_PROPERTIES", propertiesFile) - } - - Properties sentryProps = new Properties() - try { - sentryProps.load(new FileInputStream(propertiesFile)) - } catch (FileNotFoundException e) { - project.logger.info("file not found '$propertiesFile' for '$variant'") - } - - def cliPackage = resolveSentryCliPackagePath(reactRoot) - def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") - - // fix path separator for Windows - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - cliExecutable = cliExecutable.replaceAll("/", "\\\\") - } - - // - // based on: - // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs - // - def args = [cliExecutable] - - args.addAll(!config.logLevel ? [] : [ - "--log-level", config.logLevel // control verbosity of the output - ]) - args.addAll(!config.flavorAware ? [] : [ - "--url", sentryProps.get("defaults.url"), - "--auth-token", sentryProps.get("auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN") - ]) - args.addAll(["react-native", "gradle", - "--bundle", bundleOutput, // The path to a bundle that should be uploaded. - "--sourcemap", sourcemapOutput // The path to a sourcemap that should be uploaded. - ]) - args.addAll(!config.flavorAware ? [] : [ - "--org", sentryProps.get("defaults.org"), - "--project", sentryProps.get("defaults.project") - ]) - - args.addAll(extraArgs) - - project.logger.lifecycle("Sentry-CLI arguments: ${args}") - def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] - if (!System.getenv('SENTRY_DOTENV_PATH') && file("$reactRoot/.env.sentry-build-plugin").exists()) { - environment('SENTRY_DOTENV_PATH', "$reactRoot/.env.sentry-build-plugin") - } - commandLine(*osCompatibility, *args) - } - } - - enabled true - } - - modulesTask = tasks.create(nameModulesTask, Exec) { - description = "collect javascript modules from bundle source map" - group = 'sentry.io' - - workingDir reactRoot - - def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) - - def collectModulesScript = config.collectModulesScript - ? file(config.collectModulesScript).getAbsolutePath() - : "$sentryPackage/dist/js/tools/collectModules.js" - def modulesPaths = config.modulesPaths - ? config.modulesPaths.join(',') - : "$reactRoot/node_modules" - def args = ["node", - collectModulesScript, - sourcemapOutput, - modulesOutput, - modulesPaths - ] - - if ((new File(collectModulesScript)).exists()) { - project.logger.info("Sentry-CollectModules arguments: ${args}") - commandLine(*args) - - def skip = config.skipCollectModules - ? config.skipCollectModules == true - : false - enabled !skip - } else { - project.logger.info("collectModulesScript not found: $collectModulesScript") - enabled false - } - } - - // chain the upload tasks so they run sequentially in order to run - // the cliCleanUpTask after the final upload task is run - if (previousCliTask != null) { - previousCliTask.finalizedBy cliTask - } else { - bundleTask.finalizedBy cliTask - } - previousCliTask = cliTask - cliTask.finalizedBy modulesTask + previousCliTask = cliTask + cliTask.finalizedBy modulesTask } def modulesCleanUpTask = tasks.create(name: nameModulesCleanup, type: Delete) { @@ -265,9 +272,9 @@ project.afterEvaluate { } def packageTasks = tasks.findAll { - task -> ( - "package${applicationVariant}".equalsIgnoreCase(task.name) - || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name) + task -> ( + "package${applicationVariant}".equalsIgnoreCase(task.name) + || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name) ) && task.enabled } packageTasks.each { packageTask -> @@ -403,9 +410,9 @@ static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) { } /** Extract bundle and sourcemap paths from bundle task props. - * Based on https://github.dev/facebook/react-native/blob/473eb1dd870a4f62c4ebcba27e12bde1e99e3d07/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt#L109 - * Output source map path is the same for both Hermes and JSC. - */ + * Based on https://github.dev/facebook/react-native/blob/473eb1dd870a4f62c4ebcba27e12bde1e99e3d07/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt#L109 + * Output source map path is the same for both Hermes and JSC. + */ static extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger) { def props = bundleTask.getProperties() def bundleAssetName = props.bundleAssetName?.get() From f15c72463e942212677596a839c63e772f291abf Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Oct 2025 11:51:15 +0200 Subject: [PATCH 2/4] Changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759f602ae2..18ce157665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- Adds support for Gradle 9 ([#5233](https://github.com/getsentry/sentry-react-native/pull/5233)) + ### Fixes - Fixes .env file loading in Expo sourcemap uploads ([#5210](https://github.com/getsentry/sentry-react-native/pull/5210)) From 0b87778b8879b643875ef6257331f9413c667e7e Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Oct 2025 12:06:39 +0200 Subject: [PATCH 3/4] Indentation restored --- packages/core/sentry.gradle | 369 ++++++++++++++++++------------------ 1 file changed, 181 insertions(+), 188 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 16d3a650c9..a461813e8f 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -4,20 +4,15 @@ import java.util.regex.Matcher import java.util.regex.Pattern project.ext.shouldSentryAutoUploadNative = { -> - return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true' + return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true' } project.ext.shouldSentryAutoUploadGeneral = { -> - return System.getenv('SENTRY_DISABLE_AUTO_UPLOAD') != 'true' + return System.getenv('SENTRY_DISABLE_AUTO_UPLOAD') != 'true' } project.ext.shouldSentryAutoUpload = { -> - return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() -} - -interface InjectedExecOps { - @Inject //@javax.inject.Inject - ExecOperations getExecOps() + return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() } def config = project.hasProperty("sentryCli") ? project.sentryCli : []; @@ -87,181 +82,179 @@ project.afterEvaluate { def nameModulesCleanup = "${bundleTask.name}_SentryCollectModulesCleanUp" // Upload the source map several times if necessary: once for each release and versionCode. currentVariants.each { key, currentVariant -> - def variant = currentVariant[0] - def releaseName = currentVariant[1] - def versionCode = currentVariant[2] - applicationVariant = currentVariant[3] - - try { - if (versionCode instanceof String) { - versionCode = Integer.parseInt(versionCode) - versionCode = Math.abs(versionCode) - } - } catch (NumberFormatException e) { - project.logger.info("versionCode: '$versionCode' isn't an Integer, using the plain value.") - } - - // The Sentry server distinguishes source maps by release (`--release` in the command - // below) and distribution identifier (`--dist` below). Give the task a unique name - // based on where we're uploading to. - def nameCliTask = "${bundleTask.name}_SentryUpload_${releaseName}_${versionCode}" - def nameModulesTask = "${bundleTask.name}_SentryCollectModules_${releaseName}_${versionCode}" - - // If several outputs have the same releaseName and versionCode, we'd do the exact same - // upload for each of them. No need to repeat. - try { tasks.named(nameCliTask); return } catch (Exception e) {} - - /** Upload source map file to the sentry server via CLI call. */ - def cliTask = tasks.create(nameCliTask) { - onlyIf { shouldSentryAutoUploadGeneral() } - description = "upload debug symbols to sentry" - group = 'sentry.io' - - def extraArgs = [] - - def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) - def copyDebugIdScript = config.copyDebugIdScript - ? file(config.copyDebugIdScript).getAbsolutePath() - : "$sentryPackage/scripts/copy-debugid.js" - def hasSourceMapDebugIdScript = config.hasSourceMapDebugIdScript - ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() - : "$sentryPackage/scripts/has-sourcemap-debugid.js" - - doFirst { - def injected = project.objects.newInstance(InjectedExecOps) - // Copy Debug ID from packager source map to Hermes composed source map - injected.execOps.exec { - def args = ["node", - copyDebugIdScript, - packagerSourcemapOutput, - sourcemapOutput] - def osCompatibilityCopyCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : [] - commandLine(*osCompatibilityCopyCommand, *args) - } - - // Add release and dist for backward compatibility if no Debug ID detected in output soruce map - def process = ["node", hasSourceMapDebugIdScript, sourcemapOutput].execute(null, new File("$reactRoot")) - def exitValue = process.waitFor() - project.logger.lifecycle("Check generated source map for Debug ID: ${process.text}") - - project.logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") - extraArgs.addAll([ - "--release", releaseName, - "--dist", versionCode - ]) - } - - doLast { - def injected = project.objects.newInstance(InjectedExecOps) - injected.execOps.exec { - workingDir reactRoot - - def propertiesFile = config.sentryProperties - ? config.sentryProperties - : "$reactRoot/android/sentry.properties" - - if (config.flavorAware) { - propertiesFile = "$reactRoot/android/sentry-${variant}.properties" - project.logger.info("For $variant using: $propertiesFile") - } else { - environment("SENTRY_PROPERTIES", propertiesFile) - } - - Properties sentryProps = new Properties() - try { - sentryProps.load(new FileInputStream(propertiesFile)) - } catch (FileNotFoundException e) { - project.logger.info("file not found '$propertiesFile' for '$variant'") - } - - def cliPackage = resolveSentryCliPackagePath(reactRoot) - def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") - - // fix path separator for Windows - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - cliExecutable = cliExecutable.replaceAll("/", "\\\\") - } - - // - // based on: - // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs - // - def args = [cliExecutable] - - args.addAll(!config.logLevel ? [] : [ - "--log-level", config.logLevel // control verbosity of the output - ]) - args.addAll(!config.flavorAware ? [] : [ - "--url", sentryProps.get("defaults.url"), - "--auth-token", sentryProps.get("auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN") - ]) - args.addAll(["react-native", "gradle", - "--bundle", bundleOutput, // The path to a bundle that should be uploaded. - "--sourcemap", sourcemapOutput // The path to a sourcemap that should be uploaded. - ]) - args.addAll(!config.flavorAware ? [] : [ - "--org", sentryProps.get("defaults.org"), - "--project", sentryProps.get("defaults.project") - ]) - - args.addAll(extraArgs) - - project.logger.lifecycle("Sentry-CLI arguments: ${args}") - def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] - if (!System.getenv('SENTRY_DOTENV_PATH') && file("$reactRoot/.env.sentry-build-plugin").exists()) { - environment('SENTRY_DOTENV_PATH', "$reactRoot/.env.sentry-build-plugin") - } - commandLine(*osCompatibility, *args) - } - } - - enabled true - } - - modulesTask = tasks.create(nameModulesTask, Exec) { - description = "collect javascript modules from bundle source map" - group = 'sentry.io' - - workingDir reactRoot - - def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) - - def collectModulesScript = config.collectModulesScript - ? file(config.collectModulesScript).getAbsolutePath() - : "$sentryPackage/dist/js/tools/collectModules.js" - def modulesPaths = config.modulesPaths - ? config.modulesPaths.join(',') - : "$reactRoot/node_modules" - def args = ["node", - collectModulesScript, - sourcemapOutput, - modulesOutput, - modulesPaths - ] - - if ((new File(collectModulesScript)).exists()) { - project.logger.info("Sentry-CollectModules arguments: ${args}") - commandLine(*args) - - def skip = config.skipCollectModules - ? config.skipCollectModules == true - : false - enabled !skip - } else { - project.logger.info("collectModulesScript not found: $collectModulesScript") - enabled false - } - } - - // chain the upload tasks so they run sequentially in order to run - // the cliCleanUpTask after the final upload task is run - if (previousCliTask != null) { - previousCliTask.finalizedBy cliTask - } else { - bundleTask.finalizedBy cliTask + def variant = currentVariant[0] + def releaseName = currentVariant[1] + def versionCode = currentVariant[2] + applicationVariant = currentVariant[3] + + try { + if (versionCode instanceof String) { + versionCode = Integer.parseInt(versionCode) + versionCode = Math.abs(versionCode) } - previousCliTask = cliTask - cliTask.finalizedBy modulesTask + } catch (NumberFormatException e) { + project.logger.info("versionCode: '$versionCode' isn't an Integer, using the plain value.") + } + + // The Sentry server distinguishes source maps by release (`--release` in the command + // below) and distribution identifier (`--dist` below). Give the task a unique name + // based on where we're uploading to. + def nameCliTask = "${bundleTask.name}_SentryUpload_${releaseName}_${versionCode}" + def nameModulesTask = "${bundleTask.name}_SentryCollectModules_${releaseName}_${versionCode}" + + // If several outputs have the same releaseName and versionCode, we'd do the exact same + // upload for each of them. No need to repeat. + try { tasks.named(nameCliTask); return } catch (Exception e) {} + + /** Upload source map file to the sentry server via CLI call. */ + def cliTask = tasks.create(nameCliTask) { + onlyIf { shouldSentryAutoUploadGeneral() } + description = "upload debug symbols to sentry" + group = 'sentry.io' + + def extraArgs = [] + + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + def copyDebugIdScript = config.copyDebugIdScript + ? file(config.copyDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/copy-debugid.js" + def hasSourceMapDebugIdScript = config.hasSourceMapDebugIdScript + ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/has-sourcemap-debugid.js" + + doFirst { + // Copy Debug ID from packager source map to Hermes composed source map + exec { + def args = ["node", + copyDebugIdScript, + packagerSourcemapOutput, + sourcemapOutput] + def osCompatibilityCopyCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : [] + commandLine(*osCompatibilityCopyCommand, *args) + } + + // Add release and dist for backward compatibility if no Debug ID detected in output soruce map + def process = ["node", hasSourceMapDebugIdScript, sourcemapOutput].execute(null, new File("$reactRoot")) + def exitValue = process.waitFor() + project.logger.lifecycle("Check generated source map for Debug ID: ${process.text}") + + project.logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") + extraArgs.addAll([ + "--release", releaseName, + "--dist", versionCode + ]) + } + + doLast { + exec { + workingDir reactRoot + + def propertiesFile = config.sentryProperties + ? config.sentryProperties + : "$reactRoot/android/sentry.properties" + + if (config.flavorAware) { + propertiesFile = "$reactRoot/android/sentry-${variant}.properties" + project.logger.info("For $variant using: $propertiesFile") + } else { + environment("SENTRY_PROPERTIES", propertiesFile) + } + + Properties sentryProps = new Properties() + try { + sentryProps.load(new FileInputStream(propertiesFile)) + } catch (FileNotFoundException e) { + project.logger.info("file not found '$propertiesFile' for '$variant'") + } + + def cliPackage = resolveSentryCliPackagePath(reactRoot) + def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") + + // fix path separator for Windows + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cliExecutable = cliExecutable.replaceAll("/", "\\\\") + } + + // + // based on: + // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs + // + def args = [cliExecutable] + + args.addAll(!config.logLevel ? [] : [ + "--log-level", config.logLevel // control verbosity of the output + ]) + args.addAll(!config.flavorAware ? [] : [ + "--url", sentryProps.get("defaults.url"), + "--auth-token", sentryProps.get("auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN") + ]) + args.addAll(["react-native", "gradle", + "--bundle", bundleOutput, // The path to a bundle that should be uploaded. + "--sourcemap", sourcemapOutput // The path to a sourcemap that should be uploaded. + ]) + args.addAll(!config.flavorAware ? [] : [ + "--org", sentryProps.get("defaults.org"), + "--project", sentryProps.get("defaults.project") + ]) + + args.addAll(extraArgs) + + project.logger.lifecycle("Sentry-CLI arguments: ${args}") + def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] + if (!System.getenv('SENTRY_DOTENV_PATH') && file("$reactRoot/.env.sentry-build-plugin").exists()) { + environment('SENTRY_DOTENV_PATH', "$reactRoot/.env.sentry-build-plugin") + } + commandLine(*osCompatibility, *args) + } + } + + enabled true + } + + modulesTask = tasks.create(nameModulesTask, Exec) { + description = "collect javascript modules from bundle source map" + group = 'sentry.io' + + workingDir reactRoot + + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + + def collectModulesScript = config.collectModulesScript + ? file(config.collectModulesScript).getAbsolutePath() + : "$sentryPackage/dist/js/tools/collectModules.js" + def modulesPaths = config.modulesPaths + ? config.modulesPaths.join(',') + : "$reactRoot/node_modules" + def args = ["node", + collectModulesScript, + sourcemapOutput, + modulesOutput, + modulesPaths + ] + + if ((new File(collectModulesScript)).exists()) { + project.logger.info("Sentry-CollectModules arguments: ${args}") + commandLine(*args) + + def skip = config.skipCollectModules + ? config.skipCollectModules == true + : false + enabled !skip + } else { + project.logger.info("collectModulesScript not found: $collectModulesScript") + enabled false + } + } + + // chain the upload tasks so they run sequentially in order to run + // the cliCleanUpTask after the final upload task is run + if (previousCliTask != null) { + previousCliTask.finalizedBy cliTask + } else { + bundleTask.finalizedBy cliTask + } + previousCliTask = cliTask + cliTask.finalizedBy modulesTask } def modulesCleanUpTask = tasks.create(name: nameModulesCleanup, type: Delete) { @@ -272,9 +265,9 @@ project.afterEvaluate { } def packageTasks = tasks.findAll { - task -> ( - "package${applicationVariant}".equalsIgnoreCase(task.name) - || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name) + task -> ( + "package${applicationVariant}".equalsIgnoreCase(task.name) + || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name) ) && task.enabled } packageTasks.each { packageTask -> @@ -410,9 +403,9 @@ static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) { } /** Extract bundle and sourcemap paths from bundle task props. - * Based on https://github.dev/facebook/react-native/blob/473eb1dd870a4f62c4ebcba27e12bde1e99e3d07/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt#L109 - * Output source map path is the same for both Hermes and JSC. - */ + * Based on https://github.dev/facebook/react-native/blob/473eb1dd870a4f62c4ebcba27e12bde1e99e3d07/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt#L109 + * Output source map path is the same for both Hermes and JSC. + */ static extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger) { def props = bundleTask.getProperties() def bundleAssetName = props.bundleAssetName?.get() From cff9d35cd9c2ea33acc346ea0cd308cb2b15df12 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Oct 2025 12:08:19 +0200 Subject: [PATCH 4/4] fix --- packages/core/sentry.gradle | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index a461813e8f..2f0f9e15e6 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -15,6 +15,11 @@ project.ext.shouldSentryAutoUpload = { -> return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() } +interface InjectedExecOps { + @Inject //@javax.inject.Inject + ExecOperations getExecOps() +} + def config = project.hasProperty("sentryCli") ? project.sentryCli : []; // gradle.projectsEvaluated doesn't work with --configure-on-demand @@ -122,9 +127,10 @@ project.afterEvaluate { ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() : "$sentryPackage/scripts/has-sourcemap-debugid.js" + def injected = project.objects.newInstance(InjectedExecOps) doFirst { // Copy Debug ID from packager source map to Hermes composed source map - exec { + injected.execOps.exec { def args = ["node", copyDebugIdScript, packagerSourcemapOutput, @@ -146,7 +152,7 @@ project.afterEvaluate { } doLast { - exec { + injected.execOps.exec { workingDir reactRoot def propertiesFile = config.sentryProperties