Say which half of an import failed, and give the rest a log worth sending - #149
Merged
Conversation
UnhandledProtocolError and anything reaching REASON_UNKNOWN had no screen of their own. They arrived at a toast saying to restart the app, which cannot help and asks somebody to repeat what just failed. The page carries the three things a report needs and a person otherwise has to hunt for: which build this is, which exporter made their bundle, and the failure verbatim. The last is why somebody would open an export zip - a file holding the private keys to their tags - to read a version line out of it. Shown only when the app cannot name the cause. Never for a rejected passcode, an account with no tags, a network that is down, or a session wanting a code: each of those has a screen saying what to do, and this one would be strictly worse than the advice they already give. A page that appears for ordinary mistakes is one people learn to dismiss, and then it is worth nothing on the day it is right. Get the log asks which of two things is wanted, because they are not the same: copy it for pasting into the issue form's log box, or save it through the document picker to attach. It cannot hand over a log the redactor refused - null means withhold, never fall back to the raw one, because an Apple ID on a public issue cannot be un-posted and this page is reached because something already broke. All ten locales' strings for this branch land here, including ones later commits use: they share ten files, and splitting them by hunk would risk a locale going missing for the sake of a tidier history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking a zip started one Rx chain that parsed the file, committed the tags to the database, and then went to Apple for their locations. One error handler covered all three, so a network timeout, an expired session or a bad Anisette server - none of which are consulted while reading a zip - all reported "Error occurred while importing new devices. Try to restart the app and retry", for an import that had already succeeded and whose tags were visible in the device list. Issues #19 and #26 are 34 comments of people working that out for themselves. Three of them fixed their "import error" by changing their Anisette server, which is the proof: nothing about Anisette is touched while parsing a file. Three things were wrong, in order of cost. It named the wrong phase. Its advice could not work - restarting does not re-run a failed fetch, and the import it suggested retrying had succeeded. And it discarded the diagnosis: the real exception went to Log.e and nowhere a user could see, which is why none of those reports can be attributed to a cause. So the chain is split at the commit point rather than marked. A phase marker would fix the message; splitting makes the wrong message impossible to write, because there is no longer one handler that could say it. The fetch half gets a dialog saying the tags are on the phone and stay there, the exception verbatim, and Anisette named - the answer three reporters reached unaided. No bug report offered there, deliberately: it fails when somebody else's server is down or a phone is on a train, it retries by itself every minute, and asking for a report about weather fills a tracker with reports nobody can act on. Completable.defer, because the fetch is now started from the main thread and subscribeOn moves the subscription rather than the construction. combine() streams every beacon into two maps and plistFallbacks walks them again, both evaluated as arguments; while this hung off the import chain it inherited that chain's IO thread. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four rules for things the exporter never sees, so none of them will fire on a
wizard log. They live here rather than in a second redactor because two sets of
rules mean two answers to "is my Apple ID in this file", and only one of them
would get maintained.
(The Export Logs caller that needed this went in with the MapsActivity change in
the previous commit: it wrote a raw logcat while the error page's copy went
through the redactor, which was two answers from one app. Both now refuse rather
than fall back - if redaction cannot run, nothing is written, because a raw log
cannot be un-posted and whoever exported one is on their way to attaching it.)
Coordinates get two rules, labelled and as a bare pair, because the app writes
both: String.format("%.4f,%.4f", ...) for a geocoding cache key and point=(lat,
lon) for a tapped marker. Four decimal places is about eleven metres - somebody's
home rather than their town, and the most personal thing this app handles.
Place names are labelled-only. "221B Baker Street, London NW1 6XE" has no shape a
regex can find, and a pattern loose enough to catch it would redact half the log.
Nothing logs one today; the rule is here so the day somebody adds Log.d(TAG,
"resolved to " + address), it is already covered.
Passcodes are the grouped form only. The bare twelve characters of Crockford
base32 are indistinguishable from a hash fragment, and the grouped form is what a
person copies and pastes anyway.
The phone number is main.py:142, which prints Apple's SMS option verbatim when it
lists second-factor methods. Nothing was stripping it.
The coordinate rule's false-positive guard was mutation-checked and had to be
rewritten: it used floats that were not adjacent, so the rule would never have
matched them however loose it got - loosening \d{3,} to \d+ left all 41 tests
green. The replacement sits on the boundary and goes red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The issue template asks, and the only place the answer lived was inside the export zip - a file holding the private keys to somebody's tags, which the same template tells them in bold not to open. Asking a question whose answer is in a file you have told people not to open is asking them to ignore you. The Information screen lists every distinct producer, newest-used first. Not the most recent one: importing twice is ordinary - a second Mac, a re-export after buying a tag, an old bundle beside a current one - and naming one producer reads as though it accounts for every tag on the phone. A report saying 1.3.0 when half the tags came out of 1.1.0 sends whoever reads it looking in the wrong place. GROUP BY rather than SELECT DISTINCT, because the ordering is by something not in the result: SQLite rejects an ORDER BY on a column a DISTINCT query does not select, and MAX(imported_at) per producer is the sort that means "most recently used". A tag's own page says which exporter made that tag, which the aggregate cannot. When a report is about one tag out of twelve, the exporter that made that one is the fact that decides what to expect of it. Nearly free: the page already read the same Import row for Exported by and Exported at. Both say so rather than hiding when there is nothing recorded. An export predating via: is itself information - it dates the bundle - and a missing row reads as a gap in the screen. A query and no schema change, so there is no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The issue template said "this is a raw Android log and nothing is removed from it", which stopped being true this branch, and told people to press a button called Export logs on the error page, which is now called Get the log. It also asked them to audit a log line by line, which is no longer the deal. The rest is the afternoon this cost. A managed-device run that is killed rather than finished never gives its slot back, and AGP counts them in ~/.android/avd/gradle-managed/active_gradle_devices. Mine reached MDLockCount 4, after which every run waited for a slot nothing would release - booting no emulator and printing nothing, which for ten minutes is indistinguishable from a device starting slowly. Three runs were abandoned diagnosing it as port contention. adb devices said so the whole time by listing no managed AVD. Documented symptom-first, with the two hangs that look identical separated by what they leave behind: no test ever reporting is the lock, tests stopping mid-suite is the device dying under them. And a skill, because the reporting was worse than the bug. Three hand-written monitors in one afternoon each matched nothing - one required (0 skipped) against a run with five skipped, one required a space before [testEmulator] that is not there, one truncated before the verdict with head. All three exited 0, and silence from a monitor is indistinguishable from silence from a healthy run, so a red suite read as green until the log was opened by hand. watch_tests.py owns the patterns, with --once to prove they match before anything depends on them. Writing it caught two more of the same: ^BUILD without re.MULTILINE reported "not yet" on a finished run, and summing every result file on disk reported a 22-test run as 74 because AGP keeps connected/ and managedDevice/ side by side and clears neither. It now scopes to the newest run's directory and always prints the results' age, because a verdict that is quietly ten minutes old is the same bug wearing a coat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parawanderer
temporarily deployed
to
Android Build
August 23, 2026 15:09 — with
GitHub Actions
Inactive
parawanderer
temporarily deployed
to
Android Build
August 23, 2026 15:09 — with
GitHub Actions
Inactive
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.
The app told four reporters their import failed. It hadn't.
Picking a zip started one Rx chain that parsed the file, committed the tags to the database,
and then went to Apple for their locations. One error handler covered all three, so a network
timeout, an expired session or a bad Anisette server — none of which are consulted while reading
a zip — all reported:
…for an import that had already succeeded and whose tags were visible in the device list.
#19 and
#26 are 34 comments of people working
that out for themselves. Three of them fixed their "import error" by changing their Anisette
server, which is the proof: nothing about Anisette is touched while parsing a file.
Three things were wrong, in order of cost:
suggested retrying had succeeded.
Log.eand nowhere a user couldsee it — which is why none of those reports can be attributed to a cause.
What changed
The chain is split at the commit point. Reading the zip and storing the tags is one
operation; asking Apple where they are is another, started only once the first has succeeded. A
phase marker would have fixed the message; splitting makes the wrong message impossible to write,
because there is no longer one handler that could say it.
Each half now says its own thing:
No bug report on the fetch side, deliberately. That half fails when somebody's server is down
or a phone is on a train, and it retries by itself every minute. Inviting a report each time
would fill the tracker with weather. The bug page belongs to the half that reads the zip.
A page for a failure nobody can act on
UnhandledProtocolErrorand anything reachingREASON_UNKNOWNnow land on a page carrying thethree things a report needs and a person otherwise has to hunt for: which build this is, which
exporter made their bundle, and the failure verbatim. Never for a rejected passcode, an account
with no tags, a network that is down, or a session wanting a code — each has advice this page
would be strictly worse than.
Get the log asks which of two things is wanted, because they are not the same thing: copy it
for pasting into the issue form's log box, or save it through the document picker to attach. It
shipped as a share sheet, which serves neither — with text and no stream, Drive and Files do not
appear as targets at all.
Both routes out of the app now clean the log
Settings → Export Logs wrote a raw logcat while the error page's copy went through the
redactor. Two answers from one app to "is my Apple ID in this file". Same rule on both now, and
the same refusal: if redaction cannot run, nothing is written rather than a raw log handed to
somebody on their way to attaching it somewhere public.
redact.pygains four rules for things only the app produces — coordinates (labelled and as barepairs, since the app writes
%.4f,%.4f), reverse-geocoded place names, bundle passcodes, and thephone number
main.pyprints when it lists second-factor methods. They live with the exporter'srules rather than in a second redactor, because two sets of rules mean two answers and only one
of them would get maintained.
Which exporter made a bundle, in both places it is asked
The issue template asks, and the only place the answer lived was inside the export zip — a file
holding the private keys to somebody's tags, which the same template tells them in bold not to
open.
one: importing twice is ordinary, and naming one producer implies it accounts for every tag on
the phone.
twelve, the aggregate cannot tell you that. The row was nearly free — the page already read the
same
Importfor its "Exported by" and "Exported at" rows.Testing
WhichHalfOfAnImportFailedTest,DescribingTheBuildInALogTest,TheIssueLinkPointsSomewhereRealTesttest_redact.py, including the false-positive guards for the coordinate ruleEvery new assertion was mutation-checked. Two were decorations and are fixed:
thesharedLogIsTheRedactedOneasserted only that a chooser opened — its own comment claimedotherwise. It would have stayed green while shipping an unredacted log, which is the one
outcome here that cannot be taken back. It now asserts on the payload, and bypassing redaction
reddens exactly the three tests that should go red.
have matched them however loose it got — loosening
\d{3,}to\d+left all 41 green. Replacedwith a case that sits on the boundary.
ImportDaogains a query and no schema change, so there is no migration.Not verified
The Apple-facing paths are unchanged by this, but nothing here was exercised against a real
account or a real failing Anisette server — the fetch-failure dialog was driven with a fabricated
exception, not a live outage. The claim being made is about which message appears for which
failure, not about the failures themselves.
🤖 Generated with Claude Code