ADFA-5534: attach the carousel metrics file to user feedback - #1800
Open
davidschachterADFA wants to merge 9 commits into
Open
ADFA-5534: attach the carousel metrics file to user feedback#1800davidschachterADFA wants to merge 9 commits into
davidschachterADFA wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
davidschachterADFA
force-pushed
the
feature/ADFA-5531-metrics-csv-export
branch
from
September 7, 2026 12:21
1476b2d to
7e854f4
Compare
davidschachterADFA
force-pushed
the
feature/ADFA-5534-metrics-in-feedback
branch
from
September 7, 2026 12:28
c193d08 to
2405b68
Compare
davidschachterADFA
force-pushed
the
feature/ADFA-5531-metrics-csv-export
branch
from
September 7, 2026 12:43
7e854f4 to
3e3869b
Compare
davidschachterADFA
force-pushed
the
feature/ADFA-5534-metrics-in-feedback
branch
from
September 7, 2026 12:43
2405b68 to
6b40850
Compare
The assembly began on MetricsCarouselController, which reads the binding for a Context and returns early when the carousel is unbound. That is right for a button on the carousel and wrong for everything else that wants the file: the feedback FAB can be tapped with the strip closed, and a crash report (ADFA-5526) is assembled with no UI at all. Moved to MetricsSnapshotAssembler, which needs only the three watchers, the annotation store and a Context. The controller delegates and passes the context it already has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
… is empty Two additions to the format, both for callers that send the file rather than hand it to the user. hasRows answers "is this worth sending". The writer always produces a file, header included, because the export button asked for one whatever the buffers held -- rule (d), now spelled out on ADFA-5531. Sending one is a different question, and this ticket omits the attachment when there is nothing in it rather than posting an empty file. writeForReport writes the same CSV gzipped. Streamed through GZIPOutputStream, so the uncompressed megabyte never has to exist. Gzip rather than zip: it is one file, so an archive container adds a name and nothing else, and java.util.zip gives us either with no dependency. The repo had no compression of any kind before this; ADFA-5526 says "compress, as this ticket does", so this is where that gets decided. It writes to its own directory. The exports prune to three, and a feedback send must not evict an export the user is part-way through handing to another app. Measured on a real 86-row attachment: 6425 bytes to 1395, 4.6x. The rows are near-identical by nature -- the timestamp advances by a constant and the magnitudes barely move -- so a full buffer does better. A test pins the ratio at better than 4x, so that if it ever stops being true the extra step gets questioned rather than kept out of habit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
"It got slow" arrives with nothing to correlate it against. The session's own samples turn it into something diagnosable. The feedback path lives in common and the metrics live in app, which depends on common and not the other way round, so the feedback code cannot reach the metrics. It takes a provider instead, mirroring the getLogContent hook already there for the log. Suspending, unlike that one: the snapshot has to be read on the main thread and the compressed file written off it, and neither belongs in a click listener. The provider returns null when nothing has been sampled, so feedback from a freshly started IDE carries no empty attachment. It is also guarded -- feedback about a broken IDE has to send even if this part fails. FeedbackEmailHandler already built ACTION_SEND_MULTIPLE from a URI list and fell back to ACTION_SENDTO on an empty one, so a third attachment is one more URI. FeedbackButtonManager's constructor gains @jvmoverloads, because TermuxActivity constructs it from Java where Kotlin's default arguments are invisible. Verified on a Pixel 6 Pro: the send writes 2026_09_07_00_52_54_706.csv.gz and the chooser opens with "clip={message/rfc822 3 items}", the URI grants naming the metrics file, the log and the screenshot. The attachment gunzips to 86 valid rows. The empty case is unit-tested rather than device-tested: clearing the buffers via a rate change is the only way to reach it, and the next sample lands about a second later, so the window is shorter than a tap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
`both directories stay bounded` fails on this branch: pruneTo picked the oldest n across every file and then skipped the one just written, so it deleted one too few whenever that one sorted into the set, and the directory crept one over the limit each time. The fix was sitting two branches up on ADFA-5526, which left this PR red on its own test. It belongs where the test that catches it is. The tie-case test that came with it was vacuous, so it is rewritten here. Tying every existing file to one *past* timestamp does not reproduce the bug: the file written last still carries a real mtime, so it sorts last, is never in the deleted set, and the skip never fires. Dating the existing files into the future puts the new one at the front of the sort deterministically. It now fails without the fix, four files where three are allowed, as does the bounded test. Also carries the row-alignment fix into MetricsSnapshotAssembler, which is where the assembly moved on this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
The ratio test ran on a snapshot whose every column advanced by a constant, which gzip compresses about fiftyfold -- so "better than 4x" was met with room to spare by data no session produces, and the test said nothing about whether the extra step earns its place. It now runs on a seeded session whose columns move the way a device's do: memory in steps of megabytes, network in bursts, temperature and power drifting, thermal status flipping. That is deliberately noisier than reality -- 2.8x here against 4.6x measured on a real attachment -- so it is a floor. The bound is halving, which compression bypassed fails and a shift in gzip's tuning does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
davidschachterADFA
force-pushed
the
feature/ADFA-5534-metrics-in-feedback
branch
from
September 7, 2026 14:00
6b40850 to
6054780
Compare
…-5534-metrics-in-feedback # Conflicts: # app/src/main/java/com/itsaky/androidide/ui/MetricsCarouselController.kt
The comment said feedback about a broken IDE must still send if the metrics part fails. Two ways it did not. fileProviderUriFor was chained outside the runCatching. FileProvider throws IllegalArgumentException for a path outside its configured roots, and this feature added a directory -- so the throw went past the guard and took the whole send with it. Unreachable today only because the paths XML has a root-path of ".", which is not a thing to depend on. CancellationException was swallowed. runCatching catches Throwable and the provider genuinely suspends -- withContext twice -- so an activity destroyed mid-write had its cancellation eaten and the coroutine ran on to startActivity() on a dead activity. Rethrown now. The step is extracted so it can be tested at all: sendFeedbackWithAttachments needs a live activity and its lifecycle scope, and this is the part with the failure modes. Five tests; the two that name these defects fail against the old shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
…-5534-metrics-in-feedback
…-5534-metrics-in-feedback # Conflicts: # app/src/main/java/com/itsaky/androidide/ui/MetricsCarouselController.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A report of "it got slow" arrives with nothing to correlate it against. The feedback send now carries the session's own metrics, gzipped, alongside the screenshot and log it already attaches.
Three commits: move the assembly off the carousel, add the gzipped writer and an emptiness check, wire the feedback path.
The part that was actual work
The snapshot assembly began on
MetricsCarouselController, which reads the binding for aContextand returns early when the carousel is unbound. That is right for a button on the carousel and wrong for everything else that wants the file — the feedback FAB can be tapped with the strip closed, and a crash report (ADFA-5526) is assembled with no UI at all. So it moved toMetricsSnapshotAssembler, which needs only the three watchers, the annotation store and aContext. That is what makes the format module reusable rather than carousel-bound, and it is the piece both remaining consumers need.Module boundary
FeedbackManagerandFeedbackEmailHandlerare incommon; the metrics are inapp, which depends oncommonand not the reverse. So the feedback code cannot reach the metrics — it takes a provider, mirroring thegetLogContenthook already there for the log.Suspending, unlike that one: the snapshot must be read on the main thread and the compressed file written off it, and neither belongs in a click listener.
sendFeedbackWithAttachmentsis already insidelifecycleScope.launch, so it drops straight in. The call is guarded — feedback about a broken IDE has to send even if this part fails.FeedbackEmailHandleralready builtACTION_SEND_MULTIPLEfrom a URI list and fell back toACTION_SENDTOon an empty one, so a third attachment is one more URI.FeedbackButtonManager's constructor gains@JvmOverloads, becauseTermuxActivityconstructs it from Java, where Kotlin's default arguments are invisible.The two tickets contradicted each other
ADFA-5531's format section says a file is always produced; this ticket says omit the attachment when there are no data rows. Both are satisfiable — the writer always writes, the caller asks first — so
MetricsCsv.Snapshot.hasRowsis how a caller asks, and I have added rule (d) to ADFA-5531 spelling out that sending an empty file is the caller's decision and not the format's. ADFA-5526 will inherit that rather than the confusion.Compression
There was no compression of any kind in the repo before this — no
GZIPOutputStream,ZipOutputStreamorDeflaterinapporcommon. ADFA-5526 says "compress, as this ticket does", but 5526 is blocked and this one is not, so this is where it gets decided:.csv.gz, streamed throughGZIPOutputStreamso the uncompressed megabyte never has to exist.java.util.zipgives us either with no dependency. The only thing zip buys is double-click friendliness for a file we run scripts over.Measured on a real 86-row attachment: 6425 → 1395 bytes, 4.6x. The rows are near-identical by nature — the timestamp advances by a constant and the magnitudes barely move — so a full buffer does considerably better. A test pins the ratio at better than 4x, so if that ever stops being true the extra step gets questioned rather than kept out of habit.
Verification
On a Pixel 6 Pro, a real feedback send:
cache/metrics-reports/2026_09_07_00_52_54_706.csv.gz;act=android.intent.action.SEND_MULTIPLE typ=message/rfc822 clip={message/rfc822 3 items};.csv.gz,Feedback Log ….txt,Screenshot ….jpg;The empty case is unit-tested rather than device-tested, deliberately: clearing the buffers via a sampling-rate change is the only way to reach it, and the next sample lands about a second later because the in-flight
delay()still holds the old interval — so the window is shorter than a tap. I tried it and caught exactly one row, which is the writer behaving correctly rather than the check failing.Unit tests:
hasRowsboth ways, gzip round-trips to the identical CSV, the compression ratio, the two directories pruning independently, and both staying bounded. Full:appunit suite andspotlessCheckgreen.One thing I fixed while writing those: the filename assertions initially passed only because this machine is in
America/Los_Angeles. They now pass an explicit zone, so they do not depend on where CI runs.Font scale
No UI added — the feedback FAB and its dialog are unchanged. Nothing to re-verify at 2.0.
Review fixes
This PR was red on its own test.
both directories stay boundedfails here:pruneTopicked the oldest n across every file and then skipped the one just written, so it deleted one too few whenever that one sorted into the set, and the directory crept one over the limit each time. The fix was sitting two branches up on ADFA-5526. It belongs where the test that catches it is, so it has moved here.The tie-case test that came with it was vacuous, and is rewritten. Tying every existing file to one past timestamp does not reproduce the bug: the file written last still carries a real mtime, so it sorts last, is never in the deleted set, and the skip never fires. Dating the existing files into the future puts the new one at the front of the sort deterministically. It now fails without the fix — four files where three are allowed — as does the bounded test.
The compression-ratio test measured a straight line. It ran on a snapshot whose every column advanced by a constant, which gzip compresses about fiftyfold, so "better than 4x" was met with room to spare by data no session produces. It now runs on a seeded session whose columns move the way a device's do — memory in steps of megabytes, network in bursts, temperature and power drifting, thermal status flipping. That is deliberately noisier than reality (2.8x here against the 4.6x measured on a real attachment), so it is a floor. The bound is halving, which compression bypassed fails and a shift in gzip's tuning does not.
Also carries ADFA-5531's row-alignment fix into
MetricsSnapshotAssembler, which is where the assembly moved on this branch.Review fix: the whole attachment step is inside the guard it claims
The comment said feedback about a broken IDE must still send if the metrics part fails. Two ways it did not:
fileProviderUriForwas chained outside therunCatching. FileProvider throwsIllegalArgumentExceptionfor a path outside its configured roots, and this feature added a directory — so the throw went past the guard and took the whole send with it. Unreachable today only because the paths XML has aroot-pathof".", which is not a thing to depend on.CancellationExceptionwas swallowed.runCatchingcatchesThrowableand the provider genuinely suspends —withContexttwice — so an activity destroyed mid-write had its cancellation eaten and the coroutine ran on tostartActivity()on a dead activity.The step is extracted to
metricsAttachmentUriso it can be tested at all:sendFeedbackWithAttachmentsneeds a live activity and its lifecycle scope, and this is the part with the failure modes. Five tests; the two that name these defects fail against the old shape.On device
Verified on a Galaxy Note20 Ultra (SM-N986U, Android 13, arm64), running a local integration build of this stack plus ADFA-5514.
A real feedback send from the FAB produced:
Three attachments, and the gzipped metrics file landed in its own directory —
cache/metrics-reports/2026_09_07_14_52_32_316.csv.gz, 13,187 bytes — not inmetrics-exportsalongside the user's own exports. That separation is the point of the second directory, and it holds in practice: a plain export taken three minutes earlier is still sitting untouched inmetrics-exports.Compression on real data: 13,187 bytes against a 54,528-byte plain export over a comparable window, roughly 4x.
One thing worth a look before merge. The send logs a
Bundlewarning fromFeedbackManager.kt:383, which is thestartActivitycall insendFeedbackWithAttachments. The intent went through and this is a warning rather than a failure — but that line is exactly the path guarded againstTransactionTooLargeException, and this change adds a third attachment to the payload. I have not chased it; flagging it rather than leaving it in a log nobody reads.🤖 Generated with Claude Code
https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j