-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Avoid macOS 15 DevTools attach crash and analytics quit hang #4981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,36 @@ struct BrowserRemoteWorkspaceStatus: Equatable { | |
| let lastHeartbeatAt: Date? | ||
| } | ||
|
|
||
| enum BrowserDeveloperToolsAttachmentPolicy { | ||
| private static let firstStableAttachedInspectorMajorVersion = 26 | ||
|
|
||
| #if DEBUG | ||
| private nonisolated(unsafe) static var attachedInspectorAllowedOverrideForTesting: Bool? | ||
|
|
||
| static func withAttachedInspectorAllowedForTesting<T>( | ||
| _ allowed: Bool?, | ||
| _ body: () throws -> T | ||
| ) rethrows -> T { | ||
| let previous = attachedInspectorAllowedOverrideForTesting | ||
| attachedInspectorAllowedOverrideForTesting = allowed | ||
| defer { attachedInspectorAllowedOverrideForTesting = previous } | ||
| return try body() | ||
| } | ||
| #endif | ||
|
|
||
| static func allowsAttachedInspector( | ||
| osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion | ||
| ) -> Bool { | ||
| #if DEBUG | ||
| if let attachedInspectorAllowedOverrideForTesting { | ||
| return attachedInspectorAllowedOverrideForTesting | ||
| } | ||
| #endif | ||
| // macOS 15 WebKit 20621 crashes inside WebInspectorUIProxy::platformAttach. | ||
| return osVersion.majorVersion >= firstStableAttachedInspectorMajorVersion | ||
| } | ||
| } | ||
|
Comment on lines
+42
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| enum GhosttyBackgroundTheme { | ||
| static func clampedOpacity(_ opacity: Double) -> CGFloat { | ||
| WindowAppearanceSnapshot.clampedOpacity(opacity) | ||
|
|
@@ -6300,6 +6330,10 @@ extension BrowserPanel { | |
| } | ||
|
|
||
| private func prepareDeveloperToolsForRevealIfNeeded(_ inspector: NSObject) { | ||
| guard BrowserDeveloperToolsAttachmentPolicy.allowsAttachedInspector() else { | ||
| setPreferredDeveloperToolsPresentation(.detached) | ||
| return | ||
| } | ||
| if preferredDeveloperToolsPresentation != .unknown { | ||
| guard preferredDeveloperToolsPresentation == .attached else { return } | ||
| guard webView.superview != nil, webView.window != nil else { return } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,17 +21,46 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
| private let workQueueSpecificKey = DispatchSpecificKey<Void>() | ||||||||||||||||||||||||||||||
| private let utcHourFormatter: DateFormatter | ||||||||||||||||||||||||||||||
| private let utcDayFormatter: DateFormatter | ||||||||||||||||||||||||||||||
| private let flushImplementation: () -> Void | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private var didStart = false | ||||||||||||||||||||||||||||||
| private var activeCheckTimer: Timer? | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private init() { | ||||||||||||||||||||||||||||||
| workQueue = DispatchQueue(label: "com.cmux.posthog.analytics", qos: .utility) | ||||||||||||||||||||||||||||||
| utcHourFormatter = Self.makeUTCFormatter("yyyy-MM-dd'T'HH") | ||||||||||||||||||||||||||||||
| utcDayFormatter = Self.makeUTCFormatter("yyyy-MM-dd") | ||||||||||||||||||||||||||||||
| self.workQueue = DispatchQueue(label: "com.cmux.posthog.analytics", qos: .utility) | ||||||||||||||||||||||||||||||
| self.utcHourFormatter = Self.makeUTCFormatter("yyyy-MM-dd'T'HH") | ||||||||||||||||||||||||||||||
| self.utcDayFormatter = Self.makeUTCFormatter("yyyy-MM-dd") | ||||||||||||||||||||||||||||||
| self.flushImplementation = { PostHogSDK.shared.flush() } | ||||||||||||||||||||||||||||||
| workQueue.setSpecific(key: workQueueSpecificKey, value: ()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private init( | ||||||||||||||||||||||||||||||
| workQueue: DispatchQueue, | ||||||||||||||||||||||||||||||
| didStart: Bool, | ||||||||||||||||||||||||||||||
| flushImplementation: @escaping () -> Void | ||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||
| self.workQueue = workQueue | ||||||||||||||||||||||||||||||
| self.utcHourFormatter = Self.makeUTCFormatter("yyyy-MM-dd'T'HH") | ||||||||||||||||||||||||||||||
| self.utcDayFormatter = Self.makeUTCFormatter("yyyy-MM-dd") | ||||||||||||||||||||||||||||||
| self.flushImplementation = flushImplementation | ||||||||||||||||||||||||||||||
| self.didStart = didStart | ||||||||||||||||||||||||||||||
| workQueue.setSpecific(key: workQueueSpecificKey, value: ()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||||
| static func makeForTesting( | ||||||||||||||||||||||||||||||
| workQueue: DispatchQueue, | ||||||||||||||||||||||||||||||
| didStart: Bool, | ||||||||||||||||||||||||||||||
| flushImplementation: @escaping () -> Void | ||||||||||||||||||||||||||||||
| ) -> PostHogAnalytics { | ||||||||||||||||||||||||||||||
| PostHogAnalytics( | ||||||||||||||||||||||||||||||
| workQueue: workQueue, | ||||||||||||||||||||||||||||||
| didStart: didStart, | ||||||||||||||||||||||||||||||
| flushImplementation: flushImplementation | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private var isEnabled: Bool { | ||||||||||||||||||||||||||||||
| guard TelemetrySettings.enabledForCurrentLaunch else { return false } | ||||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||||
|
|
@@ -56,7 +85,7 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
| let didCaptureHourly = self.trackHourlyActiveOnWorkQueue(reason: reason, flush: false) | ||||||||||||||||||||||||||||||
| if didCaptureDaily || didCaptureHourly { | ||||||||||||||||||||||||||||||
| // On app focus we can capture both events; flush once to reduce extra work. | ||||||||||||||||||||||||||||||
| PostHogSDK.shared.flush() | ||||||||||||||||||||||||||||||
| self.flushImplementation() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -74,9 +103,9 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func flush() { | ||||||||||||||||||||||||||||||
| dispatchSyncOnWorkQueue { | ||||||||||||||||||||||||||||||
| guard didStart else { return } | ||||||||||||||||||||||||||||||
| PostHogSDK.shared.flush() | ||||||||||||||||||||||||||||||
| dispatchAsyncOnWorkQueue { [weak self] in | ||||||||||||||||||||||||||||||
| guard let self, self.didStart else { return } | ||||||||||||||||||||||||||||||
| self.flushImplementation() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
105
to
109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make Line 106 still goes through Suggested fix func flush() {
- dispatchAsyncOnWorkQueue { [weak self] in
+ workQueue.async { [weak self] in
guard let self, self.didStart else { return }
self.flushImplementation()
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
105
to
110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The quit-time caller in |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -144,7 +173,7 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if flush && Self.shouldFlushAfterCapture(event: event) { | ||||||||||||||||||||||||||||||
| // For active metrics we care more about delivery than batching. | ||||||||||||||||||||||||||||||
| PostHogSDK.shared.flush() | ||||||||||||||||||||||||||||||
| flushImplementation() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||
|
|
@@ -176,7 +205,7 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if flush && Self.shouldFlushAfterCapture(event: event) { | ||||||||||||||||||||||||||||||
| // Keep hourly freshness and avoid losing a deduped hour on abrupt exits. | ||||||||||||||||||||||||||||||
| PostHogSDK.shared.flush() | ||||||||||||||||||||||||||||||
| flushImplementation() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||
|
|
@@ -190,14 +219,6 @@ final class PostHogAnalytics { | |||||||||||||||||||||||||||||
| workQueue.async(execute: block) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private func dispatchSyncOnWorkQueue(_ block: () -> Void) { | ||||||||||||||||||||||||||||||
| if DispatchQueue.getSpecific(key: workQueueSpecificKey) != nil { | ||||||||||||||||||||||||||||||
| block() | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| workQueue.sync(execute: block) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private func utcHourString(_ date: Date) -> String { | ||||||||||||||||||||||||||||||
| utcHourFormatter.string(from: date) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3362,6 +3362,36 @@ final class UITestLaunchManifestTests: XCTestCase { | |
| } | ||
|
|
||
| final class PostHogAnalyticsPropertiesTests: XCTestCase { | ||
| func testFlushReturnsWithoutWaitingForBusyAnalyticsQueue() { | ||
| let queue = DispatchQueue(label: "com.cmux.posthog.analytics.test") | ||
| let queuedWorkStarted = DispatchSemaphore(value: 0) | ||
| let releaseQueuedWork = DispatchSemaphore(value: 0) | ||
| queue.async { | ||
| queuedWorkStarted.signal() | ||
| _ = releaseQueuedWork.wait(timeout: .now() + 5) | ||
| } | ||
| XCTAssertEqual(queuedWorkStarted.wait(timeout: .now() + 1), .success) | ||
|
|
||
| let analytics = PostHogAnalytics.makeForTesting( | ||
| workQueue: queue, | ||
| didStart: true, | ||
| flushImplementation: {} | ||
| ) | ||
| let flushReturned = expectation(description: "flush returned") | ||
| DispatchQueue.global(qos: .userInitiated).async { | ||
| analytics.flush() | ||
| flushReturned.fulfill() | ||
| } | ||
|
|
||
| let result = XCTWaiter().wait(for: [flushReturned], timeout: 0.2) | ||
| releaseQueuedWork.signal() | ||
| XCTAssertEqual( | ||
| result, | ||
| .completed, | ||
| "PostHogAnalytics.flush must not synchronously wait for the analytics queue while the main thread is terminating." | ||
| ) | ||
|
Comment on lines
+3375
to
+3392
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Assert that the queued flush eventually runs. This test proves Suggested strengthening- let analytics = PostHogAnalytics.makeForTesting(
+ let didExecuteFlush = expectation(description: "flush executed")
+ let analytics = PostHogAnalytics.makeForTesting(
workQueue: queue,
didStart: true,
- flushImplementation: {}
+ flushImplementation: {
+ didExecuteFlush.fulfill()
+ }
)
let flushReturned = expectation(description: "flush returned")
DispatchQueue.global(qos: .userInitiated).async {
analytics.flush()
flushReturned.fulfill()
@@
releaseQueuedWork.signal()
XCTAssertEqual(
result,
.completed,
"PostHogAnalytics.flush must not synchronously wait for the analytics queue while the main thread is terminating."
)
+ wait(for: [didExecuteFlush], timeout: 1.0)🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func testDailyActivePropertiesIncludeVersionAndBuild() { | ||
| let properties = PostHogAnalytics.dailyActiveProperties( | ||
| dayUTC: "2026-02-21", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the OS major-version threshold;
26effectively disables attached inspector for all current macOS releases.ProcessInfo.processInfo.operatingSystemVersion.majorVersionreturns macOS major versions (15/16/…), so>= 26keeps attached inspector disabled far beyond the intended macOS 15 workaround. This broadens the fallback behavior and likely regresses attach support on unaffected versions.Suggested fix
enum BrowserDeveloperToolsAttachmentPolicy { - private static let firstStableAttachedInspectorMajorVersion = 26 + private static let firstStableAttachedInspectorMajorVersion = 16Also applies to: 68-68
🤖 Prompt for AI Agents