mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-10-30 18:34:19 +00:00
# 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
385 lines
18 KiB
Groovy
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
|
|
}
|