COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
POCKET

TestFlight Logging System

TestFlight sessions crash or misbehave with zero visibility:

2026-01-23 // RAW LEARNING CAPTURE
PROJECTPOCKET

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.Logger only 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

FilePurpose
Core/LogManager.swiftFile I/O, pruning, export generation
Core/PocketLogger.swiftPublic API, wraps os.Logger + LogManager
UI/ShakeDetector.swiftUIViewControllerRepresentable for motionEnded
UI/LogExportSheet.swiftUIActivityViewController 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:

FileCategoryNotes
SessionManager.swiftSessionRemoved .public privacy annotations
EtClient.swiftETRemoved .public privacy annotations
SshClient.swiftSSHRemoved .public privacy annotations
TerminalView.swiftTerminalRemoved .public privacy annotations
PocketStore.swiftStoreRemoved .public privacy annotations
BonjourBrowser.swiftNetworkRemoved .public privacy annotations
CryptoHandler.swiftCryptoAdded trace level to PocketLogger
SessionRecording.swiftRecordingNew logger
SessionPlayer.swiftRecordingNew logger
ConnectionView.swiftConnectionNew 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)

  1. Settings UI: Add "Clear Logs" button to Diagnostics section
  2. Log rotation: If file grows beyond 10 MB, rotate to pocket.log.1, compress old logs
  3. Filtering: Allow exporting only ERROR/FAULT logs for signal extraction
  4. Structured logs: Consider JSON-lines format for programmatic analysis
  5. 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).

LOG.ENTRY_END
ref:pocket
RAW