diff --git a/.changeset/gradle-plugin-native-symbols.md b/.changeset/gradle-plugin-native-symbols.md new file mode 100644 index 00000000..324b79a7 --- /dev/null +++ b/.changeset/gradle-plugin-native-symbols.md @@ -0,0 +1,5 @@ +--- +'posthog-android-gradle-plugin': minor +--- + +Upload native (`.so`) debug symbols via `posthog-cli symbol-sets upload` (>= 0.7.32), so native crash stack traces can be symbolicated. A new `uploadPostHogNativeSymbols` task reads the variant's unstripped merged native libs and runs automatically after `assemble`/`install`/`bundle` when the app module builds native code; apps that only bundle prebuilt `.so` files from dependencies can invoke it explicitly. Unlike mapping upload, the task also runs for non-minified variants. diff --git a/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogAndroidGradlePlugin.kt b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogAndroidGradlePlugin.kt index b94b1f1b..edcc616c 100644 --- a/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogAndroidGradlePlugin.kt +++ b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogAndroidGradlePlugin.kt @@ -32,6 +32,11 @@ internal class PostHogAndroidGradlePlugin : Plugin { project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java) androidComponentsExt.onVariants { variant -> + // Native symbol upload is independent of minification: native + // crashes need `.so` debug symbols whether or not the JVM side + // is obfuscated. + registerNativeSymbolsUpload(project, variant) + if (!variant.isMinifyEnabled) { return@onVariants } @@ -65,6 +70,40 @@ internal class PostHogAndroidGradlePlugin : Plugin { } } + private fun registerNativeSymbolsUpload( + project: Project, + variant: ApplicationVariant, + ) { + val primaryOutput = variant.outputs.firstOrNull() + val uploadTask = + PostHogUploadNativeSymbolsTask.register( + project = project, + // The unstripped libraries as built; AGP strips them for + // packaging in a later task. The subdirectory layout varies + // across AGP versions, so point at the variant root and let + // the CLI scan recursively. + nativeLibsDirectory = + project.layout.buildDirectory + .dir("intermediates/merged_native_libs/${variant.name}"), + taskSuffix = variant.name.capitalizeUS(), + releaseName = variant.applicationId, + releaseVersion = primaryOutput?.versionName?.map { it.orEmpty() }, + build = primaryOutput?.versionCode, + ) + + project.afterEvaluate { + PostHogTasksProvider.getMergeNativeLibsTask(project, variant.name)?.let { merge -> + uploadTask.configure { dependsOn(merge) } + } + // Auto-upload alongside assemble/install/bundle only when the app + // module builds native code itself. Apps that only bundle prebuilt + // `.so` files from dependencies can run the task explicitly. + if (variant.externalNativeBuild != null) { + uploadTask.hookWithAssembleTasks(project, variant) + } + } + } + private fun assetsWiredWithDirectories( variant: ApplicationVariant, task: TaskProvider, diff --git a/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogTasksProvider.kt b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogTasksProvider.kt index 3cc1e8ac..2fb8714c 100644 --- a/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogTasksProvider.kt +++ b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogTasksProvider.kt @@ -31,6 +31,18 @@ internal object PostHogTasksProvider { return project.findTask(tasks) } + /** + * Returns the merge native libs task for the given project and variant — + * the producer of the unstripped `.so` intermediates. + * + * @return the task or null otherwise + */ + @JvmStatic + fun getMergeNativeLibsTask( + project: Project, + variantName: String, + ): TaskProvider? = project.findTask(listOf("merge${variantName.capitalized}NativeLibs")) + /** * Returns the pre bundle task for the given project and variant. * diff --git a/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogUploadNativeSymbolsTask.kt b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogUploadNativeSymbolsTask.kt new file mode 100644 index 00000000..a7236914 --- /dev/null +++ b/posthog-android-gradle-plugin/src/main/kotlin/com/posthog/android/PostHogUploadNativeSymbolsTask.kt @@ -0,0 +1,101 @@ +// Portions of this file are derived from getsentry/sentry-android-gradle-plugin +// Copyright (c) 2020 Sentry +// Licensed under the MIT License: https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/LICENSE + +package com.posthog.android + +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault + +/** + * Uploads the variant's native (`.so`) debug symbols to PostHog via + * `posthog-cli symbol-sets upload`, so native crash stack frames can be + * symbolicated server-side. + * + * The upload reads the *merged* native libs intermediates — the unstripped + * libraries as built, before AGP strips them for packaging. The CLI scans the + * directory and uploads every library that carries debug info and a GNU build + * id, reporting (but not failing on) pre-stripped libraries from dependencies. + */ +@DisableCachingByDefault(because = "Uploads should not be cached") +public abstract class PostHogUploadNativeSymbolsTask : PostHogCliExecTask() { + init { + description = "Uploads native (.so) debug symbols to PostHog" + + // Uploads have no outputs; without this the task would always rerun. + outputs.upToDateWhen { true } + } + + /** + * Root of the variant's merged native libs intermediates. Not tracked as a + * task input: uploads are gated by [outputs.upToDateWhen] and snapshotting + * every `.so` would only slow the build. + */ + @get:Internal + public abstract val nativeLibsDirectory: DirectoryProperty + + @get:Input + @get:Optional + public abstract val releaseName: Property + + @get:Input + @get:Optional + public abstract val releaseVersion: Property + + @get:Input + @get:Optional + public abstract val build: Property + + override fun getArguments(args: MutableList) { + args.add("symbol-sets") + args.add("upload") + args.add("--directory") + args.add(nativeLibsDirectory.get().asFile.toString()) + releaseName.orNull?.takeIf { it.isNotEmpty() }?.let { + args.add("--release-name") + args.add(it) + } + releaseVersion.orNull?.takeIf { it.isNotEmpty() }?.let { + args.add("--release-version") + args.add(it) + } + build.orNull?.takeIf { it > 0 }?.let { + args.add("--build") + args.add(it.toString()) + } + } + + internal companion object { + fun register( + project: Project, + nativeLibsDirectory: Provider, + taskSuffix: String = "", + releaseName: Provider? = null, + releaseVersion: Provider? = null, + build: Provider? = null, + ): TaskProvider { + return project.tasks.register( + "uploadPostHogNativeSymbols$taskSuffix", + PostHogUploadNativeSymbolsTask::class.java, + ) { + workingDir(project.rootDir) + this.nativeLibsDirectory.set(nativeLibsDirectory) + releaseName?.let { this.releaseName.set(it) } + releaseVersion?.let { this.releaseVersion.set(it) } + build?.let { this.build.set(it) } + resolvePostHogDotenvFile(project)?.let { this.postHogDotenvFile.set(it) } + onlyIf("the variant has native libraries") { + val dir = (it as PostHogUploadNativeSymbolsTask).nativeLibsDirectory.get().asFile + dir.walkTopDown().any { file -> file.extension == "so" } + } + } + } + } +}