Stay up to date on everything big and small, from product updates to SDK changes.
Code mappings connect your stack traces to source code, enabling features like source context, suspect commits, and stack trace linking. Previously, you had to configure them one-by-one through the Sentry UI — tedious for monorepos with dozens of modules.
You can now upload code mappings in bulk using sentry-cli:
sentry-cli code-mappings upload ./mappings.json
The JSON file pairs stack trace roots with their source paths in your repository:
[
{"stackRoot": "io/sentry/android/core", "sourceRoot": "sentry-android-core/src/main/java/io/sentry/android/core"},
{"stackRoot": "io/sentry/opentelemetry", "sourceRoot": "sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry"},
{"stackRoot": "io/sentry/opentelemetry", "sourceRoot": "sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry"}
]
Multiple mappings can share the same stack root — useful when the same package prefix exists across multiple modules (like io/sentry/opentelemetry above).
For Android/JVM projects, you can generate the mappings JSON from your Gradle build. Add this task to your root build.gradle.kts:
abstract class GenerateSentryMappingsTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sourceDirs: ConfigurableFileCollection
@get:Internal
abstract val rootDir: DirectoryProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun generate() {
val outFile = outputFile.get().asFile
val rootPath = rootDir.get().asFile
val mappings = sourceDirs.files
.filter { it.exists() && !it.path.contains("/build/") }
.mapNotNull { srcDir ->
val packageRoot = findPackageRoot(srcDir) ?: return@mapNotNull null
val sourceRoot = packageRoot.relativeTo(rootPath).path
val stackRoot = packageRoot.relativeTo(srcDir).path
""" {"stackRoot": "$stackRoot", "sourceRoot": "$sourceRoot"}"""
}
val json = "[\n${mappings.joinToString(",\n")}\n]"
outFile.writeText(json)
logger.lifecycle("Generated ${mappings.size} code mappings to ${outFile.name}")
}
private fun findPackageRoot(root: File): File? {
var dir = root
while (true) {
val children = dir.listFiles() ?: return null
if (children.any { it.isFile }) return dir
val subdirs = children.filter { it.isDirectory }
if (subdirs.size == 1) {
dir = subdirs.single()
} else {
return if (subdirs.isNotEmpty()) dir else null
}
}
}
}
val generateMappings = tasks.register<GenerateSentryMappingsTask>("generateSentryMappings") {
rootDir.set(rootProject.layout.projectDirectory)
outputFile.set(rootProject.layout.projectDirectory.file("sentry-mappings.json"))
}
subprojects {
plugins.withId("com.android.library") {
val android = extensions.getByType(com.android.build.gradle.BaseExtension::class.java)
generateMappings.configure {
sourceDirs.from(android.sourceSets.getByName("main").java.srcDirs)
}
}
plugins.withId("com.android.application") {
val android = extensions.getByType(com.android.build.gradle.BaseExtension::class.java)
generateMappings.configure {
sourceDirs.from(android.sourceSets.getByName("main").java.srcDirs)
}
}
plugins.withType(JavaPlugin::class.java) {
val java = extensions.getByType(JavaPluginExtension::class.java)
generateMappings.configure {
sourceDirs.from(java.sourceSets.getByName("main").allJava.srcDirs)
}
}
}
Then run:
./gradlew generateSentryMappings
sentry-cli code-mappings upload ./sentry-mappings.json

Check our docs for more details.