Files
zeekay 26a9531f7b merge: upstream/main (sync, brand preserved)
# Conflicts:
#	.github/workflows/check-insights-major-version.yml
#	.github/workflows/check-posthog-major-version.yml
#	.github/workflows/ci-security.yaml
#	.github/workflows/validate-versioning.yml
#	AGENTS.md
#	AGENTS.md~upstream_main
#	packages/ai/src/otel/capture.ts
#	packages/ai/src/otel/mappers/aiSdk.ts
#	packages/ai/src/otel/mappers/index.ts
#	packages/ai/src/otel/types.ts
#	packages/core/src/__tests__/posthog.init.spec.ts
#	packages/react-native/src/PostHogProvider.tsx
#	packages/react/src/context/PostHogContext.ts
#	packages/react/src/context/PostHogProvider.tsx
#	tooling/eslint-plugin-insights/no-unsafe-web-global.js
2026-06-02 10:52:56 -07:00

385 lines
18 KiB
Groovy

// adapted from https://github.com/getsentry/sentry-react-native/blob/e76d0d388228437e82f235546de00f4e748fcbda/packages/core/sentry.gradle
import org.apache.tools.ant.taskdefs.condition.Os
import java.util.regex.Matcher
import java.util.regex.Pattern
interface InjectedExecOps {
@Inject //@javax.inject.Inject
ExecOperations getExecOps()
}
plugins.withId('com.android.application') {
def androidComponents = extensions.getByName("androidComponents")
androidComponents.onVariants(androidComponents.selector().all()) { v ->
if (!v.name.toLowerCase().contains("debug")) {
// separately we then hook into the bundle task of react native to inject
// sourcemap generation parameters. In case for whatever reason no release
// was found for the asset folder we just bail.
def bundleTasks = tasks.findAll { task -> (task.name.startsWith("createBundle") || task.name.startsWith("bundle")) && task.name.endsWith("JsAndAssets") && !task.name.contains("Debug") && task.enabled }
// this is the task that calls react-native bundle {params}
bundleTasks.each { bundleTask ->
def shouldCleanUp
def sourcemapOutput
def bundleOutput
def packagerSourcemapOutput
def bundleCommand
def props = bundleTask.getProperties()
def reactRoot = props.get("workingDir")
if (reactRoot == null) {
reactRoot = props.get("root").get() // RN 0.71 and above
}
(shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = forceSourceMapOutputFromBundleTask(bundleTask)
// Lets leave this here if we need to debug
// println bundleTask.properties
// .sort{it.key}
// .collect{it}
// .findAll{!['class', 'active'].contains(it.key)}
// .join('\n')
def currentVariants = extractCurrentVariants(bundleTask, v)
if (currentVariants == null) return
def previousCliTask = null
def applicationVariant = null
def nameCleanup = "${bundleTask.name}_InsightsUploadCleanUp"
// 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]
def appId = currentVariant[4]
def versionName = currentVariant[5]
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.")
}
def nameCliTask = "${bundleTask.name}_InsightsUpload_${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 Insights server via CLI call. */
def cliTask = tasks.register(nameCliTask) {
description = "upload sourcemaps to Insights"
group = 'insights.hanzo.ai'
def extraArgs = []
if (appId != null && !appId.isEmpty()) {
extraArgs.addAll(["--release-name", appId.toString()])
}
if (versionName != null && !versionName.toString().isEmpty()) {
extraArgs.addAll(["--release-version", versionName.toString()])
}
if (versionCode != null) {
extraArgs.addAll(["--build", versionCode.toString()])
}
def injected = project.objects.newInstance(InjectedExecOps)
doFirst {
injected.execOps.exec {
workingDir reactRoot
def cliPackage = resolveInsightsCliPackagePath(reactRoot)
def args = [cliPackage]
args.addAll(["hermes", "clone",
"--minified-map-path", packagerSourcemapOutput, // The path to a sourcemap
"--composed-map-path", sourcemapOutput // The path of the composed source map
])
args.addAll(extraArgs)
project.logger.info("insights-cli clone arguments: ${args}")
def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : []
commandLine(*osCompatibility, *args)
}
}
doLast {
injected.execOps.exec {
workingDir reactRoot
def cliPackage = resolveInsightsCliPackagePath(reactRoot)
def args = [cliPackage]
def sourcemapDir = sourcemapOutput.getParent()
args.addAll(["hermes", "upload",
"--directory", sourcemapDir // The path to a sourcemap that should be uploaded.
])
args.addAll(extraArgs)
project.logger.info("insights-cli upload arguments: ${args}")
def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : []
commandLine(*osCompatibility, *args)
}
}
enabled true
}
// 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.configure { finalizedBy cliTask }
} else {
bundleTask.configure { finalizedBy cliTask }
}
previousCliTask = cliTask
}
/** Delete sourcemap files */
def cliCleanUpTask = tasks.register(nameCleanup, Delete) {
description = "clean up extra sourcemap"
group = 'insights.hanzo.ai'
delete sourcemapOutput
delete "$buildDir/intermediates/assets/release/index.android.bundle.map"
// react native default bundle dir
}
// register clean task extension
cliCleanUpTask.configure { onlyIf { shouldCleanUp } }
// due to chaining the last value of previousCliTask will be the final
// upload task, after which the cleanup can be done
previousCliTask.configure { finalizedBy cliCleanUpTask }
def packageTasks = tasks.matching {
task -> ("package${applicationVariant}".equalsIgnoreCase(task.name) || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name)) && task.enabled
}
}
}
}
}
def resolveInsightsCliPackagePath(reactRoot) {
// First, try to find @hanzo/cli folder from require.resolve
try {
def resolvedPath = new File(["node", "--print", "require.resolve('@hanzo/cli/package.json')"].execute(null, rootDir).text.trim()).getParentFile()
if (resolvedPath != null && resolvedPath.exists()) {
def runInsightsCliFile = new File(resolvedPath, "run-insights-cli.js")
if (runInsightsCliFile.exists()) {
project.logger.info("resolved insights-cli dynamically: `${runInsightsCliFile.getAbsolutePath()}`")
return runInsightsCliFile.getAbsolutePath()
}
}
} catch (Throwable ignored) {}
// Second, check if @hanzo/cli exists in $reactRoot/node_modules
def nodeModulesPath = new File("$reactRoot/node_modules/@hanzo/cli")
if (nodeModulesPath.exists()) {
def runInsightsCliFile = new File(nodeModulesPath, "run-insights-cli.js")
if (runInsightsCliFile.exists()) {
project.logger.info("resolved insights-cli hard-coded path (yarn or npm): `${runInsightsCliFile.getAbsolutePath()}`")
return runInsightsCliFile.getAbsolutePath()
}
}
// Third, check for pnpm installation with node_modules/.bin/insights-cli
def pnpmBinPath = new File("$reactRoot/node_modules/.bin/insights-cli")
if (pnpmBinPath.exists()) {
project.logger.info("resolved insights-cli hard-coded path (pnpm): `${pnpmBinPath.getAbsolutePath()}`")
return pnpmBinPath.getAbsolutePath()
}
// Fourth, check using npm root for local installation
try {
def npmRoot = ["npm", "root"].execute(null, rootDir).text.trim()
def npmInsightsCliPath = new File(npmRoot, "@hanzo/cli")
if (npmInsightsCliPath.exists()) {
def runInsightsCliFile = new File(npmInsightsCliPath, "run-insights-cli.js")
if (runInsightsCliFile.exists()) {
project.logger.info("resolved insights-cli via npm root: `${runInsightsCliFile.getAbsolutePath()}`")
return runInsightsCliFile.getAbsolutePath()
}
}
} catch (Throwable ignored) {}
// Fifth, check for global npm installation of @hanzo/cli
try {
def globalPrefix = ["npm", "prefix", "-g"].execute().text.trim()
def globalInsightsCliPath = new File(globalPrefix, "lib/node_modules/@hanzo/cli")
if (globalInsightsCliPath.exists()) {
def runInsightsCliFile = new File(globalInsightsCliPath, "run-insights-cli.js")
if (runInsightsCliFile.exists()) {
project.logger.info("resolved insights-cli from global npm installation: `${runInsightsCliFile.getAbsolutePath()}`")
return runInsightsCliFile.getAbsolutePath()
}
}
} catch (Throwable ignored) {}
// Finally, fallback to global install
project.logger.info("falling back to global insights-cli")
return "insights-cli"
}
def resolveInsightsReactNativeSDKPath(reactRoot) {
def resolvedPath = null
try {
resolvedPath = new File(["node", "--print", "require.resolve('@hanzo/insights-react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile();
} catch (Throwable ignored) {} // if the resolve fails we fallback to the default path
def packagePath = resolvedPath != null && resolvedPath.exists() ? resolvedPath.getAbsolutePath() : "$reactRoot/node_modules/@hanzo/insights-react-native"
return packagePath
}
/** Extract from arguments collection bundle and sourcemap files output names. */
static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) {
def bundleOutput = null
def sourcemapOutput = null
def packagerSourcemapOutput = null
// packagerBundleOutput doesn't exist, because packager output is overwritten by Hermes
cmdArgs.eachWithIndex { String arg, int i ->
if (arg == "--bundle-output") {
bundleOutput = cmdArgs[i + 1]
project.logger.info("--bundle-output: `${bundleOutput}`")
} else if (arg == "--sourcemap-output") {
sourcemapOutput = cmdArgs[i + 1]
packagerSourcemapOutput = sourcemapOutput
project.logger.info("--sourcemap-output param: `${sourcemapOutput}`")
}
}
// Best thing would be if we just had access to the local gradle variables here:
// https://github.com/facebook/react-native/blob/ff3b839e9a5a6c9e398a1327cde6dd49a3593092/react.gradle#L89-L97
// Now, the issue is that hermes builds have a different pipeline:
// `metro -> hermes -> compose-source-maps`, which then combines both intermediate sourcemaps into the final one.
// In this function here, we only grep through the first `metro` step, which only generates an intermediate sourcemap,
// which is wrong. We need the final one. Luckily, we can just generate the path from the `bundleOutput`, since
// the paths seem to be well defined.
// if sourcemapOutput is null, it means there's no source maps at all
// if hermes is enabled and has intermediates folder, we need to fix paths
// if hermes is disabled, sourcemapOutput is already ok
def enableHermes = project.ext.react.get("enableHermes", false);
project.logger.info("enableHermes: `${enableHermes}`")
if (bundleOutput != null && sourcemapOutput != null && enableHermes) {
// react-native < 0.60.1
def pattern = Pattern.compile("(/|\\\\)intermediates\\1sourcemaps\\1react\\1")
Matcher matcher = pattern.matcher(sourcemapOutput)
// if its intermediates/sourcemaps/react then it should be generated/sourcemaps/react
if (matcher.find()) {
project.logger.info("sourcemapOutput has the wrong path, let's fix it.")
// replacing from bundleOutput which is more reliable
sourcemapOutput = bundleOutput.replaceAll("(/|\\\\)generated\\1assets\\1react\\1", "\$1generated\$1sourcemaps\$1react\$1") + ".map"
project.logger.info("sourcemapOutput new path: `${sourcemapOutput}`")
}
}
// get the current bundle command, if not peresent use default plain "bundle"
// we use this later to decide how to upload source maps
def bundleCommand = project.ext.react.get("bundleCommand", "bundle")
return [bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand]
}
/** Extract bundle and sourcemap paths from bundle task props.
* Based on https://github.com/facebook/react-native/blob/93c17cd0c43ba7fe3bc09f547b64771984992fbb/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt
* 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()
if (bundleAssetName == null) {
return [null, null]
}
def bundleCommand = props.bundleCommand.get()
def bundleFile = new File(props.jsBundleDir.get().asFile.absolutePath, bundleAssetName)
def outputSourceMap = new File(props.jsSourceMapsDir.get().asFile.absolutePath, "${bundleAssetName}.map")
def packagerOutputSourceMap = new File(props.jsIntermediateSourceMapsDir.get().asFile.absolutePath, "${bundleAssetName}.packager.map")
logger.info("bundleFile: `${bundleFile}`")
logger.info("outputSourceMap: `${outputSourceMap}`")
logger.info("packagerOutputSourceMap: `${packagerOutputSourceMap}`")
return [bundleFile, outputSourceMap, packagerOutputSourceMap, bundleCommand]
}
/** Force Bundle task to produce sourcemap files if they are not pre-configured by user yet. */
def forceSourceMapOutputFromBundleTask(bundleTask) {
def props = bundleTask.getProperties()
def cmd = props.get("commandLine") as List<String>
def cmdArgs = props.get("args") as List<String>
def shouldCleanUp = false
def bundleOutput = null
def sourcemapOutput = null
def packagerSourcemapOutput = null
def bundleCommand = null
(bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger)
if (bundleOutput == null) {
(bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = extractBundleTaskArgumentsLegacy(cmdArgs, project)
}
if (sourcemapOutput == null) {
sourcemapOutput = bundleOutput + ".map"
cmd.addAll(["--sourcemap-output", sourcemapOutput])
cmdArgs.addAll(["--sourcemap-output", sourcemapOutput])
shouldCleanUp = true
bundleTask.setProperty("commandLine", cmd)
bundleTask.setProperty("args", cmdArgs)
project.logger.info("forced sourcemap file output for `${bundleTask.name}` task")
} else {
project.logger.info("Info: used pre-configured source map files: ${sourcemapOutput}")
}
return [shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand]
}
/** compose array with one item - current build flavor name */
static extractCurrentVariants(bundleTask, variant) {
// examples: bundleLocalReleaseJsAndAssets, createBundleYellowDebugJsAndAssets
def pattern = Pattern.compile("(?:create)?(?:B|b)undle([A-Z][A-Za-z0-9_]+)JsAndAssets")
def currentRelease = ""
Matcher matcher = pattern.matcher(bundleTask.name)
if (matcher.find()) {
def match = matcher.group(1)
currentRelease = match.substring(0, 1).toLowerCase() + match.substring(1)
}
def currentVariants = null
if (variant.name.equalsIgnoreCase(currentRelease)) {
currentVariants = [:]
def variantName = variant.name
variant.outputs.each { output ->
def defaultVersionCode = output.versionCode.getOrElse(0)
def versionCode = defaultVersionCode
def appId = variant.applicationId.get()
def versionName = output.versionName.getOrElse('') // may be empty if not set
def defaultReleaseName = "${appId}@${versionName}+${versionCode}"
def releaseName = defaultReleaseName
def outputName = output.baseName
if (currentVariants[outputName] == null) currentVariants[outputName] = []
currentVariants[outputName] = [outputName, releaseName, versionCode, variantName, appId, versionName]
}
}
return currentVariants
}