Set Up Snapshots
Set up snapshots for Android apps with the Sentry Android Gradle Plugin.
This feature is currently in Beta. Beta features are still in progress and may have bugs. We recognize the irony.
Set up Snapshots for your Android app with the Sentry Android Gradle Plugin. Generate snapshots locally or in your own CI using your preferred snapshot library, then upload the generated images to Sentry for image diffing, visual review, and GitHub status checks.
Snapshots require Android Gradle Plugin (AGP) 8.0 or higher. Earlier AGP versions are not supported.
Ensure the Sentry Android Gradle Plugin version 6.8.1 or higher is applied and configured.
Then enable snapshots in your build.gradle:
sentry {
snapshots {
enabled = true
}
}
sentry {
snapshots {
enabled = true
}
}
On Android you can either generate images from compose previews (recommended) or from an existing snapshot tool like Roborazzi or Paparazzi.
These methods are not mutually exclusive — you can generate and upload snapshots from your previews and any existing snapshot tests.
Paparazzi and ComposePreviewScanner automatically turns every @Preview composable in your project into a snapshot image, so you don't have to maintain explicit snapshot tests.
First, choose a Paparazzi version compatible with your project:
| Version | Gradle | compileSdk | JDK | compose-bom |
|---|---|---|---|---|
2.0.0-alpha05 | 9.x+ | 36 | 21+ | > 2025.05.00 |
1.3.5 | 8.x or 9.x | ≤ 35 | 17+ | ≤ 2025.04.00 |
Then apply the plugin:
plugins {
// existing plugins
id("app.cash.paparazzi") version "<paparazzi_version>"
}
plugins {
// existing plugins
id("app.cash.paparazzi") version "<paparazzi_version>"
}
Continue to Step 3.
Use this path if you already generate snapshots with another tool. The configuration depends on which tool you use.
If you already have Paparazzi configured, set generateTests = false inside the previews block so Sentry uses your existing tests instead of auto-generating preview-based ones:
sentry {
snapshots {
enabled = true
previews {
generateTests = false
}
}
}
sentry {
snapshots {
enabled = true
previews {
generateTests = false
}
}
}
Continue to Step 3.
If you already have Roborazzi configured, wire the Sentry upload task to the output of your Roborazzi record task:
afterEvaluate {
tasks.named<SentryUploadSnapshotsTask>("sentryUploadSnapshotsDebug") {
dependsOn(tasks.named("recordRoborazziDebug"))
snapshotsPath.set(
project.extensions.getByType<RoborazziExtension>().outputDir
)
}
}
afterEvaluate {
tasks.named<SentryUploadSnapshotsTask>("sentryUploadSnapshotsDebug") {
dependsOn(tasks.named("recordRoborazziDebug"))
snapshotsPath.set(
project.extensions.getByType<RoborazziExtension>().outputDir
)
}
}
Continue to Step 3.
The same pattern works with any snapshot tool. Set snapshotsPath to the directory your tool writes images to, and add a dependsOn for the task that generates them:
afterEvaluate {
tasks.named<SentryUploadSnapshotsTask>("sentryUploadSnapshotsDebug") {
dependsOn(tasks.named("yourSnapshotTask"))
snapshotsPath.set(layout.projectDirectory.dir("path/to/snapshots"))
}
}
afterEvaluate {
tasks.named<SentryUploadSnapshotsTask>("sentryUploadSnapshotsDebug") {
dependsOn(tasks.named("yourSnapshotTask"))
snapshotsPath.set(layout.projectDirectory.dir("path/to/snapshots"))
}
}
Alternatively, you can skip the Gradle plugin entirely and upload with sentry-cli snapshots upload:
sentry-cli snapshots upload ./build/paparazzi/snapshots \
--app-id android-app
sentry-cli snapshots upload ./build/paparazzi/snapshots \
--app-id android-app
Use this path if your build pipeline doesn't use Gradle, or if you want to run the upload outside of a Gradle task. See Uploading Snapshots for the expected directory layout.
Verify your setup by running:
./gradlew sentryUploadSnapshotsDebug
./gradlew sentryUploadSnapshotsDebug
Once the local upload succeeds, wire the same command into your CI. See Integrating Into CI for an example GitHub Actions workflow.
The options below apply when using the Compose Previews path with generateTests = true (the default).
The previews block inside snapshots exposes options that shape the generated Paparazzi tests.
sentry {
snapshots {
enabled = true
previews {
theme = "@style/Theme.MyApp" // optional
includePrivatePreviews = false // optional, defaults to true
packageTrees = listOf("com.example") // optional, defaults to Android namespace
}
}
}
sentry {
snapshots {
enabled = true
previews {
theme = "@style/Theme.MyApp" // optional
includePrivatePreviews = false // optional, defaults to true
packageTrees = listOf("com.example") // optional, defaults to Android namespace
}
}
}
| Option | Default | Description |
|---|---|---|
theme | Paparazzi default (android:Theme.Material.NoActionBar.Fullscreen) | Android theme resource used when rendering previews. Set this if your previews rely on a specific theme. |
includePrivatePreviews | true | Whether @Preview composables declared private are snapshotted. Set to false to exclude in-progress or internal-only previews. |
packageTrees | [] (falls back to Android namespace) | Package prefixes to scan for @Preview composables. When empty, the plugin uses the Android namespace from the build variant. |
If you don't set a theme, Paparazzi uses its own default (android:Theme.Material.NoActionBar.Fullscreen). If the background of your previews isn't important — for example, when you just want to compare component geometry — you can use a translucent platform theme such as android:Theme.Translucent.NoTitleBar:
sentry {
snapshots {
enabled = true
previews {
theme = "android:Theme.Translucent.NoTitleBar"
}
}
}
sentry {
snapshots {
enabled = true
previews {
theme = "android:Theme.Translucent.NoTitleBar"
}
}
}
If theme references a style that the renderer can't resolve (typo, wrong prefix, or a theme not on the classpath), neither Sentry nor Paparazzi raises an error or warning. Snapshots still generate, but with incorrect visuals. Double-check the theme string if rendered output looks unexpected.
To ignore small pixel changes across all snapshots, set diffThreshold in the plugin configuration:
sentry {
snapshots {
enabled = true
diffThreshold = 0.01 // ignore changes of 1% or less
}
}
sentry {
snapshots {
enabled = true
diffThreshold = 0.01 // ignore changes of 1% or less
}
}
The diffThreshold is a value between 0.0 and 1.0. Sentry reports a snapshot as changed only when the share of changed pixels exceeds this value. The default is 0.0 (report any difference).
To override the global threshold for specific previews, annotate them with @SentrySnapshot and set a diff threshold. First, add the runtime dependency:
dependencies {
testImplementation("io.sentry:sentry-snapshots-runtime:6.18.0")
}
dependencies {
testImplementation("io.sentry:sentry-snapshots-runtime:6.18.0")
}
Then annotate any @Preview composable:
import io.sentry.snapshots.runtime.SentrySnapshot
@SentrySnapshot(diffThreshold = 0.01f) // ignore changes of 1% or less
@Preview
@Composable
fun BillingPagePreview() {
BillingPage()
}
import io.sentry.snapshots.runtime.SentrySnapshot
@SentrySnapshot(diffThreshold = 0.01f) // ignore changes of 1% or less
@Preview
@Composable
fun BillingPagePreview() {
BillingPage()
}
Per-preview values take precedence over the global diffThreshold. See Diff Thresholds for other ways to configure thresholds.
When you use the Compose previews path, the plugin renders one image per @Preview and writes a JSON metadata file next to it. The annotations on the composable control everything about the snapshot: how it's rendered, how it's named and grouped in Sentry, and how it's compared against its baseline.
The table below lists all supported attributes across Google's @Preview annotation and Sentry's @SentrySnapshot annotation. The Metadata column shows how each attribute is recorded in the snapshot's JSON metadata. Tags are only written when the attribute differs from its default, and tag values are always serialized as strings, even for numeric and boolean attributes.
| Attribute | Annotation | Description | Metadata |
|---|---|---|---|
name | @Preview | Distinguishes multiple previews of the same composable — several @Preview annotations or a multipreview annotation like @PreviewLightDark. Appended to the display_name. | preview_name tag |
group | @Preview | Groups related snapshots so they're reviewed as a set in Sentry. Also becomes part of the display_name. | group field |
device | @Preview | Device spec (for example id:pixel_7) that sets the screen size, density, and shape used for rendering. | device tag |
widthDp | @Preview | Fixed render width in dp. Overrides the width of the device. | width_dp tag |
heightDp | @Preview | Fixed render height in dp. Overrides the height of the device. | height_dp tag |
locale | @Preview | Locale used for rendering. Defaults to en. | locale tag |
fontScale | @Preview | Font scale multiplier applied when rendering. | font_scale tag |
apiLevel | @Preview | Android API level to render against. | api_level tag |
uiMode | @Preview | Only the night mode bit is honored: UI_MODE_NIGHT_YES renders the preview in dark mode. | ui_mode tag (dark or light) |
showSystemUi | @Preview | Renders the preview at full device size. | show_system_ui tag |
showBackground | @Preview | Draws a background behind the preview — white unless backgroundColor is set. | show_background tag |
backgroundColor | @Preview | ARGB color of the background when showBackground is true. | — |
wallpaper | @Preview | Ignored. Dynamic color wallpapers aren't applied when rendering. | — |
diffThreshold | @SentrySnapshot | Minimum share of changed pixels (0.0 to 1.0) required to report the snapshot as changed. Overrides the global threshold — see Set Diff Thresholds Per Preview. | diff_threshold field |
Spaces in name and group are replaced with underscores in the generated display_name and filename, but the group field and preview_name tag keep the original value.
For example, this preview:
@Preview(name = "Dark", group = "Checkout")
@Composable
fun BillingPagePreview() {
BillingPage()
}
@Preview(name = "Dark", group = "Checkout")
@Composable
fun BillingPagePreview() {
BillingPage()
}
produces the following metadata:
{
"display_name": "BillingPagePreview.Dark_Checkout",
"group": "Checkout",
"tags": {
"preview_name": "Dark"
},
"context": {
"image_file_name": "com.example.billing.BillingScreenKt.BillingPagePreview.Dark_Checkout",
"class_name": "com.example.billing.BillingScreenKt",
"method_name": "BillingPagePreview"
}
}
{
"display_name": "BillingPagePreview.Dark_Checkout",
"group": "Checkout",
"tags": {
"preview_name": "Dark"
},
"context": {
"image_file_name": "com.example.billing.BillingScreenKt.BillingPagePreview.Dark_Checkout",
"class_name": "com.example.billing.BillingScreenKt",
"method_name": "BillingPagePreview"
}
}
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").