Skip to content

Avoid PostHog flush deadlock during quit - #6232

Merged
azooz2003-bit merged 11 commits into
mainfrom
feat-posthog-quit-deadlock
Jun 16, 2026
Merged

azooz2003-bit merged 11 commits into
mainfrom
feat-posthog-quit-deadlock

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes a quit/update hang where applicationWillTerminate synchronously waited inside PostHogAnalytics.flush() while the analytics queue was waiting on main-thread notification work.
  • Removes quit-time PostHog flushing entirely. Termination no longer schedules analytics work that may never run or block Sparkle quit-to-install.
  • Keeps active analytics delivery at the useful point: daily/hourly active events flush immediately after capture while the app is still alive.
  • Adds test injection for PostHog capture/flush/defaults/time so analytics behavior can be tested without hitting the SDK or global defaults.

Verification

  • Red commit: PostHogAnalyticsPropertiesTests.flushReturnsWithoutWaitingForBusyWorkQueue() failed with flushReturned.wait(...) == .timedOut.
  • Latest focused suite: xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-phquit-final test-without-building -only-testing:cmuxTests/PostHogAnalyticsPropertiesTests passed 7 Swift Testing tests, including activeEventCaptureFlushesBeforeShutdown().\n- Previous tagged preflight: launched cmux DEV phquit, moved it to LG HDR 4K, confirmed the debug socket, captured rendered UI with Computer Use, then terminated bundle com.cmuxterm.app.debug.phquit through NSRunningApplication.terminate(); pid 16056 reported terminated true within 10 seconds.\n\n## Risk\nTermination no longer makes a last-chance analytics flush. Active events still flush when captured, and quit/update correctness is no longer coupled to telemetry delivery.\n

Summary by CodeRabbit

  • Chores
    • Improved app shutdown/teardown to ensure background crash breadcrumbs and notifications are cleaned up reliably, and to better reflect a normal completion state.
    • Refined CI benchmark behavior by gracefully skipping a step when passwordless privileges aren’t available.
  • Refactor
    • Strengthened analytics handling around active-event capture and flushing, improving reliability during focus changes and app termination.
  • Tests
    • Added new serialized analytics property and flush behavior tests, replacing older coverage with expanded validation for daily/hourly event details and shutdown timing.

@vercel

vercel Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 16, 2026 7:26pm
cmux-staging Building Building Preview, Comment Jun 16, 2026 7:26pm

@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

PostHogAnalytics is refactored to accept injected closures for capture, flush, time, and UserDefaults instead of calling PostHogSDK.shared and Date/UserDefaults directly. The public flush() method and the synchronous work-queue dispatcher are removed. AppDelegate.applicationWillTerminate drops the now-removed conditional flush. Tests are migrated from XCTestCase to Swift Testing and extended to cover flush behavior through the injected closure. A CI workflow step adds passwordless sudo availability checking for the activation benchmark.

Changes

PostHogAnalytics dependency injection refactor and test migration

Layer / File(s) Summary
PostHogAnalytics injectable init and event tracking
Sources/PostHogAnalytics.swift
Adds @unchecked Sendable and replaces the private init() with an initializer storing capturePostHog, flushPostHog, now, and userDefaults closures. Daily and hourly active tracking switch to the injected dependencies for deduplication and event dispatch. The public flush() method and private dispatchSyncOnWorkQueue helper are deleted.
AppDelegate termination teardown
Sources/AppDelegate.swift
Removes the conditional PostHog flush call from applicationWillTerminate and adds explicit teardown: cancel crash breadcrumb task, clear notification store, mark clean exit, log completion, re-enable sudden termination.
Project build wiring for new test file
cmux.xcodeproj/project.pbxproj
Registers PostHogAnalyticsPropertiesTests.swift as a build file, file reference, group child, and source build phase file so it is compiled into the test target.
Swift Testing migration and flush-behavior tests
cmuxTests/GhosttyConfigTests.swift, cmuxTests/PostHogAnalyticsPropertiesTests.swift
Removes the old PostHogAnalyticsPropertiesTests XCTestCase from GhosttyConfigTests.swift. Introduces a new @Suite(.serialized) with #expect/#require assertions. Adds tests for version field omission, flush-policy event name inclusion, and async verification that active event capture triggers the injected flushPostHog closure.

CI workflow sudo availability check

Layer / File(s) Summary
Cmd-Tab activation benchmark sudo guard
.github/workflows/perf-activation.yml
Adds a conditional check for passwordless sudo availability before executing the Aqua session re-entry step; when unavailable, logs a skip message to perf-results/cmd-tab-activation.txt and exits early with success status.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4562: This PR wires cmuxTests/PostHogAnalyticsPropertiesTests.swift into cmux.xcodeproj/project.pbxproj, which is exactly the type of cmuxTests test-target pbxproj wiring that the referenced PR's lint/CI guards verify.
  • manaflow-ai/cmux#4318: Both PRs modify AppDelegate.applicationWillTerminate to adjust StartupBreadcrumbLog breadcrumb recording during shutdown, with this PR adding explicit completion logging.

Poem

🐇 The rabbit swapped hard calls for closures neat,
Injected time and defaults, quite a feat!
The flush no longer hardcodes its SDK way,
A semaphore confirms the tests all say:
"Flush before goodbye!" — hip hip hooray! 🎉


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Source Artifacts ❌ Error The commit adds .claude/scheduled_tasks.lock, a JSON session lock file containing process ID and session metadata. This is local tool output that violates the source-control-artifacts rule. Remove .claude/scheduled_tasks.lock from the commit or add .claude/scheduled_tasks.lock to .gitignore.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: removing PostHog flush during quit to prevent deadlock, which directly aligns with the primary refactoring in AppDelegate and PostHogAnalytics.
Description check ✅ Passed The description includes Summary and Verification sections with clear explanations of changes, testing approach, and outcomes. However, the Testing section from the template (How did you test this change?) is partially addressed under Verification rather than following template structure. The Checklist section is completely missing.
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.
Cmux Swift Actor Isolation ✅ Passed PostHogAnalytics properly uses @unchecked Sendable with documented queue confinement; mutable state confined to workQueue/mainQueue; all helpers marked nonisolated; injected closures marked @Sendab...
Cmux Swift Blocking Runtime ✅ Passed PR removes blocking PostHog flush from production code (AppDelegate, PostHogAnalytics) with zero blocking patterns remaining. Only DispatchSemaphore.wait in test-only scaffolding (allowed).
Cmux Expensive Synchronous Load ✅ Passed PR removes expensive synchronous operations (PostHogAnalytics.flush and dispatchSyncOnWorkQueue) from main/interactive paths; all public methods use asynchronous dispatchAsyncOnWorkQueue; no new ex...
Cmux Cache Substitution Correctness ✅ Passed PR does not replace fresh authoritative reads with cached values. UserDefaults is the intended persistence store for deduplication; current time always read fresh from now(); cold cache handled (em...
Cmux No Hacky Sleeps ✅ Passed PR removes PostHog quit-time flush (resolving deadlock), adds cancellation-aware watchdog timer (3.5s with bounded deadline and Task.isCancelled checks), and CI workflow sleep is package resolution...
Cmux Algorithmic Complexity ✅ Passed All production code changes use O(1) algorithms with fixed-size operations; no nested collection scans, per-target rescans, or unbounded iterations detected. Test code uses fixed-size collections a...
Cmux Swift Concurrency ✅ Passed The PR removes synchronous dispatch (dispatchSyncOnWorkQueue) that caused deadlock and keeps only async confinement. The background DispatchQueue is used for necessary state confinement, not ordi...
Cmux Swift @Concurrent ✅ Passed No violations of swift-concurrent-annotation.md found. PostHogAnalytics uses proper @unchecked Sendable with justified comments, synchronous methods that dispatch to background queue, and no proble...
Cmux Swift File And Package Boundaries ✅ Passed PostHogAnalytics.swift (292 lines) stays well below limits with clear single responsibility; AppDelegate incidentally touched with -3 lines; test code properly organized. No boundary violations.
Cmux Swift Logging ✅ Passed PR introduces no new logging violations. AppDelegate and PostHogAnalytics have no added logging statements; pre-existing NSLog calls are in #if DEBUG blocks (allowed). Test files are exempt per rules.
Cmux User-Facing Error Privacy ✅ Passed No user-facing errors, alerts, or messages violating privacy rules found. Changes are internal (AppDelegate shutdown sequence, PostHogAnalytics refactoring, tests) or workflow-only artifacts writte...
Cmux Full Internationalization ✅ Passed PR contains no user-facing string additions. Changes are limited to: PostHogAnalytics refactoring with debug-only event literals, AppDelegate termination logic, test file (gated with #if canImport)...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI changes—only AppKit delegate, analytics backend, tests, and build config. The swiftui-state-layout check is not applicable.
Cmux Architecture Rethink ✅ Passed PR is a small correctness fix that removes deadlock via quit-time synchronous flush elimination; adds clear work-queue-confinement invariants and dependency injection for testing without introducin...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed This PR does not add or materially change any user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code. Changes are limited to: (1) removing PostHog flush call in app...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-posthog-quit-deadlock

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a quit/update hang by removing the synchronous PostHog flush from applicationWillTerminate, which could deadlock when the analytics workQueue was waiting on main-thread work. Active-event flushing (daily/hourly) continues immediately after capture while the app is alive, so telemetry delivery is not materially affected.

  • Removes PostHogAnalytics.flush() (and the underlying dispatchSyncOnWorkQueue) from the termination path, eliminating the main-thread ↔ work-queue deadlock during quit-to-install.
  • Adds a #if DEBUG-gated makeForTesting factory and injects userDefaults, now, capturePostHog, and flushPostHog as constructor arguments, enabling deterministic analytics tests without touching the PostHog SDK.
  • Migrates the analytics test class from XCTestCase to Swift Testing (@Suite(.serialized)) and adds a new integration-style test that verifies capture + flush ordering through the injected closures.

Confidence Score: 5/5

Safe to merge. The deadlock path is cleanly removed and active-event flushing continues at the right point in the app lifecycle.

The change removes a synchronous cross-queue wait from the termination path — the root cause of the reported hang — and replaces it with nothing, which is correct because active events are already flushed at capture time. The @unchecked Sendable invariants (didStart confined to the work queue, activeCheckTimer confined to the main queue) hold throughout the file. Dependency injection is #if DEBUG-gated and the init stays private, preserving the singleton contract in production. The test coverage is deterministic and exercises the capture+flush ordering end-to-end.

No files require special attention.

Important Files Changed

Filename Overview
Sources/PostHogAnalytics.swift Core fix: removes flush()/dispatchSyncOnWorkQueue, adds dependency injection for testability. @unchecked Sendable is documented and the isolation invariants hold from reading the code — didStart stays on the work queue, activeCheckTimer stays on the main queue.
Sources/AppDelegate.swift Removes the conditional PostHogAnalytics.shared.flush() from applicationWillTerminate; cleanup ordering for crash breadcrumb task and notification store is unchanged.
cmuxTests/PostHogAnalyticsPropertiesTests.swift New Swift Testing suite using injected closures and semaphores for synchronization. Semaphore use is test-only scaffolding and deterministic; no production behavioral concerns.
cmuxTests/GhosttyConfigTests.swift Removes the older XCTest-based PostHogAnalyticsPropertiesTests class now covered by the new Swift Testing suite.
cmux.xcodeproj/project.pbxproj Adds the new test file reference and build membership. The new Sources entry has one fewer leading tab than its nearest neighbor, but pbxproj is resilient to local indentation differences and Xcode re-normalizes on save.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Main as Main Thread
    participant WQ as workQueue
    participant SDK as PostHogSDK

    Note over Main,SDK: BEFORE (deadlock path)
    Main->>Main: applicationWillTerminate
    Main->>WQ: "workQueue.sync { flush() }"
    WQ-->>Main: (blocked waiting for main-thread notification work)
    Main-->>WQ: (blocked waiting for workQueue)
    Note over Main,WQ: DEADLOCK

    Note over Main,SDK: AFTER (this PR)
    Main->>Main: applicationWillTerminate
    Note over Main: PostHog flush removed entirely
    Main->>Main: cancel breadcrumb task, clearAll, markCleanExit

    Note over Main,SDK: Active-event flush (while alive)
    Main->>WQ: trackActive() [async]
    WQ->>SDK: capturePostHog(daily_active)
    WQ->>SDK: capturePostHog(hourly_active)
    WQ->>SDK: flushPostHog() [once, if anything captured]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Main as Main Thread
    participant WQ as workQueue
    participant SDK as PostHogSDK

    Note over Main,SDK: BEFORE (deadlock path)
    Main->>Main: applicationWillTerminate
    Main->>WQ: "workQueue.sync { flush() }"
    WQ-->>Main: (blocked waiting for main-thread notification work)
    Main-->>WQ: (blocked waiting for workQueue)
    Note over Main,WQ: DEADLOCK

    Note over Main,SDK: AFTER (this PR)
    Main->>Main: applicationWillTerminate
    Note over Main: PostHog flush removed entirely
    Main->>Main: cancel breadcrumb task, clearAll, markCleanExit

    Note over Main,SDK: Active-event flush (while alive)
    Main->>WQ: trackActive() [async]
    WQ->>SDK: capturePostHog(daily_active)
    WQ->>SDK: capturePostHog(hourly_active)
    WQ->>SDK: flushPostHog() [once, if anything captured]
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/PostHogAnalytics.swift (1)

14-15: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider making event name constants static to eliminate duplication.

The event names "cmux_daily_active" and "cmux_hourly_active" appear as instance properties (lines 14-15) and as hardcoded literals in the static shouldFlushAfterCapture (lines 273). If someone changes one location but not the other, the flush-after-capture behavior would silently break.

♻️ Suggested refactor
-    private let dailyActiveEvent = "cmux_daily_active"
-    private let hourlyActiveEvent = "cmux_hourly_active"
+    private static let dailyActiveEvent = "cmux_daily_active"
+    private static let hourlyActiveEvent = "cmux_hourly_active"

Then update shouldFlushAfterCapture:

     nonisolated static func shouldFlushAfterCapture(event: String) -> Bool {
         switch event {
-        case "cmux_daily_active", "cmux_hourly_active":
+        case dailyActiveEvent, hourlyActiveEvent:
             return true
         default:
             return false
         }
     }

And update references in instance methods (e.g., let event = Self.dailyActiveEvent).

Also applies to: 271-278

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/PostHogAnalytics.swift` around lines 14 - 15, Convert the event name
constants dailyActiveEvent and hourlyActiveEvent from instance properties to
static properties at lines 14-15. Then update the static method
shouldFlushAfterCapture (lines 271-278) to reference these static properties
instead of using hardcoded string literals. Finally, update any instance method
references to use Self.dailyActiveEvent and Self.hourlyActiveEvent instead of
accessing them as instance properties, ensuring the event names are defined and
referenced from a single source.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/PostHogAnalytics.swift`:
- Around line 14-15: Convert the event name constants dailyActiveEvent and
hourlyActiveEvent from instance properties to static properties at lines 14-15.
Then update the static method shouldFlushAfterCapture (lines 271-278) to
reference these static properties instead of using hardcoded string literals.
Finally, update any instance method references to use Self.dailyActiveEvent and
Self.hourlyActiveEvent instead of accessing them as instance properties,
ensuring the event names are defined and referenced from a single source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ca43ffa-d887-4ade-8b41-be4b65ca6cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 952f853 and 9216d19.

📒 Files selected for processing (2)
  • Sources/PostHogAnalytics.swift
  • cmuxTests/GhosttyConfigTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/PostHogAnalyticsPropertiesTests.swift`:
- Around line 130-132: The test waits for only one flushCalled signal on line
130 before asserting both daily and hourly active events on line 132, creating a
race condition. Modify the wait logic to account for both flush events: either
call flushCalled.wait() twice consecutively with appropriate timeouts, or
restructure the synchronization to ensure both flushes are captured before the
assertion that validates the captured events contain both "cmux_daily_active"
and "cmux_hourly_active" entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dc57c362-7f36-489d-b6f6-acdc343d62a9

📥 Commits

Reviewing files that changed from the base of the PR and between 9216d19 and d8d59ee.

📒 Files selected for processing (4)
  • Sources/PostHogAnalytics.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/GhosttyConfigTests.swift
  • cmuxTests/PostHogAnalyticsPropertiesTests.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/GhosttyConfigTests.swift

Comment thread cmuxTests/PostHogAnalyticsPropertiesTests.swift
@azooz2003-bit
azooz2003-bit merged commit 3041a8e into main Jun 16, 2026
20 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-posthog-quit-deadlock branch June 16, 2026 19:26

This branch was successfully deployed

1 active deployment
Preview – cmux — 7753a9d5 Deployed Jun 16, 2026 by vercel[bot]
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.

1 participant