feat(snapshot): Write per-image sidecar metadata during test execution#1127
feat(snapshot): Write per-image sidecar metadata during test execution#1127runningcode merged 19 commits intomainfrom
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. This PR will not appear in the changelog. 🤖 This preview updates automatically when you update the PR. |
|
| } | ||
|
|
||
| private fun escapeJson(s: String): String = | ||
| s.replace("\\", "\\\\").replace("\"", "\\\"") |
There was a problem hiding this comment.
Incomplete JSON escape misses control characters
Low Severity
The escapeJson function only escapes backslashes and double quotes, but JSON requires escaping of all control characters (U+0000–U+001F) including \n, \r, \t, \b, and \f. If any preview metadata field (e.g., group, name, device) contains these characters, the generated sidecar JSON would be malformed and sentry-cli could fail to parse it.
| val snapshotDir = File(System.getProperty("paparazzi.snapshot.dir")) | ||
| val imagesDir = File(snapshotDir, "images") | ||
| imagesDir.mkdirs() |
There was a problem hiding this comment.
Bug: The task to generate snapshot sidecar files writes to a different directory (src/test/snapshots/images/) than the upload task reads from (build/sentry-snapshots/), causing the upload to fail.
Severity: HIGH
Suggested Fix
Update the snapshotsPath configuration for the SentryUploadSnapshotsTask in AndroidComponentsConfig.kt. It should be configured to use the same path derived from System.getProperty("paparazzi.snapshot.dir") that the GenerateSnapshotTestsTask uses for writing the files, ensuring both tasks point to the same directory.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt#L341-L343
Potential issue: The `GenerateSnapshotTestsTask` writes sidecar metadata JSON files to
the directory specified by the `paparazzi.snapshot.dir` system property, which defaults
to `src/test/snapshots/images/`. However, the `SentryUploadSnapshotsTask` is configured
in `AndroidComponentsConfig.kt` to look for these files in a completely different
location: `build/sentry-snapshots/<variant>/images/`. Because there is no mechanism to
copy the files between these two directories, the upload task will either fail its input
validation because the directory does not exist, or it will find no files to upload.
This completely breaks the new sidecar metadata feature.
Did we get this right? 👍 / 👎 to inform future reviews.
| sources.java?.addGeneratedSourceDirectory( | ||
| generateTask, | ||
| GenerateSnapshotTestsTask::outputDir | ||
| ) | ||
| } | ||
| } else { | ||
| // `unitTest` is deprecated, the replacement above is complex | ||
| @Suppress("DEPRECATION_ERROR") | ||
| variant.unitTest | ||
| ?.sources | ||
| ?.java | ||
| ?.addGeneratedSourceDirectory(generateTask, GenerateSnapshotTestsTask::outputDir) | ||
| // `unitTest` is deprecated, the replacement above is complex | ||
| variant.unitTest?.apply { | ||
| sources.java?.addGeneratedSourceDirectory(generateTask, GenerateSnapshotTestsTask::outputDir) | ||
| configureTestTask { it.systemProperty("paparazzi.test.record", "true") } |
There was a problem hiding this comment.
Bug: The plugin unconditionally sets paparazzi.test.record=true on the main test task, which forces existing user Paparazzi tests into record mode, breaking their verification behavior.
Severity: HIGH
Suggested Fix
The paparazzi.test.record system property should not be set on the main test task. Instead, it should be configured only for the specific, generated Sentry snapshot test. This could be achieved by creating a dedicated Gradle task for the Sentry snapshot test or by finding a way to apply system properties to a single test class within the larger test task.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/SentrySnapshotPlugin.kt#L39-L51
Potential issue: The `SentrySnapshotPlugin` unconditionally sets the system property
`paparazzi.test.record` to `true` for the entire variant-level test task, such as
`testDebugUnitTest`. This property is JVM-wide for the test fork, affecting all tests,
not just those generated by Sentry. As a result, any existing Paparazzi tests within the
same module are forced into "record mode". This causes them to overwrite their golden
snapshot files instead of performing verification, silently breaking the user's existing
test suite. The generated Sentry test itself is unaffected as it uses different logic
based on `paparazzi.test.verify`.
c71b78a to
6906dcb
Compare
1cf4262 to
59cf660
Compare
aed4be7 to
e460156
Compare
The sentry-cli `build snapshots` command supports per-image metadata via companion JSON sidecar files, but this was disconnected from the preview metadata the plugin already knows about. This adds a `SentrySnapshotHandler` to the generated Paparazzi test that writes each rendered image (with its screenshotId as filename) plus a companion JSON sidecar to `build/sentry-snapshots/images/`. The sidecar includes `display_name`, `image_file_name`, `group`, `className`, and `methodName` fields that sentry-cli merges into the upload manifest. The upload task's `snapshotsPath` is now automatically wired to this output directory, and the `sentry.snapshot.output` system property is set on all Test tasks so the generated code writes to the correct path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove SentrySnapshotHandler that used java.awt.image.BufferedImage
and javax.imageio.ImageIO which are unavailable in Android test
classpath
- Use SnapshotVerifier pointed at sentry output dir instead, which
writes images with predictable names (not content hashes)
- Fix methodName access: use preview.methodName (ComposablePreview)
instead of info.methodName (AndroidPreviewInfo doesn't have it)
- Fix declaringClass: it's a non-nullable String on ComposablePreview
- Write sidecar filenames matching SnapshotVerifier's naming pattern
(Paparazzi_Preview_Test_{screenshotId_lowercased})
- Clean sentry output dir before each test run to ensure fresh writes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add nightMode, fontScale, locale, device, apiLevel, widthDp, heightDp, showSystemUi, showBackground, previewName, and group fields to the sidecar JSON. Non-default values are included to keep the file compact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SnapshotVerifier is verify-only and always compares against golden
files. Replace it with HtmlReportWriter which has a recording mode
(paparazzi.test.record=true) that copies images with predictable
names to a configurable snapshot directory.
Pass snapshotRootDirectory=sentryOutputDir to HtmlReportWriter so
recorded images go to build/sentry-snapshots/images/ with names
matching the sidecar JSONs (Paparazzi_Preview_Test_{screenshotId}).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use build/sentry-snapshots/{variant}/images/ instead of a shared
directory so that different build variants don't overwrite each other.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of configuring a custom snapshotRootDirectory on HtmlReportWriter and passing it via a sentry.snapshot.output system property, reuse the paparazzi.snapshot.dir that Paparazzi's plugin already sets. Sidecar JSON files are now written alongside golden images in the same directory. Also simplifies sidecar JSON generation using a linkedMapOf and removes dead code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Let users pass the path via --snapshots-path instead of hardcoding the now-removed build/sentry-snapshots directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip the fully qualified class prefix from the screenshotId so display_name shows e.g. "BookmarksScreenAppStorePreview.NIGHT_PIXEL_5" instead of the full package path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop unconditionally setting paparazzi.test.record=true on the variant test task, which forced all Paparazzi tests in the module into record mode and silently overwrote golden files for existing user tests. Users should run recordPaparazzi<Variant> which sets this property correctly via Paparazzi's own plugin. Also adds unit tests for GenerateSnapshotTestsTask. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidecar JSON and PNG filenames were being lowercased, causing a mismatch with the original screenshotId casing. Remove the `.lowercase(Locale.US)` call so filenames keep their original case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Paparazzi's `Snapshot.toFileName()` applies `.lowercase(Locale.US)` to the snapshot name when generating image filenames. The sidecar JSON filenames must match, so re-add the lowercase call that was removed in e460156. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded hex literals (0x30, 0x20) with UI_MODE_NIGHT_MASK and UI_MODE_NIGHT_YES constants from PreviewConstants for consistency with the rest of the generated test code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
38025e0 to
e2c8a67
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/res/Configuration.java | ||
|
|
||
| private const val UI_MODE_NIGHT_YES: Int = 32 | ||
| internal const val UI_MODE_NIGHT_MASK: Int = 0x30 |
There was a problem hiding this comment.
Newly added UI_MODE_NIGHT_MASK constant is unused in plugin
Low Severity
UI_MODE_NIGHT_MASK was added as internal in PreviewConstants.kt but is never referenced anywhere in the plugin's own code. The generated test code imports UI_MODE_NIGHT_MASK from android.content.res.Configuration instead, making this constant dead code.


Summary
display_name,image_file_name,className,methodName, and optional fields likegroup,locale,device,nightMode,fontScale,apiLevel, dimensions, and UI flagspaparazzi.snapshot.dir) for sidecar outputsnapshotsPathand sets thesentry.snapshot.outputsystem property on Test tasks#skip-changelog
🤖 Generated with Claude Code