From 938d6d917547e7ca9b4ea8619451e60dfd4a4198 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 14 Apr 2026 17:43:44 +0200 Subject: [PATCH 1/2] fix: Unsubscribe system event breadcrumbs during background Per Apple's docs (https://developer.apple.com/documentation/uikit/processing-queued-notifications?language=objc), iOS queues notifications while an app is suspended and delivers them on resume. The time we report them, might not align witht he time they were triggered, so they should not provide diagnostic value, so we might as well not use them. This also aligns us with Android behaviour, which implemented the equivalent issue in https://github.com/getsentry/sentry-java/pull/4338. This PR makes SentrySystemEventBreadcrumbs lifecycle-aware: unsubscribes from system event notifications on didEnterBackgroundNotification, re-subscribes on willEnterForegroundNotification. An isSubscribedToSystemEvents flag makes both operations idempotent. Also fixes AND/OR matching bug in TestNSNotificationCenterWrapper.removeObserver(_:name:object:). This is being done with #4580 in mind. The reported SIGPIPE crashes show snprintf in the stack trace, but snprintf writes to a stack buffer and cannot generate SIGPIPE. Our hypothesis is that the signal originates elsewhere (e.g. stale network connections breaking after resume) and is delivered asynchronously to the main thread while it happens to be encoding a breadcrumb. SentryCrash treats SIGPIPE as fatal, so the crash report captures whatever the thread was doing at signal delivery time. So while this will in all likleyhood not fix the underlying problem, it might reduce noise so we can find the root cause. --- .../TestNSNotificationCenterWrapper.swift | 10 +++- .../SentrySystemEventBreadcrumbs.swift | 53 ++++++++++++++++-- .../SentrySystemEventBreadcrumbsTest.swift | 56 +++++++++++++++++++ 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/SentryTestUtils/Sources/TestNSNotificationCenterWrapper.swift b/SentryTestUtils/Sources/TestNSNotificationCenterWrapper.swift index 9b717b54324..d8595f51e33 100644 --- a/SentryTestUtils/Sources/TestNSNotificationCenterWrapper.swift +++ b/SentryTestUtils/Sources/TestNSNotificationCenterWrapper.swift @@ -82,10 +82,14 @@ public typealias CrossPlatformApplication = NSApplication observers.removeAll { item in switch item { case .observerWithObject(let weakObserver, _, let name, let object): - return (weakObserver.value === observer as? NSObject) || - (name == aName && ((object == nil && anObject == nil) || (object as AnyObject === anObject as AnyObject))) + guard weakObserver.value === observer as AnyObject else { return false } + if let aName = aName, name != aName { return false } + if anObject != nil && object as AnyObject? !== anObject as AnyObject? { return false } + return true case .observerWithBlock(let weakObserver, let name, _): - return (weakObserver.value === observer as? NSObject) || name == aName + guard weakObserver.value === observer as AnyObject else { return false } + if let aName = aName, name != aName { return false } + return true default: return false } diff --git a/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift b/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift index 320743ef75e..7f47c532255 100644 --- a/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift +++ b/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift @@ -17,6 +17,9 @@ final class SentrySystemEventBreadcrumbs: NSObject { private var didBeginGeneratingOrientationNotifications = false #endif + /// Whether system event observers are currently registered. + private var isSubscribedToSystemEvents = false + init( currentDeviceProvider: SentryUIDeviceWrapperProvider, fileManager: SentryFileManager, @@ -38,6 +41,48 @@ final class SentrySystemEventBreadcrumbs: NSObject { func start(with delegate: SentryBreadcrumbDelegate) { self.delegate = delegate + + notificationCenterWrapper.addObserver( + self, + selector: #selector(didEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + notificationCenterWrapper.addObserver( + self, + selector: #selector(willEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + + subscribeToSystemEvents() + } + + func timezoneEventTriggered() { + timezoneEventTriggered(storedTimezoneOffset: nil) + } + + func stop() { + notificationCenterWrapper.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + notificationCenterWrapper.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + + unsubscribeFromSystemEvents() + } + + // MARK: - Lifecycle + + @objc private func didEnterBackground() { + unsubscribeFromSystemEvents() + } + + @objc private func willEnterForeground() { + subscribeToSystemEvents() + } + + private func subscribeToSystemEvents() { + guard !isSubscribedToSystemEvents else { return } + isSubscribedToSystemEvents = true + #if os(iOS) initBatteryObserver(currentDeviceProvider.uiDeviceWrapper.currentDevice) initOrientationObserver(currentDeviceProvider.uiDeviceWrapper.currentDevice) @@ -48,14 +93,12 @@ final class SentrySystemEventBreadcrumbs: NSObject { initSignificantTimeChangeObserver() } - func timezoneEventTriggered() { - timezoneEventTriggered(storedTimezoneOffset: nil) - } + private func unsubscribeFromSystemEvents() { + guard isSubscribedToSystemEvents else { return } + isSubscribedToSystemEvents = false - func stop() { // Remove the observers with the most specific detail possible, see // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1413994-removeobserver - notificationCenterWrapper.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil) notificationCenterWrapper.removeObserver(self, name: UIApplication.significantTimeChangeNotification, object: nil) notificationCenterWrapper.removeObserver(self, name: NSNotification.Name.NSSystemTimeZoneDidChange, object: nil) diff --git a/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift b/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift index 0c0aa09a533..a6fef5a7e6a 100644 --- a/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift +++ b/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift @@ -373,8 +373,64 @@ class SentrySystemEventBreadcrumbsTest: XCTestCase { func testStopCallsSpecificRemoveObserverMethods() { sut = fixture.getSut(currentDevice: nil) sut.stop() + // 8 system event observers + 2 lifecycle observers (didEnterBackground, willEnterForeground) + XCTAssertEqual(fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.count, 10) + } + + // MARK: - Lifecycle Tests + + func testBackgroundUnsubscribesFromSystemEvents() { + sut = fixture.getSut(currentDevice: nil) + fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.removeAll() + + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.didEnterBackgroundNotification)) + + // Should have removed all 8 system event observers XCTAssertEqual(fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.count, 8) } + + func testForegroundResubscribesToSystemEvents() { + sut = fixture.getSut(currentDevice: nil) + + // Go to background first + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.didEnterBackgroundNotification)) + fixture.notificationCenterWrapper.addObserverWithObjectInvocations.removeAll() + + // Then come back to foreground + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.willEnterForegroundNotification)) + + // Should have re-registered system event observers (8 on iOS: battery x2, orientation, keyboard x2, screenshot, timezone, significant time) + XCTAssertEqual(fixture.notificationCenterWrapper.addObserverWithObjectInvocations.count, 8) + } + + func testRepeatedBackgroundDoesNotDoubleUnsubscribe() { + sut = fixture.getSut(currentDevice: nil) + fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.removeAll() + + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.didEnterBackgroundNotification)) + let firstCount = fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.count + + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.didEnterBackgroundNotification)) + let secondCount = fixture.notificationCenterWrapper.removeObserverWithNameAndObjectInvocations.count + + XCTAssertEqual(firstCount, secondCount, "Second background notification should be a no-op") + } + + func testRepeatedForegroundDoesNotDoubleSubscribe() { + sut = fixture.getSut(currentDevice: nil) + + // Go to background and back + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.didEnterBackgroundNotification)) + fixture.notificationCenterWrapper.addObserverWithObjectInvocations.removeAll() + + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.willEnterForegroundNotification)) + let firstCount = fixture.notificationCenterWrapper.addObserverWithObjectInvocations.count + + fixture.notificationCenterWrapper.post(Notification(name: UIApplication.willEnterForegroundNotification)) + let secondCount = fixture.notificationCenterWrapper.addObserverWithObjectInvocations.count + + XCTAssertEqual(firstCount, secondCount, "Second foreground notification should be a no-op") + } private func postBatteryLevelNotification(uiDevice: UIDevice?) { Dynamic(sut).batteryStateChanged(Notification(name: UIDevice.batteryLevelDidChangeNotification, object: uiDevice)) From 0287ee3fc5b3586ccf6c874609ce85e5b795f917 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 14 Apr 2026 17:50:40 +0200 Subject: [PATCH 2/2] add cl entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c101da5d4..4decc60ee32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Detect development builds via provisioning profile and debugger attachment (#7702) +- Unsubscribe system event breadcrumbs during background (#7702) ## 9.10.0