Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gradle-plugin-native-symbols.md
Original file line number Diff line number Diff line change
@@ -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<Variant>` 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.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ internal class PostHogAndroidGradlePlugin : Plugin<Project> {
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
}
Expand Down Expand Up @@ -65,6 +70,40 @@ internal class PostHogAndroidGradlePlugin : Plugin<Project> {
}
}

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 <T : Task> assetsWiredWithDirectories(
variant: ApplicationVariant,
task: TaskProvider<T>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task>? = project.findTask(listOf("merge${variantName.capitalized}NativeLibs"))

/**
* Returns the pre bundle task for the given project and variant.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>

@get:Input
@get:Optional
public abstract val releaseVersion: Property<String>

@get:Input
@get:Optional
public abstract val build: Property<Int?>

override fun getArguments(args: MutableList<String>) {
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<org.gradle.api.file.Directory>,
taskSuffix: String = "",
releaseName: Provider<String>? = null,
releaseVersion: Provider<String>? = null,
build: Provider<Int?>? = null,
): TaskProvider<PostHogUploadNativeSymbolsTask> {
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" }
}
}
}
}
}
Loading