Repository navigation
Debug log export is too thin to diagnose field reports: null tags, no device context, silent mic-tap path #112
Description
Summary
Investigating #111 with a real user log export showed the log is too thin to diagnose the reported bug. Every line is tagged null, there is no device/OS/permission context, and the exact code path users complain about (mic tap in the keyboard) can fail while writing nothing at all to the file.
Findings below are verified against the bytecode and against a control run on an emulator, not inferred.
1. Every log line is tagged [D/null] — verified root cause
Both the user export and a local emulator log show the same thing on every single line:
2026-08-21 11:22:05.212 [D/null] IME status: enabled=false, selected=false
FileLoggingTree extends Timber.Tree (core/.../logging/FileLoggingTree.kt:23). Disassembling timber-5.0.1:
--- Timber$Tree ---
private final ThreadLocal<String> explicitTag;
public String getTag$timber_release(); // returns explicitTag only -> null
--- Timber$DebugTree ---
public String getTag$timber_release(); // overridden
protected String createStackElementTag(StackTraceElement); // derives tag from stack
The stack-trace-derived tag lives in DebugTree, not in Tree. FileLoggingTree never overrides getTag(), so unless a caller uses Timber.tag(...) explicitly the tag is always null.
Why this actually hurts: "Bound to DictationService" is logged by both MainActivity.kt:99 and DictusImeService.kt:195, with byte-identical text. With a null tag the two are indistinguishable in the export. While triaging #111 the app-side lines could only be attributed because they happened to always follow an IME status line — luck, not design.
Fix: make FileLoggingTree extend Timber.DebugTree(), or copy createStackElementTag's stack-walk into an overridden getTag(). Note R8: keep enough attributes for class names to survive in release, or the tag degrades again.
2. The export has no device or permission context
dictus.log contains log lines and nothing else. It does not record:
- Android version / API level
- OEM or alternative ROM (Onboarding enables the Dictus keyboard but never selects it — users think it is active and the mic does nothing (v1.1.0) #111 involves a GrapheneOS user — the file gives no way to know that)
- App version and build number
RECORD_AUDIOgrant state- Currently selected IME
Consequence for #111: it was impossible to tell "permission was revoked" from "the keyboard was never selected" out of the file alone. It took a control run on an emulator to disambiguate.
Fix: have LogExporter prepend a device header block to the exported file.
3. The reported failure path can log absolutely nothing
gatedMicTap (ime/.../DictusImeService.kt:640) is the handler wired to the mic button. It has no entry log:
val gatedMicTap = {
autocorrectCoordinator.onOtherInput()
if (dictationState is DictationState.Idle) {
if (checkSelfPermission(RECORD_AUDIO) == PERMISSION_GRANTED) {
...
runGateCommand(micGate.request(engineState)) // -> dictationController?.startRecording()
} else {
Timber.w("RECORD_AUDIO permission not granted")
}
} else {
handleMicTap()
}
}If the permission is granted but dictationController is null, runGateCommand does dictationController?.startRecording() — the safe call swallows it and no line is written. handleMicTap does log on entry (:581), but gatedMicTap never reaches it in the Idle path.
So a user tapping the mic and getting nothing produces a log where nothing happened. That is precisely the report in #111.
Fix: log on entry to gatedMicTap with state + bound flag + permission state, and log the null-controller branch explicitly instead of relying on ?..
4. No log when the keyboard is actually shown
DictusImeService logs "DictusImeService created" in onCreate (:231), but onStartInputView logs nothing. "Did the user ever actually open the Dictus keyboard, and in which app/editor?" is unanswerable from the export.
Fix: log onStartInputView (with restarting flag), and log onFinishInputView.
5. The privacy purge silently erases history with no marker
TimberSetup.purgeLegacySensitiveLogOnce (core/.../TimberSetup.kt:43) deletes dictus.log once on upgrade. The user log in #111 starts abruptly at an app launch, and there is no way to tell whether that is the true first entry or the purge point.
Fix: write a first line recording that the purge ran, and log a session-start banner (app version, build, timestamp) on every TimberSetup.init.
Priority
1 and 3 block diagnosing #111 and any future keyboard report. 2 is what turns a user's ZIP into something actionable without a round-trip.
Activity
Pivii commentedon Aug 28, 2026
Point 3 is now proven empirically, and it is the worst of the five
Emulator, Pixel 4 API 29, current
develop, Dictus keyboard active in a real editor (contact form). Log captured before and after tapping the blue mic:A successful keyboard mic tap writes two lines, both from
DictationService, and not one line from the IME.Consequences, both of which actually bit during the triage of #111:
This led to a wrong conclusion in #111 that took an emulator control run to catch. Until
gatedMicTaplogs on entry — state, bound flag, permission state, engine state — no field report about the keyboard can be diagnosed from an export.Raising this to the top of the list.
feat(logging): name the refusal and the editor behind a failed correc…