Add on-device debug logging: file capture + share (for real-session testing)

- fileLogger: append redacted API requests/responses + app events to an on-device
  log file (persists across restarts); the client's logSink now writes to it
- Diagnostics screen (Account > Diagnostics): toggle recording, Share log file
  (expo-sharing), clear. StartSession logs the session attempt/result explicitly.
- Lets a real parking session be captured while away from a computer, then shared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-06 11:59:41 -07:00
parent 80812d25cf
commit 7ec7ae9200
8 changed files with 216 additions and 19 deletions

View file

@ -0,0 +1,83 @@
import * as FileSystem from 'expo-file-system';
import * as Sharing from 'expo-sharing';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
* On-device debug log. When enabled, API requests/responses (already redacted by
* the client) and app events are appended to a file the user can share afterward
* useful for capturing a real parking-session attempt while away from a computer.
*/
const LOG_URI = FileSystem.documentDirectory + 'bbp-debug.log';
const ENABLED_KEY = 'ps_file_logging';
const MAX_LINES = 4000;
let enabled = false;
let buffer: string[] = [];
let flushTimer: ReturnType<typeof setTimeout> | null = null;
export async function initFileLogging(): Promise<boolean> {
enabled = (await AsyncStorage.getItem(ENABLED_KEY)) === 'true';
if (enabled) {
// Carry over the previous session's log so a crash mid-test isn't lost.
try {
const info = await FileSystem.getInfoAsync(LOG_URI);
if (info.exists) {
const prev = await FileSystem.readAsStringAsync(LOG_URI);
buffer = prev.split('\n').slice(-MAX_LINES);
}
} catch {
/* ignore */
}
}
return enabled;
}
export function isFileLoggingEnabled(): boolean {
return enabled;
}
export async function setFileLogging(on: boolean): Promise<void> {
enabled = on;
await AsyncStorage.setItem(ENABLED_KEY, String(on));
if (on) logLine('--- logging enabled ---');
else await flush();
}
export function logLine(line: string): void {
if (!enabled) return;
buffer.push(`${new Date().toISOString()} ${line}`);
if (buffer.length > MAX_LINES) buffer = buffer.slice(-MAX_LINES);
if (flushTimer) clearTimeout(flushTimer);
flushTimer = setTimeout(() => void flush(), 1200);
}
export async function flush(): Promise<void> {
try {
await FileSystem.writeAsStringAsync(LOG_URI, buffer.join('\n'));
} catch {
/* ignore */
}
}
export async function shareLog(): Promise<void> {
await flush();
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(LOG_URI, {
mimeType: 'text/plain',
dialogTitle: 'BigBrainParking debug log',
});
}
}
export async function clearLog(): Promise<void> {
buffer = [];
try {
await FileSystem.deleteAsync(LOG_URI, { idempotent: true });
} catch {
/* ignore */
}
}
export function logStats(): { lines: number } {
return { lines: buffer.length };
}