Skip to content

feat(swift-sdk): in-app log export for beta diagnostics#4131

Merged
QuantumExplorer merged 2 commits into
v4.1-devfrom
claude/suspicious-moser-8cf3ed
Jul 15, 2026
Merged

feat(swift-sdk): in-app log export for beta diagnostics#4131
QuantumExplorer merged 2 commits into
v4.1-devfrom
claude/suspicious-moser-8cf3ed

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 15, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Beta testers hitting crashes or sync issues have no way to get diagnostics off their device — the per-session Rust tracing logs under Library/Logs/SwiftDashSDK/<timestamp>/ were exactly what diagnosed the recent startup crash-loop (rust-dashcore#892/#893) and the sync-height regression (#4130), but so far the only way to retrieve them was get_logs.sh against a simulator. Requested by @dustinface in team chat.

What was done?

  • New "Diagnostics → Export Logs" row in SwiftExampleApp's Settings tab. Tapping it zips the most recent log sessions and presents a share sheet (AirDrop / Mail / Save to Files). Nothing is uploaded automatically — export is strictly user-initiated.
  • New LogExporter (app-side only): includes the current session plus up to two before it (after a crash, the crashed run is usually the previous session — no .ips parsing needed), capped at 15 MB raw input; always includes the current session. Adds a summary.txt with app version, git commit, OS, hardware, network, and per-session sizes/roles. Zipping uses NSFileCoordinator .forUploading — no third-party dependencies.
  • LoggingPreferences (SwiftDashSDK): removed the simulator-only gate so real devices also write per-session run.log files (otherwise there would be nothing to export in the field), exposed logsRootDirectory/currentSessionDirectory, and added startup pruning to the newest 20 sessions so device disk use stays bounded.

No FFI or Rust changes — platform_wallet_enable_file_logging was already exposed; this is pure Swift app/SDK UI + file handling per packages/swift-sdk/CLAUDE.md.

How Has This Been Tested?

  • Built via bash packages/swift-sdk/build_ios.sh --target sim (Rust xcframework + SwiftDashSDK + SwiftExampleApp, warnings-as-errors) — BUILD SUCCEEDED.
  • On a booted iPhone 16 (iOS 18.6) simulator: tapped Settings → Diagnostics → Export Logs; share sheet appeared with SwiftDashSDK-logs-2026-07-15T04-25-36Z.zip (316 KB).
  • Inspected the zip from the app container: it contained both session directories present on the sim (all per-crate run.log/metrics.log files intact) plus summary.txt. The size cap was exercised for real — the third-newest session (>10 MB) was correctly dropped, and the summary reported "Included 2 of 14 session(s) … (limit: 3 sessions / 15.7 MB raw)".

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable SDK file-log storage with access to the current logging session (and retention bounds).
    • Added a new Diagnostics option to export recent SDK logs as a shareable ZIP, including a generated summary with session directory names, sizes, and device/network/OS context.
  • Bug Fixes

    • Enabled file logging whenever a session directory is available.
    • Added automated pruning of older log sessions based on both maximum retained session count and total retained bytes (without deleting the current session).
  • Tests

    • Added selection logic coverage for log export session ordering and size limits.

Adds an Export Logs row to SwiftExampleApp's Settings tab that zips
the most recent SDK log sessions (current + up to two before it,
15 MB raw cap) together with a summary.txt of build/device/network
context, and hands the archive to a share sheet (AirDrop / Mail /
Files). Nothing is uploaded automatically — export is user-initiated.

To make this useful outside the simulator, file logging is no longer
simulator-gated: devices now write the same per-session run.log
files, with startup pruning to the newest 20 sessions so disk use
stays bounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d8b04de-4448-4bed-afaf-ebc0f55c8a45

📥 Commits

Reviewing files that changed from the base of the PR and between 7241245 and fd6e7e4.

📒 Files selected for processing (4)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/LogExporterSelectionTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift

📝 Walkthrough

Walkthrough

SwiftDashSDK now creates and retains file-log session directories. The Swift example app selects recent sessions, packages them with diagnostic metadata into a zip archive, and provides a Diagnostics UI for sharing the archive or displaying export failures.

Changes

Log session export

Layer / File(s) Summary
Session directory lifecycle
packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift
Logging preferences expose session locations, enable file logging when a session directory is available, and asynchronously prune older sessions by count and byte limits.
Log archive generation
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/LogExporterSelectionTests.swift
Recent sessions are selected with current-session priority and count/size limits, copied into staging, documented with summary.txt, and archived with explicit export errors and selection tests.
Diagnostics export interface
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
The Diagnostics section starts detached exports, tracks progress and failures, and presents successful archives through a share sheet.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OptionsView
  participant LogExporter
  participant LoggingPreferences
  participant FileManager
  OptionsView->>LogExporter: export(network, appVersion, currentSession)
  LogExporter->>LoggingPreferences: read logsRootDirectory
  LogExporter->>FileManager: select and stage log sessions
  LogExporter->>FileManager: write summary.txt and zip archive
  LogExporter-->>OptionsView: archive URL or export error
  OptionsView->>OptionsView: present share sheet or failure alert
Loading

Possibly related PRs

  • dashpay/platform#3785: Introduces the file-logging capability used by this session setup and export flow.

Suggested reviewers: shumkov, llbartekll, zocolini

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding in-app log export for beta diagnostics.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/suspicious-moser-8cf3ed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

⛔ Blockers found — Sonnet deferred (commit fd6e7e4)
Canonical validated blockers: 1

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The user-initiated export flow is appropriately scoped, but four verified lifecycle and metadata defects make its diagnostics unreliable and leave device storage unbounded. Logging retries create false sessions, export selection can omit the active session, normal builds report a stale commit, and the new behavior has no automated coverage.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 4 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift:54-60: Make logging configuration idempotent across bootstrap retries
  `retryBootstrap()` calls `bootstrap()` again, which invokes `LoggingPreferences.configure()` on every attempt. The Rust initializer creates seven log files and writes `build_info.txt` before `try_init()` discovers that the process-global subscriber is already installed. Each delayed retry therefore leaves a newer empty session directory, and pruning is skipped because the initializer returned false. The console fallback also cannot replace the existing subscriber, so logging continues into the original directory while the exporter sees the empty retry directories as newer sessions. Configure logging only once per process, or make the FFI initializer check subscriber availability before creating the session.
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift:26-29: Bound retained logs by bytes, not only session count
  Retaining20 directory names does not bound device storage as claimed. The Rust backend opens every log in append mode without rotation or a size check, so the active session and every retained session can be arbitrarily large. Development iOS artifacts additionally emit runtime metrics every second; release artifacts still have unbounded info-level `run.log` growth. The exporter's15 MB limit only controls what is shared, not what remains on disk. Add active-file rotation or enforce a total byte quota now that file logging is enabled on physical devices.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift:61-75: Select the authoritative current session before sorting older sessions
  The exporter ignores `LoggingPreferences.currentSessionDirectory` and assumes the lexicographically newest timestamp is the active session. A clock rollback, a future-dated stale directory, or the empty directories produced by bootstrap retries can rank ahead of the real writer. Three such entries make `prefix(maxSessions)` omit the active session entirely, while a large earlier entry can trigger the byte-cap break before it is selected. The summary then labels the wrong directory as current. Pass the recorded current session into the export operation and include it first, applying timestamp sorting and the byte cap only to older sessions.
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift:182: Generate the exported commit identifier during the build
  `AppVersion.gitCommit` is the checked-in value `814df`, while this PR's head is `72412`. The app target has no shell-script build phase and the shared scheme has no pre-action invoking `generate-version.sh`. Even if invoked, that script writes `${SRCROOT}/Version.swift`, while the compiled synchronized source is `${SRCROOT}/SwiftExampleApp/Version.swift`. Normal builds therefore cannot refresh the value, and every diagnostic archive reports misleading commit metadata. Integrate commit generation into the target build and write the generated value to a compiled source or build setting.
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift:48-75: Add tests for log selection and logging lifecycle
  No tests were added for the new selection, byte-cap, archive, pruning, or repeated-configuration behavior. Add tests with injectable temporary log roots and an explicit current session to verify archive contents, current-session inclusion, cap behavior, retention, and bootstrap retry idempotence. Use an isolated process for the global Rust subscriber lifecycle where necessary.

Comment thread packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift Outdated
Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift Outdated
Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift Outdated
Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/LogExporter.swift Outdated
- configure() installs the logging pipeline at most once per process,
  so bootstrap retries no longer create decoy empty session dirs
- startup pruning enforces a 100 MB total-bytes quota in addition to
  the 20-session count cap (current session always kept)
- the exporter takes the authoritative currentSessionDirectory from
  LoggingPreferences instead of trusting timestamp order; selection
  is a pure, unit-tested function
- summary.txt drops the stale checked-in AppVersion.gitCommit and
  points at the per-session build_info.txt, which records the exact
  compile-time platform-wallet commit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit dc1d645 into v4.1-dev Jul 15, 2026
18 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/suspicious-moser-8cf3ed branch July 15, 2026 06:41

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

At exact head fd6e7e4, the fixes correctly make logging installation idempotent, select the authoritative current session, and export build-generated provenance. Device log storage remains unbounded during a long-running current session, and the logging lifecycle and retention fixes still lack regression coverage.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift:136-147: The current session can still grow without a byte bound
  [prior id=cec1a78fbe14] The 100 MB quota runs only during startup pruning, which unconditionally retains the current directory even when it exceeds the limit. The Rust logger opens seven files with append mode and installs those writers for the process lifetime (`rs-platform-wallet-ffi/src/logging.rs:39-60,142-151`), with no rotation or live size enforcement. A long-running device session can therefore grow indefinitely until a later launch, contradicting the PR's stated requirement that device log storage remain bounded. Additionally, when a clock rollback makes the current session sort behind newer-stamped old directories, entries retained before reaching the current directory can leave the startup total above 100 MB as well. Add live file rotation or another enforceable current-session limit and ensure pruning cannot retain earlier history after the current directory pushes the total over quota.
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/SDKLogger.swift:56-77: Cover the logging lifecycle fixes with regression tests
  [prior id=c0db94d40ec7] `LogExporterSelectionTests` now covers authoritative selection, ordering, count limits, and export byte limits. However, the test tree still has no coverage for repeated `configure()` calls, preservation of `currentSessionDirectory`, or startup pruning across the session-count, retained-byte, and current-session rules at `SDKLogger.swift:116-169`. These paths implement the fixes for two prior blocking regressions and should have focused lifecycle and filesystem-policy tests.

Comment on lines +136 to +147
if session.lastPathComponent == currentName {
// Counted against the byte quota so a huge
// just-finished-syncing session pushes old
// history out, but never deleted itself.
keptCount += 1
keptBytes += directoryBytes(of: session)
continue
}
let bytes = directoryBytes(of: session)
if keptCount >= maxRetainedSessions
|| keptBytes + bytes > maxRetainedBytes {
try? fm.removeItem(at: session)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: The current session can still grow without a byte bound

[prior id=cec1a78fbe14] The 100 MB quota runs only during startup pruning, which unconditionally retains the current directory even when it exceeds the limit. The Rust logger opens seven files with append mode and installs those writers for the process lifetime (rs-platform-wallet-ffi/src/logging.rs:39-60,142-151), with no rotation or live size enforcement. A long-running device session can therefore grow indefinitely until a later launch, contradicting the PR's stated requirement that device log storage remain bounded. Additionally, when a clock rollback makes the current session sort behind newer-stamped old directories, entries retained before reaching the current directory can leave the startup total above 100 MB as well. Add live file rotation or another enforceable current-session limit and ensure pruning cannot retain earlier history after the current directory pushes the total over quota.

source: ['codex']

Comment on lines +56 to +77
/// The tracing subscriber is process-global and first-init-wins,
/// so the install must run at most once per process. Without
/// this guard, every `configure()` call after the first (e.g.
/// a bootstrap retry) would make the Rust initializer lay out a
/// fresh session directory of empty log files before `try_init`
/// discovers the existing subscriber and bails — leaving decoy
/// "newest" sessions that logging never writes to.
@MainActor
private static var didInstallLogging = false

@discardableResult
@MainActor
public static func configure() -> LoggingPreset {
let preset = loadPreset()
let enableSwiftVerbose: Bool

// File logging is gated to the iOS Simulator
#if targetEnvironment(simulator)
if let sessionRoot = launchLogPaths(),
SDK.enableFileLogging(level: .info, sessionRoot: sessionRoot) {
} else {
SDK.enableLogging(level: .info)
if !didInstallLogging {
didInstallLogging = true
if let sessionRoot = launchLogPaths(),
SDK.enableFileLogging(level: .info, sessionRoot: sessionRoot.path) {
currentSessionDirectory = sessionRoot
pruneOldSessions(keeping: sessionRoot)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Cover the logging lifecycle fixes with regression tests

[prior id=c0db94d40ec7] LogExporterSelectionTests now covers authoritative selection, ordering, count limits, and export byte limits. However, the test tree still has no coverage for repeated configure() calls, preservation of currentSessionDirectory, or startup pruning across the session-count, retained-byte, and current-session rules at SDKLogger.swift:116-169. These paths implement the fixes for two prior blocking regressions and should have focused lifecycle and filesystem-policy tests.

source: ['codex']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants