- 01.Problem
- 02.Solution: PocketLogger + LogManager
- 03.Architecture
- 04.Implementation Files
- 05.Log Format
- 06.Export Header
- 07.Migration Work
- 08.UX Integration
- 09.Shake Gesture
- 10.Settings Panel
- 11.Session Recordings Export
- 12.File Management
- 13.Build Script (Bonus)
- 14.SourceKit False Positives
- 15.Testing Notes
- 16.Privacy Annotation Gotcha
- 17.Next Steps (Future)
- 18.Commits
- 19.References
TestFlight Logging System
TestFlight sessions crash or misbehave with zero visibility:
Context: Need visibility into iOS app behavior during TestFlight sessions. Console.app logs aren't accessible from TestFlight builds on physical devices.
Problem
TestFlight sessions crash or misbehave with zero visibility:
os.Loggeronly writes to Console.app (not accessible remotely)- Crash reports show stack traces but no app-level context
- sysdiagnose requires user coordination and produces massive archives
- Remote logging services add dependencies and privacy concerns
Goal: Persistent file logging that users can export on-demand via shake gesture or settings.
Solution: PocketLogger + LogManager
Architecture
PocketLogger (public API)
├─> os.Logger (Console.app, development)
└─> LogManager (file, always-on)
└─> DispatchQueue(.utility) → FileHandle
Key decisions:
- Dual output: Write to both Console.app (for Xcode debugging) and file (for TestFlight)
- Serial queue: Prevent race conditions on FileHandle writes
- Plain text: Human-readable, grep-friendly, no parsing dependencies
- 24-hour retention: Prune logs older than 86400s on launch + every 6 hours
- Shake to export: Low-friction UX for sharing logs with developer
Implementation Files
| File | Purpose |
|---|---|
Core/LogManager.swift | File I/O, pruning, export generation |
Core/PocketLogger.swift | Public API, wraps os.Logger + LogManager |
UI/ShakeDetector.swift | UIViewControllerRepresentable for motionEnded |
UI/LogExportSheet.swift | UIActivityViewController wrapper |
Log Format
2026-01-22T14:30:05.123Z [ERROR] [ET] Connection failed: timeout
- Timestamp: ISO 8601 with milliseconds (UTC)
- Level: DEBUG, INFO, WARNING, ERROR, FAULT, TRACE
- Category: ET, SSH, Session, UI, Store, Network, Terminal, Crypto, Recording, Connection
- Message: Plain string (no privacy redaction markers)
Export Header
When user exports logs, file includes device context:
=== Device Info ===
Model: iPhone 17 Pro
iOS Version: 19.2
App Version: 0.1
Build Number: 2
====================
[logs follow]
Migration Work
Replaced os.Logger with PocketLogger across 10 call sites:
| File | Category | Notes |
|---|---|---|
| SessionManager.swift | Session | Removed .public privacy annotations |
| EtClient.swift | ET | Removed .public privacy annotations |
| SshClient.swift | SSH | Removed .public privacy annotations |
| TerminalView.swift | Terminal | Removed .public privacy annotations |
| PocketStore.swift | Store | Removed .public privacy annotations |
| BonjourBrowser.swift | Network | Removed .public privacy annotations |
| CryptoHandler.swift | Crypto | Added trace level to PocketLogger |
| SessionRecording.swift | Recording | New logger |
| SessionPlayer.swift | Recording | New logger |
| ConnectionView.swift | Connection | New logger |
Build error encountered:
CryptoHandler.swift:47:16: Value of type 'PocketLogger' has no member 'trace'
Fix: Added trace method to PocketLogger (forwards to logger.log(level: .debug)).
UX Integration
Shake Gesture
// RootView.swift
ShakeDetector {
showLogExportSheet = true
}
.sheet(isPresented: $showLogExportSheet) {
LogExportSheet()
}Trigger: Shake device → export sheet appears.
Settings Panel
Added "Diagnostics" section to SettingsView:
- "Export Logs" button (same sheet as shake)
- Shows file path and size
- Future: Add "Clear Logs" button
Session Recordings Export
Added swipe-to-share action on session recordings (bonus feature):
// TerminalDebugView.swift
.swipeActions(edge: .trailing) {
Button {
shareRecording = recording
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
.tint(.blue)
}Reuses ActivityViewSheet pattern from log export.
File Management
Storage:
- Path:
Application Support/Pocket/logs/ - File:
pocket.log - Kept open via
FileHandle(no open/close per write)
Pruning:
- On launch:
LogManager.shared.pruneLogs(olderThan: 86400) - Background timer: Every 6 hours
- Removes entries older than 24 hours (line-by-line scan)
Export:
- Generates temp file with device header
- Appends current log content
- Shares via UIActivityViewController
- Temp file cleaned up by system
Build Script (Bonus)
Created Pocket/run.sh for quick simulator iteration:
#!/bin/bash
set -e
cd "$(dirname "$0")"
echo "Building Pocket..."
xcodebuild -scheme Pocket \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
build 2>&1 | grep -v "DVTCoreDeviceEnabledState" | grep "▸\|error:\|warning:"
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData/Pocket-*/Build/Products/Debug-iphonesimulator -name 'Pocket.app' -type d | head -1)
echo "Installing to simulator..."
xcrun simctl install "iPhone 17 Pro" "$APP_PATH"
echo "Launching Pocket..."
xcrun simctl launch "iPhone 17 Pro" com.digitalpine.pocket
echo "✓ Pocket is running"Iteration cycle: ./run.sh (2.3s build + install + launch).
SourceKit False Positives
During development, SourceKit showed many red errors:
- "No such module 'UIKit'" (in ShakeDetector.swift)
- "Cannot find 'PocketLogger' in scope" (in multiple files)
Cause: SourceKit hadn't indexed new files yet.
Resolution: All errors resolved at build time (zero warnings in final build).
Testing Notes
Manual verification checklist:
- Shake gesture triggers export sheet
- Export includes device header with correct info
- Logs are human-readable (timestamp, level, category, message)
- Logs persist across app restarts
- Pruning removes old entries (test by changing retention to 1 second)
- All 10 migrated call sites write to file
- Console.app still shows logs during Xcode debugging
- Session recording export works (swipe action)
Automated tests: Not implemented (would require mocking FileHandle + URLSession).
Privacy Annotation Gotcha
os.Logger supports privacy redaction:
logger.info("Connected to \(host, privacy: .public)")PocketLogger takes plain String:
logger.info("Connected to \(host)")Migration impact: Removed 6 instances of , privacy: .public from call sites.
Trade-off: File logs are always unredacted (intentional for debugging). Console.app logs remain unredacted for now (could add back if needed).
Next Steps (Future)
- Settings UI: Add "Clear Logs" button to Diagnostics section
- Log rotation: If file grows beyond 10 MB, rotate to
pocket.log.1, compress old logs - Filtering: Allow exporting only ERROR/FAULT logs for signal extraction
- Structured logs: Consider JSON-lines format for programmatic analysis
- Remote upload: Optional "Share with Developer" that uploads to S3 (privacy-gated)
Commits
This work should be committed as:
feat: Add persistent file logging for TestFlight debugging
- Implement LogManager with 24-hour retention and serial queue
- Create PocketLogger API wrapping os.Logger + file output
- Add shake gesture and settings export UI
- Migrate 10 call sites from os.Logger to PocketLogger
- Add session recording export via swipe action
- Create run.sh for quick simulator builds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
References
Files created:
/Users/joel/Code/pocket/Pocket/Pocket/Core/LogManager.swift/Users/joel/Code/pocket/Pocket/Pocket/Core/PocketLogger.swift/Users/joel/Code/pocket/Pocket/Pocket/UI/ShakeDetector.swift/Users/joel/Code/pocket/Pocket/Pocket/UI/LogExportSheet.swift/Users/joel/Code/pocket/Pocket/run.sh
Files modified:
- SessionManager.swift, EtClient.swift, SshClient.swift
- TerminalView.swift, PocketStore.swift, BonjourBrowser.swift
- CryptoHandler.swift, SessionRecording.swift, SessionPlayer.swift
- ConnectionView.swift, RootView.swift, SettingsView.swift
- PocketApp.swift, TerminalDebugView.swift
Build result: Zero warnings, clean build (2.3s on M3).