From be663782991e6a926e35d50082da3cb1cf4f8506 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 25 Nov 2025 15:35:40 +0100 Subject: [PATCH 01/25] Flush logs when app terminates or resigns active --- Sentry.xcodeproj/project.pbxproj | 44 +++++- SentryTestUtils/Sources/TestClient.swift | 5 + Sources/Sentry/SentryBaseIntegration.m | 5 + Sources/Sentry/SentryClient.m | 5 + Sources/Sentry/SentryLogFlushIntegration.m | 55 +++++++ Sources/Sentry/SentrySDKInternal.m | 7 +- .../HybridPublic/SentryBaseIntegration.h | 1 + Sources/Sentry/include/SentryClient+Private.h | 2 + .../include/SentryLogFlushIntegration.h | 10 ++ .../Swift/{ => AppState}/SentryAppState.swift | 0 .../AppState/SentryAppStateListener.swift | 7 + .../SentryAppStateManager.swift | 43 +++++- .../Helper/SentryAppStateManagerTests.swift | 51 ++++++- .../Logs/SentryLogFlushIntegrationTests.swift | 138 ++++++++++++++++++ Tests/SentryTests/SentryClientTests.swift | 22 ++- .../SentryTests/SentryTests-Bridging-Header.h | 1 + 16 files changed, 379 insertions(+), 17 deletions(-) create mode 100644 Sources/Sentry/SentryLogFlushIntegration.m create mode 100644 Sources/Sentry/include/SentryLogFlushIntegration.h rename Sources/Swift/{ => AppState}/SentryAppState.swift (100%) create mode 100644 Sources/Swift/AppState/SentryAppStateListener.swift rename Sources/Swift/{ => AppState}/SentryAppStateManager.swift (72%) create mode 100644 Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 78a4e2aa171..a8fbb5da53e 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -711,11 +711,15 @@ 92235CAC2E15369900865983 /* SentryLogBatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAB2E15369900865983 /* SentryLogBatcher.swift */; }; 92235CAE2E15549C00865983 /* SentryLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAD2E15549C00865983 /* SentryLogger.swift */; }; 92235CB02E155B2600865983 /* SentryLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */; }; + 9246A2322ED5CDA7002FA318 /* SentryLogFlushIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9246A2342ED5CDB5002FA318 /* SentryLogFlushIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */; }; + 9246A2372ED5D008002FA318 /* SentryAppStateListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */; }; 925824C22CB5897700C9B20B /* SentrySessionReplayIntegration-Hybrid.h in Headers */ = {isa = PBXBuildFile; fileRef = D80382BE2C09C6FD0090E048 /* SentrySessionReplayIntegration-Hybrid.h */; settings = {ATTRIBUTES = (Private, ); }; }; 9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */; }; 9264E1ED2E2E397C00B077CF /* SentryLogMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */; }; 92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 927A5CC42DD7626B00B82404 /* SentryEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */; }; + 927D21FB2ED5DE8A00916D31 /* SentryLogFlushIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */; }; 928207C42E251B8F009285A4 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */; }; 9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9286059729A5098900F96038 /* SentryGeo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9286059629A5098900F96038 /* SentryGeo.m */; }; @@ -2074,10 +2078,14 @@ 92235CAB2E15369900865983 /* SentryLogBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcher.swift; sourceTree = ""; }; 92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = ""; }; 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = ""; }; + 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogFlushIntegration.h; path = include/SentryLogFlushIntegration.h; sourceTree = ""; }; + 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLogFlushIntegration.m; sourceTree = ""; }; + 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAppStateListener.swift; sourceTree = ""; }; 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = ""; }; 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = ""; }; 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = ""; }; 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = ""; }; + 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogFlushIntegrationTests.swift; sourceTree = ""; }; 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = ""; }; 9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = ""; }; 9286059629A5098900F96038 /* SentryGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryGeo.m; sourceTree = ""; }; @@ -2939,6 +2947,7 @@ D85596EF280580BE0041FF8B /* Screenshot */, 0A9BF4E028A114690068D266 /* ViewHierarchy */, D80CD8D52B752FD9002F710B /* SessionReplay */, + 9246A22E2ED5CD59002FA318 /* Log */, FA034AC72DD3DB4900FE3107 /* SentryIntegrationProtocol.h */, 7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */, 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */, @@ -3516,6 +3525,7 @@ 7B944FA924697E9700A10721 /* Integrations */ = { isa = PBXGroup; children = ( + 927D21F42ED5DE7800916D31 /* Logs */, 843FB3422D156B9900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, D808FB85281AB2EF009A2A33 /* UIEvents */, @@ -4196,6 +4206,33 @@ name = Transaction; sourceTree = ""; }; + 9246A22E2ED5CD59002FA318 /* Log */ = { + isa = PBXGroup; + children = ( + 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */, + 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */, + ); + name = Log; + sourceTree = ""; + }; + 9246A2352ED5CFDC002FA318 /* AppState */ = { + isa = PBXGroup; + children = ( + FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, + 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */, + FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, + ); + path = AppState; + sourceTree = ""; + }; + 927D21F42ED5DE7800916D31 /* Logs */ = { + isa = PBXGroup; + children = ( + 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */, + ); + path = Logs; + sourceTree = ""; + }; D4009EA02D77196F0007AF30 /* ViewCapture */ = { isa = PBXGroup; children = ( @@ -4464,9 +4501,9 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + 9246A2352ED5CFDC002FA318 /* AppState */, FAAB95CC2EA18B260030A2DB /* SentryDependencyContainer.swift */, FAAB95B92EA1633E0030A2DB /* State */, - FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, F429D37E2E8532A300DBF387 /* Networking */, F4FE9E062E6248BB0014FED5 /* SentryCrash */, FABB48B22E59310D0071397E /* Transaction */, @@ -4479,7 +4516,6 @@ D856272A2A374A6800FB8062 /* Tools */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, FA4C32962DF7513F001D7B00 /* SentryExperimentalOptions.swift */, - FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, FA6251FE2EB52DD700BFC967 /* SentryHub.swift */, FA6252052EB5489B00BFC967 /* SentryClient.swift */, FA27EC152EB9236000F2ECF7 /* Options.swift */, @@ -5299,6 +5335,7 @@ 7BD86EC5264A63F6005439DB /* SentrySysctlObjC.h in Headers */, 63BE85701ECEC6DE00DC44F5 /* SentryDateUtils.h in Headers */, 63FE709520DA4C1000CDBAE8 /* SentryCrashReportFilterBasic.h in Headers */, + 9246A2322ED5CDA7002FA318 /* SentryLogFlushIntegration.h in Headers */, D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */, 7B63459D280EBA6300CFA05A /* SentryUIEventTracker.h in Headers */, 7B7D873424864C6600D2ECFF /* SentryCrashDefaultMachineContextWrapper.h in Headers */, @@ -5959,6 +5996,7 @@ FA67DD042DDBD4EA00896B02 /* SwiftDescriptor.swift in Sources */, FA67DD052DDBD4EA00896B02 /* SentrySDKLog.swift in Sources */, FA67DD062DDBD4EA00896B02 /* SentryRedactOptions.swift in Sources */, + 9246A2372ED5D008002FA318 /* SentryAppStateListener.swift in Sources */, FA67DD072DDBD4EA00896B02 /* SentryLevel.swift in Sources */, FA67DD082DDBD4EA00896B02 /* SentryDefaultViewRenderer.swift in Sources */, FA67DD092DDBD4EA00896B02 /* URLSessionTaskHelper.swift in Sources */, @@ -6109,6 +6147,7 @@ 621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */, FAB007362E9EF8D3001C806A /* SentryUIViewControllerPerformanceTracker.swift in Sources */, D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */, + 9246A2342ED5CDB5002FA318 /* SentryLogFlushIntegration.m in Sources */, D8F67B1B2BE9728600C9197B /* SentrySRDefaultBreadcrumbConverter.swift in Sources */, 8EBF870926140D37001A6853 /* SentryPerformanceTracker.m in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, @@ -6324,6 +6363,7 @@ 7B26BBFB24C0A66D00A79CCC /* SentrySdkInfoNilTests.m in Sources */, D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */, 7B984A9F28E572AF001F4BEE /* CrashReport.swift in Sources */, + 927D21FB2ED5DE8A00916D31 /* SentryLogFlushIntegrationTests.swift in Sources */, D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */, D46712622DCD059900D4074A /* SentryRedactDefaultOptionsTests.swift in Sources */, D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */, diff --git a/SentryTestUtils/Sources/TestClient.swift b/SentryTestUtils/Sources/TestClient.swift index 57edc820ab7..8416c39d795 100644 --- a/SentryTestUtils/Sources/TestClient.swift +++ b/SentryTestUtils/Sources/TestClient.swift @@ -167,4 +167,9 @@ public class TestClient: SentryClientInternal { captureLogInvocations.record((castLog, scope)) } } + + public var flushLogsInvocations = Invocations() + public override func flushLogs() { + flushLogsInvocations.record(()) + } } diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 1f79d446aa5..0df7aab75e4 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -203,6 +203,11 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options #endif // SENTRY_HAS_UIKIT } + if ((integrationOptions & kIntegrationOptionEnableLogs) && !options.enableLogs) { + [self logWithOptionName:@"enableLogs"]; + return NO; + } + return YES; } diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8054766d511..be2be72f879 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -1111,6 +1111,11 @@ - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope } } +- (void)flushLogs +{ + [self.logBatcher captureLogs]; +} + - (void)captureLogsData:(NSData *)data with:(NSNumber *)itemCount { SentryEnvelopeItem *envelopeItem = diff --git a/Sources/Sentry/SentryLogFlushIntegration.m b/Sources/Sentry/SentryLogFlushIntegration.m new file mode 100644 index 00000000000..466363f4665 --- /dev/null +++ b/Sources/Sentry/SentryLogFlushIntegration.m @@ -0,0 +1,55 @@ +#import "SentryLogFlushIntegration.h" +#import "SentryClient+Private.h" +#import "SentryHub.h" +#import "SentryLogC.h" +#import "SentrySDK+Private.h" +#import "SentrySwift.h" + +#if SENTRY_HAS_UIKIT + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryLogFlushIntegration () + +@end + +@implementation SentryLogFlushIntegration + +- (BOOL)installWithOptions:(SentryOptions *)options +{ + if (![super installWithOptions:options]) { + return NO; + } + + [[[SentryDependencyContainer sharedInstance] appStateManager] addListener:self]; + + return YES; +} + +- (SentryIntegrationOption)integrationOptions +{ + return kIntegrationOptionEnableLogs; +} + +- (void)uninstall +{ + [[[SentryDependencyContainer sharedInstance] appStateManager] removeListener:self]; +} + +# pragma mark - SentryAppStateListener + +- (void)appStateManagerWillResignActive +{ + [[SentrySDKInternal.currentHub getClient] flushLogs]; +} + +- (void)appStateManagerWillTerminate +{ + [[SentrySDKInternal.currentHub getClient] flushLogs]; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index b80c6172a92..0b162b4e9a6 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -31,6 +31,7 @@ #if SENTRY_HAS_UIKIT # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" +# import "SentryLogFlushIntegration.h" # import "SentryPerformanceTrackingIntegration.h" # import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" @@ -530,7 +531,11 @@ + (void)endSession #if SENTRY_TARGET_REPLAY_SUPPORTED [SentryScreenshotIntegration class], #endif // SENTRY_TARGET_REPLAY_SUPPORTED - [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], + [SentryANRTrackingIntegration class], +#if SENTRY_HAS_UIKIT + [SentryLogFlushIntegration class], +#endif // SENTRY_HAS_UIKIT + [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], [SentrySwiftAsyncIntegration class], nil]; diff --git a/Sources/Sentry/include/HybridPublic/SentryBaseIntegration.h b/Sources/Sentry/include/HybridPublic/SentryBaseIntegration.h index 87edbbf5771..59ea9eee534 100644 --- a/Sources/Sentry/include/HybridPublic/SentryBaseIntegration.h +++ b/Sources/Sentry/include/HybridPublic/SentryBaseIntegration.h @@ -30,6 +30,7 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) { kIntegrationOptionEnableMetricKit = 1 << 17, kIntegrationOptionEnableReplay = 1 << 18, kIntegrationOptionStartFramesTracker = 1 << 19, + kIntegrationOptionEnableLogs = 1 << 20, }; @class SentryOptions; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index a2676f57b47..5aad9cdaf4e 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -82,6 +82,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope; +- (void)flushLogs; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryLogFlushIntegration.h b/Sources/Sentry/include/SentryLogFlushIntegration.h new file mode 100644 index 00000000000..c1faa71b936 --- /dev/null +++ b/Sources/Sentry/include/SentryLogFlushIntegration.h @@ -0,0 +1,10 @@ +#import "SentryBaseIntegration.h" +#import "SentryDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryLogFlushIntegration : SentryBaseIntegration + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/SentryAppState.swift b/Sources/Swift/AppState/SentryAppState.swift similarity index 100% rename from Sources/Swift/SentryAppState.swift rename to Sources/Swift/AppState/SentryAppState.swift diff --git a/Sources/Swift/AppState/SentryAppStateListener.swift b/Sources/Swift/AppState/SentryAppStateListener.swift new file mode 100644 index 00000000000..3a370b51803 --- /dev/null +++ b/Sources/Swift/AppState/SentryAppStateListener.swift @@ -0,0 +1,7 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +@_spi(Private) @objc public protocol SentryAppStateListener: NSObjectProtocol { + @objc optional func appStateManagerWillResignActive() + @objc optional func appStateManagerWillTerminate() +} diff --git a/Sources/Swift/SentryAppStateManager.swift b/Sources/Swift/AppState/SentryAppStateManager.swift similarity index 72% rename from Sources/Swift/SentryAppStateManager.swift rename to Sources/Swift/AppState/SentryAppStateManager.swift index 6d2d0acb84c..4e06f4300cb 100644 --- a/Sources/Swift/SentryAppStateManager.swift +++ b/Sources/Swift/AppState/SentryAppStateManager.swift @@ -12,7 +12,9 @@ import UIKit #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT private let _updateAppState: (@escaping (SentryAppState) -> Void) -> Void private let _buildCurrentAppState: () -> SentryAppState - private let helper: SentryDefaultAppStateManager + private var helper: SentryDefaultAppStateManager? + private let listeners = NSHashTable.weakObjects() + private let listenersLock = NSRecursiveLock() #endif init(releaseName: String?, crashWrapper: SentryCrashWrapper, fileManager: SentryFileManager?, sysctlWrapper: SentrySysctl) { @@ -41,31 +43,38 @@ import UIKit } } _updateAppState = updateAppState - helper = SentryDefaultAppStateManager(storeCurrent: { + super.init() + + let helper = SentryDefaultAppStateManager(storeCurrent: { fileManager?.store(buildCurrentAppState()) - }, updateTerminated: { + }, updateTerminated: { [weak self] in updateAppState { $0.wasTerminated = true } + self?.notifyListeners { $0.appStateManagerWillTerminate?() } }, updateSDKNotRunning: { updateAppState { $0.isSDKRunning = false } - }, updateActive: { active in + }, updateActive: { [weak self] active in updateAppState { $0.isActive = active } + if !active { + self?.notifyListeners { $0.appStateManagerWillResignActive?() } + } }) + self.helper = helper #endif } #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT var startCount: Int { - helper.startCount + helper?.startCount ?? 0 } @objc public func start() { - helper.start() + helper?.start() } @objc public func stop() { - helper.stop() + helper?.stop() } @objc public func stop(withForce force: Bool) { - helper.stop(withForce: force) + helper?.stop(withForce: force) } /** @@ -90,5 +99,23 @@ import UIKit @objc public func updateAppState(_ block: @escaping (SentryAppState) -> Void) { _updateAppState(block) } + + @objc public func addListener(_ listener: SentryAppStateListener) { + listenersLock.synchronized { + listeners.add(listener) + } + } + + @objc public func removeListener(_ listener: SentryAppStateListener) { + listenersLock.synchronized { + listeners.remove(listener) + } + } + + @objc private func notifyListeners(_ block: @escaping (SentryAppStateListener) -> Void) { + listenersLock.synchronized { + listeners.allObjects.forEach(block) + } + } #endif } diff --git a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift index 8cecc76b4a9..bb419191c37 100644 --- a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift @@ -3,8 +3,8 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -class SentryAppStateManagerTests: XCTestCase { - private static let dsnAsString = TestConstants.dsnAsString(username: "SentryOutOfMemoryTrackerTests") +final class SentryAppStateManagerTests: XCTestCase { + private static let dsnAsString = TestConstants.dsnAsString(username: "SentryAppStateManagerTests") private class Fixture { @@ -51,6 +51,7 @@ class SentryAppStateManagerTests: XCTestCase { override func tearDown() { super.tearDown() + sut.stop(withForce: true) fixture.fileManager.deleteAppState() clearTestState() } @@ -123,5 +124,51 @@ class SentryAppStateManagerTests: XCTestCase { XCTAssertEqual(fixture.fileManager.readAppState()!.wasTerminated, true) } + + func testListnerCalledForWillResignActive() { + let listener = TestAppStateListener() + sut.addListener(listener) + sut.start() + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(listener.appStateManagerWillResignActiveInvocations, 1) + } + + func testListnerCalledForWillTerminate() { + let listener = TestAppStateListener() + sut.addListener(listener) + sut.start() + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + XCTAssertEqual(listener.appStateManagerWillTerminateInvocations, 1) + } + + func testListenerNotCalledAfterRemoval() { + let listener = TestAppStateListener() + sut.addListener(listener) + sut.start() + sut.removeListener(listener) + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + XCTAssertEqual(listener.appStateManagerWillResignActiveInvocations, 0) + XCTAssertEqual(listener.appStateManagerWillTerminateInvocations, 0) + } +} + +class TestAppStateListener: NSObject, SentryAppStateListener { + var appStateManagerWillResignActiveInvocations = 0 + var appStateManagerWillTerminateInvocations = 0 + + func appStateManagerWillResignActive() { + appStateManagerWillResignActiveInvocations += 1 + } + + func appStateManagerWillTerminate() { + appStateManagerWillTerminateInvocations += 1 + } } #endif diff --git a/Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift new file mode 100644 index 00000000000..995c0add6fd --- /dev/null +++ b/Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift @@ -0,0 +1,138 @@ +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +class SentryLogFlushIntegrationTests: XCTestCase { + + private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") + + private class Fixture { + let dateProvider = TestCurrentDateProvider() + let dispatchQueueWrapper = TestSentryDispatchQueueWrapper() + let notificationCenterWrapper = TestNSNotificationCenterWrapper() + let options: Options + let client: TestClient + let hub: SentryHubInternal + let fileManager: TestFileManager + let appStateManager: SentryAppStateManager + + init() throws { + options = Options() + options.dsn = SentryLogFlushIntegrationTests.dsnAsString + options.releaseName = TestData.appState.releaseName + options.enableLogs = true + + fileManager = try TestFileManager( + options: options, + dateProvider: dateProvider, + dispatchQueueWrapper: dispatchQueueWrapper + ) + + client = TestClient(options: options, fileManager: fileManager) + hub = TestHub(client: client, andScope: nil) + + SentryDependencyContainer.sharedInstance().sysctlWrapper = TestSysctl() + SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueueWrapper + SentryDependencyContainer.sharedInstance().notificationCenterWrapper = notificationCenterWrapper + + appStateManager = SentryAppStateManager( + releaseName: options.releaseName, + crashWrapper: TestSentryCrashWrapper(processInfoWrapper: ProcessInfo.processInfo), + fileManager: fileManager, + sysctlWrapper: SentryDependencyContainer.sharedInstance().sysctlWrapper + ) + + SentryDependencyContainer.sharedInstance().appStateManager = appStateManager + } + + func getSut() -> SentryLogFlushIntegration { + return SentryLogFlushIntegration() + } + } + + private var fixture: Fixture! + + override func setUpWithError() throws { + try super.setUpWithError() + fixture = try Fixture() + SentrySDKInternal.setCurrentHub(fixture.hub) + } + + override func tearDown() { + super.tearDown() + fixture.appStateManager.stop(withForce: true) + fixture.fileManager.deleteAppState() + clearTestState() + } + + func testInstall_Success() { + let sut = fixture.getSut() + let result = sut.install(with: fixture.options) + + XCTAssertTrue(result) + } + + func testInstall_FailsWhenLogsDisabled() { + fixture.options.enableLogs = false + + let sut = fixture.getSut() + let result = sut.install(with: fixture.options) + + XCTAssertFalse(result) + } + + func testIntegrationOptions_ReturnsEnableLogs() { + let sut = fixture.getSut() + let options = sut.integrationOptions() + + XCTAssertEqual(options, .integrationOptionEnableLogs) + } + + func testAppStateManagerWillResignActive_FlushesLogs() { + let sut = fixture.getSut() + sut.install(with: fixture.options) + + fixture.appStateManager.start() + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testAppStateManagerWillTerminate_FlushesLogs() { + let sut = fixture.getSut() + sut.install(with: fixture.options) + + fixture.appStateManager.start() + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testUninstall_RemovesListener() { + let sut = fixture.getSut() + sut.install(with: fixture.options) + + fixture.appStateManager.start() + sut.uninstall() + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + // Should not flush logs after uninstall + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) + } + + func testMultipleAppStateChanges_FlushesLogsMultipleTimes() { + let sut = fixture.getSut() + sut.install(with: fixture.options) + + fixture.appStateManager.start() + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) + } +} +#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 0ca8fb6c30c..99ed600697d 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -2243,7 +2243,6 @@ class SentryClientTests: XCTestCase { func testFlushCallsLogBatcherCaptureLogs() { let sut = fixture.getSut() - // Create a test batcher to verify captureLogs is called let testDelegate = TestLogBatcherDelegateForClient() let testBatcher = TestLogBatcherForClient( options: sut.options, @@ -2252,13 +2251,28 @@ class SentryClientTests: XCTestCase { ) Dynamic(sut).logBatcher = testBatcher - // Verify initial state XCTAssertEqual(testBatcher.captureLogsInvocations.count, 0) - // Call flush - this should trigger the log batcher to capture logs sut.flush(timeout: 1.0) - // Verify that captureLogs was called on the log batcher + XCTAssertEqual(testBatcher.captureLogsInvocations.count, 1) + } + + func testFlushLogsCallsLogBatcherCaptureLogs() { + let sut = fixture.getSut() + + let testDelegate = TestLogBatcherDelegateForClient() + let testBatcher = TestLogBatcherForClient( + options: sut.options, + dispatchQueue: TestSentryDispatchQueueWrapper(), + delegate: testDelegate + ) + Dynamic(sut).logBatcher = testBatcher + + XCTAssertEqual(testBatcher.captureLogsInvocations.count, 0) + + sut.flushLogs() + XCTAssertEqual(testBatcher.captureLogsInvocations.count, 1) } diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 694f10fb78e..57262956a7d 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -123,6 +123,7 @@ #import "SentryInvalidJSONString.h" #import "SentryLevelMapper.h" #import "SentryLogC.h" +#import "SentryLogFlushIntegration.h" #import "SentryLogTestHelper.h" #import "SentryMechanism.h" #import "SentryMechanismContext.h" From 115a625d4fe1675f5ab1c209278103c4aa27c4b9 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 25 Nov 2025 16:30:53 +0100 Subject: [PATCH 02/25] fix deadlock issue --- Sources/Swift/Tools/SentryLogBatcher.swift | 13 ++++++- Tests/SentryTests/SentryLogBatcherTests.swift | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 5151907c0ea..cfe97b4f7c2 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -114,8 +114,17 @@ import Foundation @discardableResult @_spi(Private) @objc public func captureLogs() -> TimeInterval { let startTimeNs = SentryDefaultCurrentDateProvider.getAbsoluteTime() - dispatchQueue.dispatchSync { [weak self] in - self?.performCaptureLogs() + + // Guard against sync call deadlock when we are already on the dispatchQueue queue. + let currentQueueLabel = String(validatingUTF8: __dispatch_queue_get_label(nil)) ?? "" + let targetQueueLabel = dispatchQueue.queue.label + + if currentQueueLabel == targetQueueLabel { + performCaptureLogs() + } else { + dispatchQueue.dispatchSync { [weak self] in + self?.performCaptureLogs() + } } let endTimeNs = SentryDefaultCurrentDateProvider.getAbsoluteTime() return TimeInterval(endTimeNs - startTimeNs) / 1_000_000_000.0 // Convert nanoseconds to seconds diff --git a/Tests/SentryTests/SentryLogBatcherTests.swift b/Tests/SentryTests/SentryLogBatcherTests.swift index 962eba5238d..a46664d3fcf 100644 --- a/Tests/SentryTests/SentryLogBatcherTests.swift +++ b/Tests/SentryTests/SentryLogBatcherTests.swift @@ -203,6 +203,44 @@ final class SentryLogBatcherTests: XCTestCase { XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 0) } + func testCaptureLogs_WhenAlreadyOnQueue_DoesNotDeadlock() { + // Arrange: Create a real dispatch queue wrapper (not test wrapper) to test actual queue behavior + let realDispatchQueue = SentryDispatchQueueWrapper(name: "io.sentry.test.log-batcher", attributes: nil) + let testDelegate = TestLogBatcherDelegate() + + let batcher = SentryLogBatcher( + options: options, + flushTimeout: 0.1, + maxLogCount: 10, + maxBufferSizeBytes: 8_000, + dispatchQueue: realDispatchQueue, + delegate: testDelegate + ) + + let log1 = createTestLog(body: "Log 1") + let log2 = createTestLog(body: "Log 2") + + batcher.addLog(log1, scope: scope) + batcher.addLog(log2, scope: scope) + + // Add logs asynchronously and wait for them to be processed + let addLogsExpectation = expectation(description: "logs added") + realDispatchQueue.dispatchAsync { + batcher.captureLogs() + addLogsExpectation.fulfill() + } + waitForExpectations(timeout: 1.0) { error in + if let error = error { + XCTFail("Test timed out or failed - possible deadlock: \(error)") + } + } + + let capturedLogs = testDelegate.getCapturedLogs() + XCTAssertEqual(capturedLogs.count, 2, "Should have captured both logs without deadlock.") + XCTAssertEqual(capturedLogs[0].body, "Log 1") + XCTAssertEqual(capturedLogs[1].body, "Log 2") + } + // MARK: - Edge Cases Tests func testScheduledFlushAfterBufferAlreadyFlushed_DoesNothing() throws { From 162273e599f7a13187ec3308b2c0bb86064e4d3d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 26 Nov 2025 10:49:57 +0100 Subject: [PATCH 03/25] rename group name --- Sentry.xcodeproj/project.pbxproj | 6 +++--- .../{Logs => Log}/SentryLogFlushIntegrationTests.swift | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename Tests/SentryTests/Integrations/{Logs => Log}/SentryLogFlushIntegrationTests.swift (100%) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index a8fbb5da53e..be97dca8879 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -3525,7 +3525,7 @@ 7B944FA924697E9700A10721 /* Integrations */ = { isa = PBXGroup; children = ( - 927D21F42ED5DE7800916D31 /* Logs */, + 927D21F42ED5DE7800916D31 /* Log */, 843FB3422D156B9900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, D808FB85281AB2EF009A2A33 /* UIEvents */, @@ -4225,12 +4225,12 @@ path = AppState; sourceTree = ""; }; - 927D21F42ED5DE7800916D31 /* Logs */ = { + 927D21F42ED5DE7800916D31 /* Log */ = { isa = PBXGroup; children = ( 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */, ); - path = Logs; + path = Log; sourceTree = ""; }; D4009EA02D77196F0007AF30 /* ViewCapture */ = { diff --git a/Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift similarity index 100% rename from Tests/SentryTests/Integrations/Logs/SentryLogFlushIntegrationTests.swift rename to Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift From 3f4813d1668952e8e600b33ca0a07695191565b1 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 26 Nov 2025 11:06:36 +0100 Subject: [PATCH 04/25] move into existing confitional block --- Sources/Sentry/SentrySDKInternal.m | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index 0b162b4e9a6..369759ac919 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -526,16 +526,12 @@ + (void)endSession [SentryAppStartTrackingIntegration class], [SentryFramesTrackingIntegration class], [SentryPerformanceTrackingIntegration class], [SentryUIEventTrackingIntegration class], [SentryViewHierarchyIntegration class], - [SentryWatchdogTerminationTrackingIntegration class], + [SentryWatchdogTerminationTrackingIntegration class], [SentryLogFlushIntegration class], #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_REPLAY_SUPPORTED [SentryScreenshotIntegration class], #endif // SENTRY_TARGET_REPLAY_SUPPORTED - [SentryANRTrackingIntegration class], -#if SENTRY_HAS_UIKIT - [SentryLogFlushIntegration class], -#endif // SENTRY_HAS_UIKIT - [SentryAutoBreadcrumbTrackingIntegration class], + [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], [SentrySwiftAsyncIntegration class], nil]; From aa1fbc91d1d0ee77845d230774d0763ac367a9b0 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 26 Nov 2025 11:56:50 +0100 Subject: [PATCH 05/25] Use dispatch_queue_set_specific/dispatch_get_specific to get corrrect queue --- CHANGELOG.md | 6 ++ .../_SentryDispatchQueueWrapperInternal.m | 13 ++++ .../_SentryDispatchQueueWrapperInternal.h | 2 + .../Helper/SentryDispatchQueueWrapper.swift | 7 ++ Sources/Swift/Tools/SentryLogBatcher.swift | 7 +- .../SentryDispatchQueueWrapperTests.m | 74 +++++++++++++++++++ 6 files changed, 104 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f8f59ae75..b728682bb43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Improvements + +- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) + ## 9.0.0-rc.1 ### Breaking Changes diff --git a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m index 8ca3a9da22c..7fd1f8a2b5e 100644 --- a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m +++ b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m @@ -20,6 +20,8 @@ - (instancetype)initWithName:(const char *)name { if (self = [super init]) { _queue = dispatch_queue_create(name, attributes); + void *key = (__bridge void *)self; + dispatch_queue_set_specific(_queue, key, key, NULL); } return self; } @@ -32,6 +34,8 @@ - (instancetype)initWithName:(const char *)name relativePriority:(int)relativePr dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, relativePriority); _queue = dispatch_queue_create(name, attributes); + void *key = (__bridge void *)self; + dispatch_queue_set_specific(_queue, key, key, NULL); } return self; } @@ -107,6 +111,15 @@ - (void)dispatchOnce:(dispatch_once_t *)predicate block:(void (^)(void))block dispatch_once(predicate, block); } +- (BOOL)isCurrentQueue +{ + // Use dispatch_get_specific with the instance address as a unique context pointer + // instead of comparing queue labels, as labels are not guaranteed to be unique + // and can cause false positives leading to data races. + void *key = (__bridge void *)self; + return dispatch_get_specific(key) == key; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h index 060ac0e4a1c..f7d6e2eed3a 100644 --- a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h +++ b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h @@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)dispatchAsyncOnMainQueueIfNotMainThread:(void (^)(void))block NS_SWIFT_NAME(dispatchAsyncOnMainQueueIfNotMainThread(block:)); +- (BOOL)isCurrentQueue; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift index 1a1e25c5742..7eb1f2068bb 100644 --- a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift +++ b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift @@ -71,4 +71,11 @@ public var shouldCreateDispatchBlock: Bool { return true } + + /// Returns `true` if the current execution context is on this queue. + /// Uses `dispatch_get_specific` with a unique context pointer for reliable detection, + /// as queue labels are not guaranteed to be unique. + @_spi(Private) public func isCurrentQueue() -> Bool { + return internalWrapper.isCurrentQueue() + } } diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index cfe97b4f7c2..498a341e9e1 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -115,11 +115,8 @@ import Foundation @_spi(Private) @objc public func captureLogs() -> TimeInterval { let startTimeNs = SentryDefaultCurrentDateProvider.getAbsoluteTime() - // Guard against sync call deadlock when we are already on the dispatchQueue queue. - let currentQueueLabel = String(validatingUTF8: __dispatch_queue_get_label(nil)) ?? "" - let targetQueueLabel = dispatchQueue.queue.label - - if currentQueueLabel == targetQueueLabel { + // Guard against sync call deadlock when we are already on the dispatchQueue.queue. + if dispatchQueue.isCurrentQueue() { performCaptureLogs() } else { dispatchQueue.dispatchSync { [weak self] in diff --git a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m index 0a5a6e33efb..26cf5d9b88c 100644 --- a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m +++ b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m @@ -77,6 +77,80 @@ - (void)testInitWithNameAndAttributes_customAttributes_shouldCreateQueueWithGive XCTAssertEqual(actualQoSClass, QOS_CLASS_UNSPECIFIED); XCTAssertEqual(actualRelativePriority, 0); } + +- (void)testIsCurrentQueue_whenCalledFromDifferentQueue_shouldReturnFalse +{ + // -- Arrange -- + SentryDispatchQueueWrapper *wrappedQueue = + [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" + attributes:DISPATCH_QUEUE_SERIAL]; + + // -- Act & Assert -- + XCTAssertFalse([wrappedQueue isCurrentQueue]); +} + +- (void)testIsCurrentQueue_whenCalledFromWithinQueue_shouldReturnTrue +{ + // -- Arrange -- + SentryDispatchQueueWrapper *wrappedQueue = + [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" + attributes:DISPATCH_QUEUE_SERIAL]; + XCTestExpectation *expectation = [self expectationWithDescription:@"queue execution"]; + + // -- Act -- + [wrappedQueue dispatchAsyncWithBlock:^{ + // -- Assert -- + XCTAssertTrue([wrappedQueue isCurrentQueue]); + [expectation fulfill]; + }]; + + // -- Wait -- + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testIsCurrentQueue_whenCalledFromSyncDispatch_shouldReturnTrue +{ + // -- Arrange -- + SentryDispatchQueueWrapper *wrappedQueue = + [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" + attributes:DISPATCH_QUEUE_SERIAL]; + + // -- Act & Assert -- + dispatch_sync(wrappedQueue.queue, ^{ XCTAssertTrue([wrappedQueue isCurrentQueue]); }); +} + +- (void)testIsCurrentQueue_differentInstances_shouldHaveUniqueDetection +{ + // -- Arrange -- + SentryDispatchQueueWrapper *queue1 = + [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue1" + attributes:DISPATCH_QUEUE_SERIAL]; + SentryDispatchQueueWrapper *queue2 = + [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue2" + attributes:DISPATCH_QUEUE_SERIAL]; + + // -- Act -- + XCTestExpectation *expectation = [self expectationWithDescription:@"queue execution"]; + [queue1 dispatchAsyncWithBlock:^{ + // -- Assert -- + XCTAssertTrue([queue1 isCurrentQueue], @"queue1 should detect it's on its own queue"); + XCTAssertFalse([queue2 isCurrentQueue], @"queue2 should not detect it's on queue1"); + [expectation fulfill]; + }]; + // -- Wait -- + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // -- Act -- + XCTestExpectation *expectation2 = [self expectationWithDescription:@"queue execution 2"]; + [queue2 dispatchAsyncWithBlock:^{ + // -- Assert -- + XCTAssertFalse([queue1 isCurrentQueue], @"queue1 should not detect it's on queue2"); + XCTAssertTrue([queue2 isCurrentQueue], @"queue2 should detect it's on its own queue"); + [expectation2 fulfill]; + }]; + // -- Wait -- + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} @end NS_ASSUME_NONNULL_END From 6cdd1b0f85dafc4a640ba03610aa844059fe8d0d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 26 Nov 2025 12:02:13 +0100 Subject: [PATCH 06/25] cleanup queu specific key --- Sources/Sentry/_SentryDispatchQueueWrapperInternal.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m index 7fd1f8a2b5e..5f176b0e961 100644 --- a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m +++ b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m @@ -113,13 +113,18 @@ - (void)dispatchOnce:(dispatch_once_t *)predicate block:(void (^)(void))block - (BOOL)isCurrentQueue { - // Use dispatch_get_specific with the instance address as a unique context pointer - // instead of comparing queue labels, as labels are not guaranteed to be unique - // and can cause false positives leading to data races. void *key = (__bridge void *)self; return dispatch_get_specific(key) == key; } +- (void)dealloc +{ + if (_queue != NULL) { + void *key = (__bridge void *)self; + dispatch_queue_set_specific(_queue, key, NULL, NULL); + } +} + @end NS_ASSUME_NONNULL_END From 8e360441f4afd3d66324a5f489a2025cd0858b31 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 26 Nov 2025 13:50:06 +0100 Subject: [PATCH 07/25] check client for nil --- Sources/Sentry/SentryLogFlushIntegration.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentryLogFlushIntegration.m b/Sources/Sentry/SentryLogFlushIntegration.m index 466363f4665..0aa31e8cd4f 100644 --- a/Sources/Sentry/SentryLogFlushIntegration.m +++ b/Sources/Sentry/SentryLogFlushIntegration.m @@ -40,12 +40,18 @@ - (void)uninstall - (void)appStateManagerWillResignActive { - [[SentrySDKInternal.currentHub getClient] flushLogs]; + SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; + if (client != nil) { + [client flushLogs]; + } } - (void)appStateManagerWillTerminate { - [[SentrySDKInternal.currentHub getClient] flushLogs]; + SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; + if (client != nil) { + [client flushLogs]; + } } @end From d22d16e01b07949256605dadb68b3d1e4756aa13 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 11:19:18 +0100 Subject: [PATCH 08/25] Listen to notifications directly in integration --- Sentry.xcodeproj/project.pbxproj | 4 -- Sources/Sentry/SentryLogFlushIntegration.m | 34 +++++++---- .../AppState/SentryAppStateListener.swift | 7 --- .../AppState/SentryAppStateManager.swift | 42 +++----------- .../Helper/SentryAppStateManagerTests.swift | 46 --------------- .../Log/SentryLogFlushIntegrationTests.swift | 58 +++++-------------- 6 files changed, 45 insertions(+), 146 deletions(-) delete mode 100644 Sources/Swift/AppState/SentryAppStateListener.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ea01a48062a..bbcb9a79985 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -707,7 +707,6 @@ 92235CB02E155B2600865983 /* SentryLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */; }; 9246A2322ED5CDA7002FA318 /* SentryLogFlushIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */; settings = {ATTRIBUTES = (Private, ); }; }; 9246A2342ED5CDB5002FA318 /* SentryLogFlushIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */; }; - 9246A2372ED5D008002FA318 /* SentryAppStateListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */; }; 9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */; }; 9264E1ED2E2E397C00B077CF /* SentryLogMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */; }; 92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -2072,7 +2071,6 @@ 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = ""; }; 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogFlushIntegration.h; path = include/SentryLogFlushIntegration.h; sourceTree = ""; }; 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLogFlushIntegration.m; sourceTree = ""; }; - 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAppStateListener.swift; sourceTree = ""; }; 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = ""; }; 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = ""; }; 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = ""; }; @@ -4210,7 +4208,6 @@ isa = PBXGroup; children = ( FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, - 9246A2362ED5D008002FA318 /* SentryAppStateListener.swift */, FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, ); path = AppState; @@ -5989,7 +5986,6 @@ FA67DD042DDBD4EA00896B02 /* SwiftDescriptor.swift in Sources */, FA67DD052DDBD4EA00896B02 /* SentrySDKLog.swift in Sources */, FA67DD062DDBD4EA00896B02 /* SentryRedactOptions.swift in Sources */, - 9246A2372ED5D008002FA318 /* SentryAppStateListener.swift in Sources */, FA67DD072DDBD4EA00896B02 /* SentryLevel.swift in Sources */, FA67DD082DDBD4EA00896B02 /* SentryDefaultViewRenderer.swift in Sources */, FA67DD092DDBD4EA00896B02 /* URLSessionTaskHelper.swift in Sources */, diff --git a/Sources/Sentry/SentryLogFlushIntegration.m b/Sources/Sentry/SentryLogFlushIntegration.m index 0aa31e8cd4f..e15747d8d13 100644 --- a/Sources/Sentry/SentryLogFlushIntegration.m +++ b/Sources/Sentry/SentryLogFlushIntegration.m @@ -2,6 +2,7 @@ #import "SentryClient+Private.h" #import "SentryHub.h" #import "SentryLogC.h" +#import "SentryNotificationNames.h" #import "SentrySDK+Private.h" #import "SentrySwift.h" @@ -9,10 +10,6 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentryLogFlushIntegration () - -@end - @implementation SentryLogFlushIntegration - (BOOL)installWithOptions:(SentryOptions *)options @@ -21,7 +18,15 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } - [[[SentryDependencyContainer sharedInstance] appStateManager] addListener:self]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(willResignActive) + name:SentryWillResignActiveNotification + object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(willTerminate) + name:SentryWillTerminateNotification + object:nil]; return YES; } @@ -33,12 +38,16 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { - [[[SentryDependencyContainer sharedInstance] appStateManager] removeListener:self]; -} + [NSNotificationCenter.defaultCenter removeObserver:self + name:SentryWillResignActiveNotification + object:nil]; -# pragma mark - SentryAppStateListener + [NSNotificationCenter.defaultCenter removeObserver:self + name:SentryWillTerminateNotification + object:nil]; +} -- (void)appStateManagerWillResignActive +- (void)willResignActive { SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; if (client != nil) { @@ -46,7 +55,7 @@ - (void)appStateManagerWillResignActive } } -- (void)appStateManagerWillTerminate +- (void)willTerminate { SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; if (client != nil) { @@ -54,6 +63,11 @@ - (void)appStateManagerWillTerminate } } +- (void)dealloc +{ + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/AppState/SentryAppStateListener.swift b/Sources/Swift/AppState/SentryAppStateListener.swift deleted file mode 100644 index 3a370b51803..00000000000 --- a/Sources/Swift/AppState/SentryAppStateListener.swift +++ /dev/null @@ -1,7 +0,0 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -@_spi(Private) @objc public protocol SentryAppStateListener: NSObjectProtocol { - @objc optional func appStateManagerWillResignActive() - @objc optional func appStateManagerWillTerminate() -} diff --git a/Sources/Swift/AppState/SentryAppStateManager.swift b/Sources/Swift/AppState/SentryAppStateManager.swift index 4e06f4300cb..f0df1c02aab 100644 --- a/Sources/Swift/AppState/SentryAppStateManager.swift +++ b/Sources/Swift/AppState/SentryAppStateManager.swift @@ -12,9 +12,7 @@ import UIKit #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT private let _updateAppState: (@escaping (SentryAppState) -> Void) -> Void private let _buildCurrentAppState: () -> SentryAppState - private var helper: SentryDefaultAppStateManager? - private let listeners = NSHashTable.weakObjects() - private let listenersLock = NSRecursiveLock() + private var helper: SentryDefaultAppStateManager #endif init(releaseName: String?, crashWrapper: SentryCrashWrapper, fileManager: SentryFileManager?, sysctlWrapper: SentrySysctl) { @@ -43,38 +41,32 @@ import UIKit } } _updateAppState = updateAppState - super.init() - let helper = SentryDefaultAppStateManager(storeCurrent: { + helper = SentryDefaultAppStateManager(storeCurrent: { fileManager?.store(buildCurrentAppState()) - }, updateTerminated: { [weak self] in + }, updateTerminated: { updateAppState { $0.wasTerminated = true } - self?.notifyListeners { $0.appStateManagerWillTerminate?() } }, updateSDKNotRunning: { updateAppState { $0.isSDKRunning = false } - }, updateActive: { [weak self] active in + }, updateActive: { active in updateAppState { $0.isActive = active } - if !active { - self?.notifyListeners { $0.appStateManagerWillResignActive?() } - } }) - self.helper = helper #endif } #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT var startCount: Int { - helper?.startCount ?? 0 + helper.startCount } @objc public func start() { - helper?.start() + helper.start() } @objc public func stop() { - helper?.stop() + helper.stop() } @objc public func stop(withForce force: Bool) { - helper?.stop(withForce: force) + helper.stop(withForce: force) } /** @@ -99,23 +91,5 @@ import UIKit @objc public func updateAppState(_ block: @escaping (SentryAppState) -> Void) { _updateAppState(block) } - - @objc public func addListener(_ listener: SentryAppStateListener) { - listenersLock.synchronized { - listeners.add(listener) - } - } - - @objc public func removeListener(_ listener: SentryAppStateListener) { - listenersLock.synchronized { - listeners.remove(listener) - } - } - - @objc private func notifyListeners(_ block: @escaping (SentryAppStateListener) -> Void) { - listenersLock.synchronized { - listeners.allObjects.forEach(block) - } - } #endif } diff --git a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift index bb419191c37..5d95d111a8a 100644 --- a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift @@ -124,51 +124,5 @@ final class SentryAppStateManagerTests: XCTestCase { XCTAssertEqual(fixture.fileManager.readAppState()!.wasTerminated, true) } - - func testListnerCalledForWillResignActive() { - let listener = TestAppStateListener() - sut.addListener(listener) - sut.start() - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - XCTAssertEqual(listener.appStateManagerWillResignActiveInvocations, 1) - } - - func testListnerCalledForWillTerminate() { - let listener = TestAppStateListener() - sut.addListener(listener) - sut.start() - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - XCTAssertEqual(listener.appStateManagerWillTerminateInvocations, 1) - } - - func testListenerNotCalledAfterRemoval() { - let listener = TestAppStateListener() - sut.addListener(listener) - sut.start() - sut.removeListener(listener) - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - XCTAssertEqual(listener.appStateManagerWillResignActiveInvocations, 0) - XCTAssertEqual(listener.appStateManagerWillTerminateInvocations, 0) - } -} - -class TestAppStateListener: NSObject, SentryAppStateListener { - var appStateManagerWillResignActiveInvocations = 0 - var appStateManagerWillTerminateInvocations = 0 - - func appStateManagerWillResignActive() { - appStateManagerWillResignActiveInvocations += 1 - } - - func appStateManagerWillTerminate() { - appStateManagerWillTerminateInvocations += 1 - } } #endif diff --git a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift index 995c0add6fd..95bf9acef77 100644 --- a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift @@ -3,47 +3,22 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -class SentryLogFlushIntegrationTests: XCTestCase { +final class SentryLogFlushIntegrationTests: XCTestCase { private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") private class Fixture { - let dateProvider = TestCurrentDateProvider() - let dispatchQueueWrapper = TestSentryDispatchQueueWrapper() - let notificationCenterWrapper = TestNSNotificationCenterWrapper() let options: Options let client: TestClient let hub: SentryHubInternal - let fileManager: TestFileManager - let appStateManager: SentryAppStateManager init() throws { options = Options() options.dsn = SentryLogFlushIntegrationTests.dsnAsString - options.releaseName = TestData.appState.releaseName options.enableLogs = true - fileManager = try TestFileManager( - options: options, - dateProvider: dateProvider, - dispatchQueueWrapper: dispatchQueueWrapper - ) - - client = TestClient(options: options, fileManager: fileManager) + client = TestClient(options: options)! hub = TestHub(client: client, andScope: nil) - - SentryDependencyContainer.sharedInstance().sysctlWrapper = TestSysctl() - SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueueWrapper - SentryDependencyContainer.sharedInstance().notificationCenterWrapper = notificationCenterWrapper - - appStateManager = SentryAppStateManager( - releaseName: options.releaseName, - crashWrapper: TestSentryCrashWrapper(processInfoWrapper: ProcessInfo.processInfo), - fileManager: fileManager, - sysctlWrapper: SentryDependencyContainer.sharedInstance().sysctlWrapper - ) - - SentryDependencyContainer.sharedInstance().appStateManager = appStateManager } func getSut() -> SentryLogFlushIntegration { @@ -61,8 +36,6 @@ class SentryLogFlushIntegrationTests: XCTestCase { override func tearDown() { super.tearDown() - fixture.appStateManager.stop(withForce: true) - fixture.fileManager.deleteAppState() clearTestState() } @@ -89,48 +62,43 @@ class SentryLogFlushIntegrationTests: XCTestCase { XCTAssertEqual(options, .integrationOptionEnableLogs) } - func testAppStateManagerWillResignActive_FlushesLogs() { + func testWillResignActive_FlushesLogs() { let sut = fixture.getSut() sut.install(with: fixture.options) - fixture.appStateManager.start() - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) } - func testAppStateManagerWillTerminate_FlushesLogs() { + func testWillTerminate_FlushesLogs() { let sut = fixture.getSut() sut.install(with: fixture.options) - fixture.appStateManager.start() - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) } - func testUninstall_RemovesListener() { + func testUninstall_RemovesObservers() { let sut = fixture.getSut() sut.install(with: fixture.options) - - fixture.appStateManager.start() sut.uninstall() - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) // Should not flush logs after uninstall XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) } - func testMultipleAppStateChanges_FlushesLogsMultipleTimes() { + func testMultipleNotifications_FlushesLogsMultipleTimes() { let sut = fixture.getSut() sut.install(with: fixture.options) - fixture.appStateManager.start() - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) } From 539d6523a24a3a940852940a8a6e786be4bcbe36 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 11:52:37 +0100 Subject: [PATCH 09/25] move integration to swift class --- Sentry.xcodeproj/project.pbxproj | 34 +++-- Sources/Sentry/SentryLogFlushIntegration.m | 75 ----------- Sources/Sentry/SentrySDKInternal.m | 3 +- .../include/SentryLogFlushIntegration.h | 10 -- .../Core/Integrations/Integrations.swift | 11 +- .../Log/FlushLogsIntegration.swift | 82 ++++++++++++ Sources/Swift/SentryDependencyContainer.swift | 4 + .../Log/FlushLogsIntegrationTests.swift | 121 ++++++++++++++++++ .../Log/SentryLogFlushIntegrationTests.swift | 106 --------------- .../SentryTests/SentryTests-Bridging-Header.h | 1 - 10 files changed, 231 insertions(+), 216 deletions(-) delete mode 100644 Sources/Sentry/SentryLogFlushIntegration.m delete mode 100644 Sources/Sentry/include/SentryLogFlushIntegration.h create mode 100644 Sources/Swift/Integrations/Log/FlushLogsIntegration.swift create mode 100644 Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift delete mode 100644 Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index bbcb9a79985..6f3b19b05ec 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -705,13 +705,12 @@ 92235CAC2E15369900865983 /* SentryLogBatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAB2E15369900865983 /* SentryLogBatcher.swift */; }; 92235CAE2E15549C00865983 /* SentryLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAD2E15549C00865983 /* SentryLogger.swift */; }; 92235CB02E155B2600865983 /* SentryLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */; }; - 9246A2322ED5CDA7002FA318 /* SentryLogFlushIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 9246A2342ED5CDB5002FA318 /* SentryLogFlushIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */; }; + 925189AC2EDDA6A300557BD1 /* FlushLogsIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */; }; 9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */; }; 9264E1ED2E2E397C00B077CF /* SentryLogMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */; }; 92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 927A5CC42DD7626B00B82404 /* SentryEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */; }; - 927D21FB2ED5DE8A00916D31 /* SentryLogFlushIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */; }; + 927D21FB2ED5DE8A00916D31 /* FlushLogsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */; }; 928207C42E251B8F009285A4 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */; }; 9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9286059729A5098900F96038 /* SentryGeo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9286059629A5098900F96038 /* SentryGeo.m */; }; @@ -2069,13 +2068,12 @@ 92235CAB2E15369900865983 /* SentryLogBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcher.swift; sourceTree = ""; }; 92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = ""; }; 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = ""; }; - 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogFlushIntegration.h; path = include/SentryLogFlushIntegration.h; sourceTree = ""; }; - 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLogFlushIntegration.m; sourceTree = ""; }; + 925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlushLogsIntegration.swift; sourceTree = ""; }; 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = ""; }; 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = ""; }; 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = ""; }; 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = ""; }; - 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogFlushIntegrationTests.swift; sourceTree = ""; }; + 927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlushLogsIntegrationTests.swift; sourceTree = ""; }; 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = ""; }; 9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = ""; }; 9286059629A5098900F96038 /* SentryGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryGeo.m; sourceTree = ""; }; @@ -2941,7 +2939,6 @@ D85596EF280580BE0041FF8B /* Screenshot */, 0A9BF4E028A114690068D266 /* ViewHierarchy */, D80CD8D52B752FD9002F710B /* SessionReplay */, - 9246A22E2ED5CD59002FA318 /* Log */, FA034AC72DD3DB4900FE3107 /* SentryIntegrationProtocol.h */, 7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */, 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */, @@ -4195,28 +4192,27 @@ name = Transaction; sourceTree = ""; }; - 9246A22E2ED5CD59002FA318 /* Log */ = { + 9246A2352ED5CFDC002FA318 /* AppState */ = { isa = PBXGroup; children = ( - 9246A2312ED5CDA7002FA318 /* SentryLogFlushIntegration.h */, - 9246A2332ED5CDB5002FA318 /* SentryLogFlushIntegration.m */, + FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, + FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, ); - name = Log; + path = AppState; sourceTree = ""; }; - 9246A2352ED5CFDC002FA318 /* AppState */ = { + 925189AB2EDDA6A300557BD1 /* Log */ = { isa = PBXGroup; children = ( - FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, - FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, + 925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */, ); - path = AppState; + path = Log; sourceTree = ""; }; 927D21F42ED5DE7800916D31 /* Log */ = { isa = PBXGroup; children = ( - 927D21FA2ED5DE7F00916D31 /* SentryLogFlushIntegrationTests.swift */, + 927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */, ); path = Log; sourceTree = ""; @@ -4864,6 +4860,7 @@ D8CAC02D2BA0663E00E38F34 /* Integrations */ = { isa = PBXGroup; children = ( + 925189AB2EDDA6A300557BD1 /* Log */, FAB0073C2E9F47DE001C806A /* Session */, FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */, D49064862DFAE1B700555785 /* Screenshot */, @@ -5323,7 +5320,6 @@ 7BD86EC5264A63F6005439DB /* SentrySysctlObjC.h in Headers */, 63BE85701ECEC6DE00DC44F5 /* SentryDateUtils.h in Headers */, 63FE709520DA4C1000CDBAE8 /* SentryCrashReportFilterBasic.h in Headers */, - 9246A2322ED5CDA7002FA318 /* SentryLogFlushIntegration.h in Headers */, D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */, 7B63459D280EBA6300CFA05A /* SentryUIEventTracker.h in Headers */, 7B7D873424864C6600D2ECFF /* SentryCrashDefaultMachineContextWrapper.h in Headers */, @@ -5806,6 +5802,7 @@ 63AA75EF1EB8B3C400D153DE /* SentryClient.m in Sources */, D4B0DC7F2DA9257A00DE61B6 /* SentryRenderVideoResult.swift in Sources */, FAAB964E2EA698730030A2DB /* SentryDebugImageProvider.swift in Sources */, + 925189AC2EDDA6A300557BD1 /* FlushLogsIntegration.swift in Sources */, 7B7D873624864C9D00D2ECFF /* SentryCrashDefaultMachineContextWrapper.m in Sources */, 63FE712F20DA4C1100CDBAE8 /* SentryCrashSysCtl.c in Sources */, 62212B872D520CB00062C2FA /* SentryEventCodable.swift in Sources */, @@ -6136,7 +6133,6 @@ 621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */, FAB007362E9EF8D3001C806A /* SentryUIViewControllerPerformanceTracker.swift in Sources */, D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */, - 9246A2342ED5CDB5002FA318 /* SentryLogFlushIntegration.m in Sources */, D8F67B1B2BE9728600C9197B /* SentrySRDefaultBreadcrumbConverter.swift in Sources */, 8EBF870926140D37001A6853 /* SentryPerformanceTracker.m in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, @@ -6350,7 +6346,7 @@ D88817DD26D72BA500BF2251 /* SentryTraceContextTests.swift in Sources */, D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */, 7B984A9F28E572AF001F4BEE /* CrashReport.swift in Sources */, - 927D21FB2ED5DE8A00916D31 /* SentryLogFlushIntegrationTests.swift in Sources */, + 927D21FB2ED5DE8A00916D31 /* FlushLogsIntegrationTests.swift in Sources */, D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */, D46712622DCD059900D4074A /* SentryRedactDefaultOptionsTests.swift in Sources */, D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */, diff --git a/Sources/Sentry/SentryLogFlushIntegration.m b/Sources/Sentry/SentryLogFlushIntegration.m deleted file mode 100644 index e15747d8d13..00000000000 --- a/Sources/Sentry/SentryLogFlushIntegration.m +++ /dev/null @@ -1,75 +0,0 @@ -#import "SentryLogFlushIntegration.h" -#import "SentryClient+Private.h" -#import "SentryHub.h" -#import "SentryLogC.h" -#import "SentryNotificationNames.h" -#import "SentrySDK+Private.h" -#import "SentrySwift.h" - -#if SENTRY_HAS_UIKIT - -NS_ASSUME_NONNULL_BEGIN - -@implementation SentryLogFlushIntegration - -- (BOOL)installWithOptions:(SentryOptions *)options -{ - if (![super installWithOptions:options]) { - return NO; - } - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(willResignActive) - name:SentryWillResignActiveNotification - object:nil]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(willTerminate) - name:SentryWillTerminateNotification - object:nil]; - - return YES; -} - -- (SentryIntegrationOption)integrationOptions -{ - return kIntegrationOptionEnableLogs; -} - -- (void)uninstall -{ - [NSNotificationCenter.defaultCenter removeObserver:self - name:SentryWillResignActiveNotification - object:nil]; - - [NSNotificationCenter.defaultCenter removeObserver:self - name:SentryWillTerminateNotification - object:nil]; -} - -- (void)willResignActive -{ - SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; - if (client != nil) { - [client flushLogs]; - } -} - -- (void)willTerminate -{ - SentryClientInternal *client = [SentrySDKInternal.currentHub getClient]; - if (client != nil) { - [client flushLogs]; - } -} - -- (void)dealloc -{ - [NSNotificationCenter.defaultCenter removeObserver:self]; -} - -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index 6feeeb2aec5..312cdb379d8 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -30,7 +30,6 @@ #if SENTRY_HAS_UIKIT # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" -# import "SentryLogFlushIntegration.h" # import "SentryPerformanceTrackingIntegration.h" # import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" @@ -524,7 +523,7 @@ + (void)endSession [SentryAppStartTrackingIntegration class], [SentryFramesTrackingIntegration class], [SentryPerformanceTrackingIntegration class], [SentryUIEventTrackingIntegration class], [SentryViewHierarchyIntegration class], - [SentryWatchdogTerminationTrackingIntegration class], [SentryLogFlushIntegration class], + [SentryWatchdogTerminationTrackingIntegration class], #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_REPLAY_SUPPORTED [SentryScreenshotIntegration class], diff --git a/Sources/Sentry/include/SentryLogFlushIntegration.h b/Sources/Sentry/include/SentryLogFlushIntegration.h deleted file mode 100644 index c1faa71b936..00000000000 --- a/Sources/Sentry/include/SentryLogFlushIntegration.h +++ /dev/null @@ -1,10 +0,0 @@ -#import "SentryBaseIntegration.h" -#import "SentryDefines.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryLogFlushIntegration : SentryBaseIntegration - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index b265df7ce63..c7ae4829048 100644 --- a/Sources/Swift/Core/Integrations/Integrations.swift +++ b/Sources/Swift/Core/Integrations/Integrations.swift @@ -34,11 +34,16 @@ private struct AnyIntegration { @_spi(Private) @objc public final class SentrySwiftIntegrationInstaller: NSObject { @objc public class func install(with options: Options) { let dependencies = SentryDependencyContainer.sharedInstance() + var integrations: [AnyIntegration] = [] + #if os(iOS) && !SENTRY_NO_UIKIT - let integrations: [AnyIntegration] = [.init(UserFeedbackIntegration.self)] - #else - let integrations: [AnyIntegration] = [] + integrations.append(.init(UserFeedbackIntegration.self)) #endif + + #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) + integrations.append(.init(FlushLogsIntegration.self)) + #endif + integrations.forEach { anyIntegration in guard let integration = anyIntegration.install(options, dependencies) else { return } diff --git a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift new file mode 100644 index 00000000000..ce2a1685b87 --- /dev/null +++ b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift @@ -0,0 +1,82 @@ +@_implementationOnly import _SentryPrivate + +#if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +import UIKit +private typealias CrossPlatformApplication = UIApplication +#elseif (os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT +import AppKit +private typealias CrossPlatformApplication = NSApplication +#endif + +#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) + +protocol NotificationCenterProvider { + var notificationCenterWrapper: SentryNSNotificationCenterWrapper { get } +} + +final class FlushLogsIntegration: NSObject, SwiftIntegration { + + private let notificationCenter: SentryNSNotificationCenterWrapper + + init?(with options: Options, dependencies: Dependencies) { + guard options.enableLogs else { + return nil + } + + self.notificationCenter = dependencies.notificationCenterWrapper + + super.init() + + notificationCenter.addObserver( + self, + selector: #selector(willResignActive), + name: CrossPlatformApplication.willResignActiveNotification, + object: nil + ) + + notificationCenter.addObserver( + self, + selector: #selector(willTerminate), + name: CrossPlatformApplication.willTerminateNotification, + object: nil + ) + } + + func uninstall() { + notificationCenter.removeObserver( + self, + name: CrossPlatformApplication.willResignActiveNotification, + object: nil + ) + + notificationCenter.removeObserver( + self, + name: CrossPlatformApplication.willTerminateNotification, + object: nil + ) + } + + deinit { + uninstall() + } + + @objc private func willResignActive() { + guard let client = SentrySDKInternal.currentHub().getClient() else { + return + } + client.flushLogs() + } + + @objc private func willTerminate() { + guard let client = SentrySDKInternal.currentHub().getClient() else { + return + } + client.flushLogs() + } + + static var name: String { + "FlushLogsIntegration" + } +} + +#endif diff --git a/Sources/Swift/SentryDependencyContainer.swift b/Sources/Swift/SentryDependencyContainer.swift index 8f8b441b658..93b8ab291ab 100644 --- a/Sources/Swift/SentryDependencyContainer.swift +++ b/Sources/Swift/SentryDependencyContainer.swift @@ -256,3 +256,7 @@ extension SentryFileManager: SentryFileManagerProtocol { } #if os(iOS) && !SENTRY_NO_UIKIT extension SentryDependencyContainer: ScreenshotSourceProvider { } #endif + +#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) +extension SentryDependencyContainer: NotificationCenterProvider { } +#endif diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift new file mode 100644 index 00000000000..bfc6402775f --- /dev/null +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -0,0 +1,121 @@ +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +final class SentryLogFlushIntegrationTests: XCTestCase { + + private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") + + private class Fixture { + let options: Options + let client: TestClient + let hub: SentryHubInternal + let dependencies: SentryDependencyContainer + let notificationCenterWrapper: TestNSNotificationCenterWrapper + + init() throws { + options = Options() + options.dsn = SentryLogFlushIntegrationTests.dsnAsString + options.enableLogs = true + + client = TestClient(options: options)! + hub = TestHub(client: client, andScope: nil) + dependencies = SentryDependencyContainer.sharedInstance() + notificationCenterWrapper = TestNSNotificationCenterWrapper() + dependencies.notificationCenterWrapper = notificationCenterWrapper + } + + func getSut() -> FlushLogsIntegration? { + return FlushLogsIntegration(with: options, dependencies: dependencies) + } + } + + private var fixture: Fixture! + + override func setUpWithError() throws { + try super.setUpWithError() + fixture = try Fixture() + SentrySDKInternal.setCurrentHub(fixture.hub) + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func testInstall_Success() { + let sut = fixture.getSut() + + XCTAssertNotNil(sut) + } + + func testInstall_FailsWhenLogsDisabled() { + fixture.options.enableLogs = false + + let sut = fixture.getSut() + + XCTAssertNil(sut) + } + + func testName_ReturnsCorrectName() { + XCTAssertEqual(FlushLogsIntegration.name, "FlushLogsIntegration") + } + + func testWillResignActive_FlushesLogs() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testWillTerminate_FlushesLogs() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testUninstall_RemovesObservers() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + + sut.uninstall() + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + // Should not flush logs after uninstall + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) + } + + func testMultipleNotifications_FlushesLogsMultipleTimes() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) + } +} +#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift deleted file mode 100644 index 95bf9acef77..00000000000 --- a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift +++ /dev/null @@ -1,106 +0,0 @@ -@_spi(Private) @testable import Sentry -@_spi(Private) import SentryTestUtils -import XCTest - -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -final class SentryLogFlushIntegrationTests: XCTestCase { - - private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") - - private class Fixture { - let options: Options - let client: TestClient - let hub: SentryHubInternal - - init() throws { - options = Options() - options.dsn = SentryLogFlushIntegrationTests.dsnAsString - options.enableLogs = true - - client = TestClient(options: options)! - hub = TestHub(client: client, andScope: nil) - } - - func getSut() -> SentryLogFlushIntegration { - return SentryLogFlushIntegration() - } - } - - private var fixture: Fixture! - - override func setUpWithError() throws { - try super.setUpWithError() - fixture = try Fixture() - SentrySDKInternal.setCurrentHub(fixture.hub) - } - - override func tearDown() { - super.tearDown() - clearTestState() - } - - func testInstall_Success() { - let sut = fixture.getSut() - let result = sut.install(with: fixture.options) - - XCTAssertTrue(result) - } - - func testInstall_FailsWhenLogsDisabled() { - fixture.options.enableLogs = false - - let sut = fixture.getSut() - let result = sut.install(with: fixture.options) - - XCTAssertFalse(result) - } - - func testIntegrationOptions_ReturnsEnableLogs() { - let sut = fixture.getSut() - let options = sut.integrationOptions() - - XCTAssertEqual(options, .integrationOptionEnableLogs) - } - - func testWillResignActive_FlushesLogs() { - let sut = fixture.getSut() - sut.install(with: fixture.options) - - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) - } - - func testWillTerminate_FlushesLogs() { - let sut = fixture.getSut() - sut.install(with: fixture.options) - - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) - } - - func testUninstall_RemovesObservers() { - let sut = fixture.getSut() - sut.install(with: fixture.options) - sut.uninstall() - - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - // Should not flush logs after uninstall - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) - } - - func testMultipleNotifications_FlushesLogsMultipleTimes() { - let sut = fixture.getSut() - sut.install(with: fixture.options) - - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - NotificationCenter.default.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) - } -} -#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 08aa78cd8a3..c9dec3767f6 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -124,7 +124,6 @@ #import "SentryInvalidJSONString.h" #import "SentryLevelMapper.h" #import "SentryLogC.h" -#import "SentryLogFlushIntegration.h" #import "SentryLogTestHelper.h" #import "SentryMechanism.h" #import "SentryMechanismContext.h" From e214d5e1d31cbcba0c3901d83c5b304253e9f3d6 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 14:04:49 +0100 Subject: [PATCH 10/25] use dedicated queue for log batcher --- Sources/Sentry/SentryClient.m | 16 ++- .../_SentryDispatchQueueWrapperInternal.m | 16 --- .../_SentryDispatchQueueWrapperInternal.h | 2 - .../Helper/SentryDispatchQueueWrapper.swift | 7 - Sources/Swift/Tools/SentryLogBatcher.swift | 10 +- .../Log/SentryLogFlushIntegrationTests.swift | 121 ++++++++++++++++++ .../SentryDispatchQueueWrapperTests.m | 73 ----------- Tests/SentryTests/SentryLogBatcherTests.swift | 38 ------ 8 files changed, 135 insertions(+), 148 deletions(-) create mode 100644 Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 44fdabbea00..ec9e93e6675 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -114,10 +114,18 @@ - (instancetype)initWithOptions:(SentryOptions *)options self.locale = locale; self.timezone = timezone; self.attachmentProcessors = [[NSMutableArray alloc] init]; - self.logBatcher = [[SentryLogBatcher alloc] - initWithOptions:options - dispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper - delegate:self]; + + // Uses DEFAULT priority (not LOW) because captureLogs() is called synchronously during + // app lifecycle events (willResignActive, willTerminate) and needs to complete quickly. + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + SentryDispatchQueueWrapper *logBatcherQueue = + [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.log-batcher" + attributes:attributes]; + + self.logBatcher = [[SentryLogBatcher alloc] initWithOptions:options + dispatchQueue:logBatcherQueue + delegate:self]; // The SDK stores the installationID in a file. The first call requires file IO. To avoid // executing this on the main thread, we cache the installationID async here. diff --git a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m index 5f176b0e961..da4c940b71e 100644 --- a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m +++ b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m @@ -20,8 +20,6 @@ - (instancetype)initWithName:(const char *)name { if (self = [super init]) { _queue = dispatch_queue_create(name, attributes); - void *key = (__bridge void *)self; - dispatch_queue_set_specific(_queue, key, key, NULL); } return self; } @@ -111,20 +109,6 @@ - (void)dispatchOnce:(dispatch_once_t *)predicate block:(void (^)(void))block dispatch_once(predicate, block); } -- (BOOL)isCurrentQueue -{ - void *key = (__bridge void *)self; - return dispatch_get_specific(key) == key; -} - -- (void)dealloc -{ - if (_queue != NULL) { - void *key = (__bridge void *)self; - dispatch_queue_set_specific(_queue, key, NULL, NULL); - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h index f7d6e2eed3a..060ac0e4a1c 100644 --- a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h +++ b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h @@ -33,8 +33,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)dispatchAsyncOnMainQueueIfNotMainThread:(void (^)(void))block NS_SWIFT_NAME(dispatchAsyncOnMainQueueIfNotMainThread(block:)); -- (BOOL)isCurrentQueue; - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift index 7eb1f2068bb..1a1e25c5742 100644 --- a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift +++ b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift @@ -71,11 +71,4 @@ public var shouldCreateDispatchBlock: Bool { return true } - - /// Returns `true` if the current execution context is on this queue. - /// Uses `dispatch_get_specific` with a unique context pointer for reliable detection, - /// as queue labels are not guaranteed to be unique. - @_spi(Private) public func isCurrentQueue() -> Bool { - return internalWrapper.isCurrentQueue() - } } diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 498a341e9e1..5151907c0ea 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -114,14 +114,8 @@ import Foundation @discardableResult @_spi(Private) @objc public func captureLogs() -> TimeInterval { let startTimeNs = SentryDefaultCurrentDateProvider.getAbsoluteTime() - - // Guard against sync call deadlock when we are already on the dispatchQueue.queue. - if dispatchQueue.isCurrentQueue() { - performCaptureLogs() - } else { - dispatchQueue.dispatchSync { [weak self] in - self?.performCaptureLogs() - } + dispatchQueue.dispatchSync { [weak self] in + self?.performCaptureLogs() } let endTimeNs = SentryDefaultCurrentDateProvider.getAbsoluteTime() return TimeInterval(endTimeNs - startTimeNs) / 1_000_000_000.0 // Convert nanoseconds to seconds diff --git a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift new file mode 100644 index 00000000000..6ffc919d081 --- /dev/null +++ b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift @@ -0,0 +1,121 @@ +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +final class SentryLogFlushIntegrationTests: XCTestCase { + + private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") + + private class Fixture { + let options: Options + let client: TestClient + let hub: SentryHubInternal + let dependencies: SentryDependencyContainer + let notificationCenterWrapper: TestNSNotificationCenterWrapper + + init() throws { + options = Options() + options.dsn = SentryLogFlushIntegrationTests.dsnAsString + options.enableLogs = true + + client = TestClient(options: options)! + hub = TestHub(client: client, andScope: nil) + dependencies = SentryDependencyContainer.sharedInstance() + notificationCenterWrapper = TestNSNotificationCenterWrapper() + dependencies.notificationCenterWrapper = notificationCenterWrapper + } + + func getSut() -> LogFlushIntegration? { + return LogFlushIntegration(with: options, dependencies: dependencies) + } + } + + private var fixture: Fixture! + + override func setUpWithError() throws { + try super.setUpWithError() + fixture = try Fixture() + SentrySDKInternal.setCurrentHub(fixture.hub) + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func testInstall_Success() { + let sut = fixture.getSut() + + XCTAssertNotNil(sut) + } + + func testInstall_FailsWhenLogsDisabled() { + fixture.options.enableLogs = false + + let sut = fixture.getSut() + + XCTAssertNil(sut) + } + + func testName_ReturnsCorrectName() { + XCTAssertEqual(LogFlushIntegration.name, "SentryLogFlushIntegration") + } + + func testWillResignActive_FlushesLogs() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testWillTerminate_FlushesLogs() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + } + + func testUninstall_RemovesObservers() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + + sut.uninstall() + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + + // Should not flush logs after uninstall + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) + } + + func testMultipleNotifications_FlushesLogsMultipleTimes() { + guard let sut = fixture.getSut() else { + XCTFail("Integration should be initialized") + return + } + // Keep sut alive so observers don't get deallocated + _ = sut + + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) + } +} +#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m index 26cf5d9b88c..e4361752075 100644 --- a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m +++ b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m @@ -78,79 +78,6 @@ - (void)testInitWithNameAndAttributes_customAttributes_shouldCreateQueueWithGive XCTAssertEqual(actualRelativePriority, 0); } -- (void)testIsCurrentQueue_whenCalledFromDifferentQueue_shouldReturnFalse -{ - // -- Arrange -- - SentryDispatchQueueWrapper *wrappedQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" - attributes:DISPATCH_QUEUE_SERIAL]; - - // -- Act & Assert -- - XCTAssertFalse([wrappedQueue isCurrentQueue]); -} - -- (void)testIsCurrentQueue_whenCalledFromWithinQueue_shouldReturnTrue -{ - // -- Arrange -- - SentryDispatchQueueWrapper *wrappedQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" - attributes:DISPATCH_QUEUE_SERIAL]; - XCTestExpectation *expectation = [self expectationWithDescription:@"queue execution"]; - - // -- Act -- - [wrappedQueue dispatchAsyncWithBlock:^{ - // -- Assert -- - XCTAssertTrue([wrappedQueue isCurrentQueue]); - [expectation fulfill]; - }]; - - // -- Wait -- - [self waitForExpectationsWithTimeout:1.0 handler:nil]; -} - -- (void)testIsCurrentQueue_whenCalledFromSyncDispatch_shouldReturnTrue -{ - // -- Arrange -- - SentryDispatchQueueWrapper *wrappedQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue" - attributes:DISPATCH_QUEUE_SERIAL]; - - // -- Act & Assert -- - dispatch_sync(wrappedQueue.queue, ^{ XCTAssertTrue([wrappedQueue isCurrentQueue]); }); -} - -- (void)testIsCurrentQueue_differentInstances_shouldHaveUniqueDetection -{ - // -- Arrange -- - SentryDispatchQueueWrapper *queue1 = - [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue1" - attributes:DISPATCH_QUEUE_SERIAL]; - SentryDispatchQueueWrapper *queue2 = - [[SentryDispatchQueueWrapper alloc] initWithName:"sentry.test.queue2" - attributes:DISPATCH_QUEUE_SERIAL]; - - // -- Act -- - XCTestExpectation *expectation = [self expectationWithDescription:@"queue execution"]; - [queue1 dispatchAsyncWithBlock:^{ - // -- Assert -- - XCTAssertTrue([queue1 isCurrentQueue], @"queue1 should detect it's on its own queue"); - XCTAssertFalse([queue2 isCurrentQueue], @"queue2 should not detect it's on queue1"); - [expectation fulfill]; - }]; - // -- Wait -- - [self waitForExpectationsWithTimeout:1.0 handler:nil]; - - // -- Act -- - XCTestExpectation *expectation2 = [self expectationWithDescription:@"queue execution 2"]; - [queue2 dispatchAsyncWithBlock:^{ - // -- Assert -- - XCTAssertFalse([queue1 isCurrentQueue], @"queue1 should not detect it's on queue2"); - XCTAssertTrue([queue2 isCurrentQueue], @"queue2 should detect it's on its own queue"); - [expectation2 fulfill]; - }]; - // -- Wait -- - [self waitForExpectationsWithTimeout:1.0 handler:nil]; -} @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryLogBatcherTests.swift b/Tests/SentryTests/SentryLogBatcherTests.swift index a46664d3fcf..962eba5238d 100644 --- a/Tests/SentryTests/SentryLogBatcherTests.swift +++ b/Tests/SentryTests/SentryLogBatcherTests.swift @@ -203,44 +203,6 @@ final class SentryLogBatcherTests: XCTestCase { XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 0) } - func testCaptureLogs_WhenAlreadyOnQueue_DoesNotDeadlock() { - // Arrange: Create a real dispatch queue wrapper (not test wrapper) to test actual queue behavior - let realDispatchQueue = SentryDispatchQueueWrapper(name: "io.sentry.test.log-batcher", attributes: nil) - let testDelegate = TestLogBatcherDelegate() - - let batcher = SentryLogBatcher( - options: options, - flushTimeout: 0.1, - maxLogCount: 10, - maxBufferSizeBytes: 8_000, - dispatchQueue: realDispatchQueue, - delegate: testDelegate - ) - - let log1 = createTestLog(body: "Log 1") - let log2 = createTestLog(body: "Log 2") - - batcher.addLog(log1, scope: scope) - batcher.addLog(log2, scope: scope) - - // Add logs asynchronously and wait for them to be processed - let addLogsExpectation = expectation(description: "logs added") - realDispatchQueue.dispatchAsync { - batcher.captureLogs() - addLogsExpectation.fulfill() - } - waitForExpectations(timeout: 1.0) { error in - if let error = error { - XCTFail("Test timed out or failed - possible deadlock: \(error)") - } - } - - let capturedLogs = testDelegate.getCapturedLogs() - XCTAssertEqual(capturedLogs.count, 2, "Should have captured both logs without deadlock.") - XCTAssertEqual(capturedLogs[0].body, "Log 1") - XCTAssertEqual(capturedLogs[1].body, "Log 2") - } - // MARK: - Edge Cases Tests func testScheduledFlushAfterBufferAlreadyFlushed_DoesNothing() throws { From 13de6227537d19f3d4bc54ba58729c3b214a1b3d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 14:10:02 +0100 Subject: [PATCH 11/25] update --- CHANGELOG.md | 4 +--- Sources/Sentry/_SentryDispatchQueueWrapperInternal.m | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0586bbb27..1739d8fe7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,6 @@ ## Unreleased -### Improvements - -- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) This changelog lists every breaking change. For a high-level overview and upgrade guidance, see the [migration guide](https://docs.sentry.io/platforms/apple/migration/). ### Breaking Changes @@ -86,6 +83,7 @@ This changelog lists every breaking change. For a high-level overview and upgrad - Expose attachment type on `SentryAttachment` for downstream SDKs (like sentry-godot) (#6521) - Increase attachment max size to 100MB (#6537) - Increase maximum attachment size to 200MB (#6726) +- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) ## 9.0.0-rc.1 diff --git a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m index da4c940b71e..8ca3a9da22c 100644 --- a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m +++ b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m @@ -32,8 +32,6 @@ - (instancetype)initWithName:(const char *)name relativePriority:(int)relativePr dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, relativePriority); _queue = dispatch_queue_create(name, attributes); - void *key = (__bridge void *)self; - dispatch_queue_set_specific(_queue, key, key, NULL); } return self; } From e270fa1238f2a3023dab45e923dd0baa8bdddb17 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 14:17:02 +0100 Subject: [PATCH 12/25] cleanup --- SentryTestUtils/Sources/TestClient.swift | 6 +- Sources/Sentry/SentryClient.m | 2 +- Sources/Sentry/include/SentryClient+Private.h | 2 +- .../Log/FlushLogsIntegration.swift | 4 +- .../Log/FlushLogsIntegrationTests.swift | 12 +- .../Log/SentryLogFlushIntegrationTests.swift | 121 ------------------ Tests/SentryTests/SentryClientTests.swift | 4 +- 7 files changed, 15 insertions(+), 136 deletions(-) delete mode 100644 Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift diff --git a/SentryTestUtils/Sources/TestClient.swift b/SentryTestUtils/Sources/TestClient.swift index 8416c39d795..86b50cd838c 100644 --- a/SentryTestUtils/Sources/TestClient.swift +++ b/SentryTestUtils/Sources/TestClient.swift @@ -168,8 +168,8 @@ public class TestClient: SentryClientInternal { } } - public var flushLogsInvocations = Invocations() - public override func flushLogs() { - flushLogsInvocations.record(()) + public var captureLogsInvocations = Invocations() + public override func captureLogs() { + captureLogsInvocations.record(()) } } diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index ec9e93e6675..2334d02a837 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -1113,7 +1113,7 @@ - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope } } -- (void)flushLogs +- (void)captureLogs { [self.logBatcher captureLogs]; } diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index 5aad9cdaf4e..9792434999a 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -82,7 +82,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope; -- (void)flushLogs; +- (void)captureLogs; @end diff --git a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift index ce2a1685b87..0b937fd5250 100644 --- a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift +++ b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift @@ -64,14 +64,14 @@ final class FlushLogsIntegration: NSOb guard let client = SentrySDKInternal.currentHub().getClient() else { return } - client.flushLogs() + client.captureLogs() } @objc private func willTerminate() { guard let client = SentrySDKInternal.currentHub().getClient() else { return } - client.flushLogs() + client.captureLogs() } static var name: String { diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift index bfc6402775f..101969e0ca4 100644 --- a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -3,7 +3,7 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -final class SentryLogFlushIntegrationTests: XCTestCase { +final class FlushLogsIntegrationTests: XCTestCase { private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") @@ -16,7 +16,7 @@ final class SentryLogFlushIntegrationTests: XCTestCase { init() throws { options = Options() - options.dsn = SentryLogFlushIntegrationTests.dsnAsString + options.dsn = FlushLogsIntegrationTests.dsnAsString options.enableLogs = true client = TestClient(options: options)! @@ -72,7 +72,7 @@ final class SentryLogFlushIntegrationTests: XCTestCase { fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + XCTAssertEqual(fixture.client.captureLogsInvocations.count, 1) } func testWillTerminate_FlushesLogs() { @@ -85,7 +85,7 @@ final class SentryLogFlushIntegrationTests: XCTestCase { fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) + XCTAssertEqual(fixture.client.captureLogsInvocations.count, 1) } func testUninstall_RemovesObservers() { @@ -100,7 +100,7 @@ final class SentryLogFlushIntegrationTests: XCTestCase { fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) // Should not flush logs after uninstall - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) + XCTAssertEqual(fixture.client.captureLogsInvocations.count, 0) } func testMultipleNotifications_FlushesLogsMultipleTimes() { @@ -115,7 +115,7 @@ final class SentryLogFlushIntegrationTests: XCTestCase { fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) + XCTAssertEqual(fixture.client.captureLogsInvocations.count, 3) } } #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift deleted file mode 100644 index 6ffc919d081..00000000000 --- a/Tests/SentryTests/Integrations/Log/SentryLogFlushIntegrationTests.swift +++ /dev/null @@ -1,121 +0,0 @@ -@_spi(Private) @testable import Sentry -@_spi(Private) import SentryTestUtils -import XCTest - -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -final class SentryLogFlushIntegrationTests: XCTestCase { - - private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") - - private class Fixture { - let options: Options - let client: TestClient - let hub: SentryHubInternal - let dependencies: SentryDependencyContainer - let notificationCenterWrapper: TestNSNotificationCenterWrapper - - init() throws { - options = Options() - options.dsn = SentryLogFlushIntegrationTests.dsnAsString - options.enableLogs = true - - client = TestClient(options: options)! - hub = TestHub(client: client, andScope: nil) - dependencies = SentryDependencyContainer.sharedInstance() - notificationCenterWrapper = TestNSNotificationCenterWrapper() - dependencies.notificationCenterWrapper = notificationCenterWrapper - } - - func getSut() -> LogFlushIntegration? { - return LogFlushIntegration(with: options, dependencies: dependencies) - } - } - - private var fixture: Fixture! - - override func setUpWithError() throws { - try super.setUpWithError() - fixture = try Fixture() - SentrySDKInternal.setCurrentHub(fixture.hub) - } - - override func tearDown() { - super.tearDown() - clearTestState() - } - - func testInstall_Success() { - let sut = fixture.getSut() - - XCTAssertNotNil(sut) - } - - func testInstall_FailsWhenLogsDisabled() { - fixture.options.enableLogs = false - - let sut = fixture.getSut() - - XCTAssertNil(sut) - } - - func testName_ReturnsCorrectName() { - XCTAssertEqual(LogFlushIntegration.name, "SentryLogFlushIntegration") - } - - func testWillResignActive_FlushesLogs() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) - } - - func testWillTerminate_FlushesLogs() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 1) - } - - func testUninstall_RemovesObservers() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - - sut.uninstall() - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - - // Should not flush logs after uninstall - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 0) - } - - func testMultipleNotifications_FlushesLogsMultipleTimes() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut - - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - XCTAssertEqual(fixture.client.flushLogsInvocations.count, 3) - } -} -#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 226a821fc3d..35b6918bb29 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -2438,7 +2438,7 @@ class SentryClientTests: XCTestCase { XCTAssertEqual(testBatcher.captureLogsInvocations.count, 1) } - func testFlushLogsCallsLogBatcherCaptureLogs() { + func testCaptureLogsCallsLogBatcherCaptureLogs() { let sut = fixture.getSut() let testDelegate = TestLogBatcherDelegateForClient() @@ -2451,7 +2451,7 @@ class SentryClientTests: XCTestCase { XCTAssertEqual(testBatcher.captureLogsInvocations.count, 0) - sut.flushLogs() + sut.captureLogs() XCTAssertEqual(testBatcher.captureLogsInvocations.count, 1) } From 19df878aa41080f8381239c0e12ea666f9e72d9f Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 1 Dec 2025 14:21:22 +0100 Subject: [PATCH 13/25] cleanup --- Sources/Swift/AppState/SentryAppStateManager.swift | 3 +-- Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/Swift/AppState/SentryAppStateManager.swift b/Sources/Swift/AppState/SentryAppStateManager.swift index f0df1c02aab..6d2d0acb84c 100644 --- a/Sources/Swift/AppState/SentryAppStateManager.swift +++ b/Sources/Swift/AppState/SentryAppStateManager.swift @@ -12,7 +12,7 @@ import UIKit #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT private let _updateAppState: (@escaping (SentryAppState) -> Void) -> Void private let _buildCurrentAppState: () -> SentryAppState - private var helper: SentryDefaultAppStateManager + private let helper: SentryDefaultAppStateManager #endif init(releaseName: String?, crashWrapper: SentryCrashWrapper, fileManager: SentryFileManager?, sysctlWrapper: SentrySysctl) { @@ -41,7 +41,6 @@ import UIKit } } _updateAppState = updateAppState - helper = SentryDefaultAppStateManager(storeCurrent: { fileManager?.store(buildCurrentAppState()) }, updateTerminated: { diff --git a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m index e4361752075..0a5a6e33efb 100644 --- a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m +++ b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m @@ -77,7 +77,6 @@ - (void)testInitWithNameAndAttributes_customAttributes_shouldCreateQueueWithGive XCTAssertEqual(actualQoSClass, QOS_CLASS_UNSPECIFIED); XCTAssertEqual(actualRelativePriority, 0); } - @end NS_ASSUME_NONNULL_END From e3f2df891273bbf8b7a4ee2df66e980559468601 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 10:32:22 +0100 Subject: [PATCH 14/25] update cl --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e45529391..45fca1a44b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Add attributes data to `SentryScope` (#6830) - Add `SentryScope` attributes into log messages (#6834) +### Improvements + +- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) + ## 9.0.0 This changelog lists every breaking change. For a high-level overview and upgrade guidance, see the [migration guide](https://docs.sentry.io/platforms/apple/migration/). @@ -90,7 +94,6 @@ This changelog lists every breaking change. For a high-level overview and upgrad - Expose attachment type on `SentryAttachment` for downstream SDKs (like sentry-godot) (#6521) - Increase attachment max size to 100MB (#6537) - Increase maximum attachment size to 200MB (#6726) -- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) ## 9.0.0-rc.1 From dd0006532f7734dcb25a611140b5c841872735db Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 11:52:20 +0100 Subject: [PATCH 15/25] fix build issue :facepalm --- Sources/Swift/Core/Integrations/Integrations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index c0dc9b857cc..1c7ace88bd6 100644 --- a/Sources/Swift/Core/Integrations/Integrations.swift +++ b/Sources/Swift/Core/Integrations/Integrations.swift @@ -37,7 +37,7 @@ private struct AnyIntegration { var integrations: [AnyIntegration] = [.init(SwiftAsyncIntegration.self)] #if os(iOS) && !SENTRY_NO_UIKIT - integrations.append(UserFeedbackIntegration.self) + integrations.append(.init(UserFeedbackIntegration.self)) #endif #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) From f2e5b8bd771860bee8413396029d1b21961c2fc6 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 11:54:28 +0100 Subject: [PATCH 16/25] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45fca1a44b3..6350c05be92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Improvements -- Flush Logs on `WillTerminate` or `WillResignActive` App State (#6909) +- Flush Logs on `WillTerminate` or `WillResignActive` Notifications (#6909) ## 9.0.0 From d828f699c62b5bb6b28ff9c0540de4d0cba39226 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 17:31:34 +0100 Subject: [PATCH 17/25] remove kIntegrationOptionEnableLogs --- Sources/Sentry/SentryBaseIntegration.m | 5 ----- Sources/Sentry/include/SentryBaseIntegration.h | 1 - 2 files changed, 6 deletions(-) diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 0df7aab75e4..1f79d446aa5 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -203,11 +203,6 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options #endif // SENTRY_HAS_UIKIT } - if ((integrationOptions & kIntegrationOptionEnableLogs) && !options.enableLogs) { - [self logWithOptionName:@"enableLogs"]; - return NO; - } - return YES; } diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h index f8256fc15b7..3f6ad613be1 100644 --- a/Sources/Sentry/include/SentryBaseIntegration.h +++ b/Sources/Sentry/include/SentryBaseIntegration.h @@ -30,7 +30,6 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) { kIntegrationOptionEnableMetricKit = 1 << 17, kIntegrationOptionEnableReplay = 1 << 18, kIntegrationOptionStartFramesTracker = 1 << 19, - kIntegrationOptionEnableLogs = 1 << 20, }; @class SentryOptions; From 3b22387fc748c2071afe36073f2891bb4cf7b1fc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 17:34:49 +0100 Subject: [PATCH 18/25] use dsnForTestCase --- Tests/SentryTests/Helper/SentryAppStateManagerTests.swift | 2 +- .../Integrations/Log/FlushLogsIntegrationTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift index 5d95d111a8a..452221859b9 100644 --- a/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift @@ -4,7 +4,7 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) final class SentryAppStateManagerTests: XCTestCase { - private static let dsnAsString = TestConstants.dsnAsString(username: "SentryAppStateManagerTests") + private static let dsnAsString = TestConstants.dsnForTestCase(type: SentryAppStateManagerTests.self) private class Fixture { diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift index 101969e0ca4..07d67fa86ed 100644 --- a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -5,7 +5,7 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) final class FlushLogsIntegrationTests: XCTestCase { - private static let dsnAsString = TestConstants.dsnAsString(username: "SentryLogFlushIntegrationTests") + private static let dsnAsString = TestConstants.dsnForTestCase(type: FlushLogsIntegrationTests.self) private class Fixture { let options: Options From 0682b5baca53055a9738e0fcfc55ed7e4dbd0fd7 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 17:42:52 +0100 Subject: [PATCH 19/25] update test setup --- .../Log/FlushLogsIntegrationTests.swift | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift index 07d67fa86ed..fb21b261e71 100644 --- a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -5,8 +5,6 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) final class FlushLogsIntegrationTests: XCTestCase { - private static let dsnAsString = TestConstants.dsnForTestCase(type: FlushLogsIntegrationTests.self) - private class Fixture { let options: Options let client: TestClient @@ -16,7 +14,7 @@ final class FlushLogsIntegrationTests: XCTestCase { init() throws { options = Options() - options.dsn = FlushLogsIntegrationTests.dsnAsString + options.dsn = TestConstants.dsnForTestCase(type: FlushLogsIntegrationTests.self) options.enableLogs = true client = TestClient(options: options)! @@ -32,6 +30,7 @@ final class FlushLogsIntegrationTests: XCTestCase { } private var fixture: Fixture! + private var sut: FlushLogsIntegration? override func setUpWithError() throws { try super.setUpWithError() @@ -42,33 +41,29 @@ final class FlushLogsIntegrationTests: XCTestCase { override func tearDown() { super.tearDown() clearTestState() + sut = nil } func testInstall_Success() { - let sut = fixture.getSut() - + sut = fixture.getSut() XCTAssertNotNil(sut) } func testInstall_FailsWhenLogsDisabled() { fixture.options.enableLogs = false - - let sut = fixture.getSut() + sut = fixture.getSut() XCTAssertNil(sut) } func testName_ReturnsCorrectName() { + sut = fixture.getSut() + XCTAssertEqual(FlushLogsIntegration.name, "FlushLogsIntegration") } func testWillResignActive_FlushesLogs() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut + sut = fixture.getSut() fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) @@ -76,12 +71,7 @@ final class FlushLogsIntegrationTests: XCTestCase { } func testWillTerminate_FlushesLogs() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut + sut = fixture.getSut() fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) @@ -89,11 +79,10 @@ final class FlushLogsIntegrationTests: XCTestCase { } func testUninstall_RemovesObservers() { - guard let sut = fixture.getSut() else { + guard let sut = fixture.getSut()else { XCTFail("Integration should be initialized") return } - sut.uninstall() fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) @@ -104,12 +93,7 @@ final class FlushLogsIntegrationTests: XCTestCase { } func testMultipleNotifications_FlushesLogsMultipleTimes() { - guard let sut = fixture.getSut() else { - XCTFail("Integration should be initialized") - return - } - // Keep sut alive so observers don't get deallocated - _ = sut + sut = fixture.getSut() fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) From fda13c283dad2e6162303a7c217991825beb1854 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 3 Dec 2025 17:55:09 +0100 Subject: [PATCH 20/25] Call with QOS_CLASS_DEFAULT instead of DISPATCH_QUEUE_PRIORITY_DEFAULT --- Sources/Sentry/SentryClient.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 2334d02a837..c071620c149 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -117,8 +117,8 @@ - (instancetype)initWithOptions:(SentryOptions *)options // Uses DEFAULT priority (not LOW) because captureLogs() is called synchronously during // app lifecycle events (willResignActive, willTerminate) and needs to complete quickly. - dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_queue_attr_t attributes + = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0); SentryDispatchQueueWrapper *logBatcherQueue = [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.log-batcher" attributes:attributes]; From 7a57e7a3714b18ed83a5f23e70967353c841c901 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 4 Dec 2025 11:36:12 +0100 Subject: [PATCH 21/25] fix incorrect import of NSApplication in macCatalyst environments --- .../Integrations/FramesTracking/SentryFramesTracker.swift | 2 +- Sources/Swift/Core/Integrations/Integrations.swift | 3 ++- Sources/Swift/Integrations/Log/FlushLogsIntegration.swift | 4 ++-- Sources/Swift/Integrations/Session/SessionTracker.swift | 6 +++--- Sources/Swift/SentryDependencyContainer.swift | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/Swift/Core/Integrations/FramesTracking/SentryFramesTracker.swift b/Sources/Swift/Core/Integrations/FramesTracking/SentryFramesTracker.swift index 9025f687b2d..7ba9f7ea632 100644 --- a/Sources/Swift/Core/Integrations/FramesTracking/SentryFramesTracker.swift +++ b/Sources/Swift/Core/Integrations/FramesTracking/SentryFramesTracker.swift @@ -4,7 +4,7 @@ #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT import UIKit private typealias CrossPlatformApplication = UIApplication -#elseif (os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT +#elseif os(macOS) import AppKit private typealias CrossPlatformApplication = NSApplication #endif diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index 1c7ace88bd6..f187415607a 100644 --- a/Sources/Swift/Core/Integrations/Integrations.swift +++ b/Sources/Swift/Core/Integrations/Integrations.swift @@ -36,11 +36,12 @@ private struct AnyIntegration { let dependencies = SentryDependencyContainer.sharedInstance() var integrations: [AnyIntegration] = [.init(SwiftAsyncIntegration.self)] + #if os(iOS) && !SENTRY_NO_UIKIT integrations.append(.init(UserFeedbackIntegration.self)) #endif - #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) + #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS) integrations.append(.init(FlushLogsIntegration.self)) #endif diff --git a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift index 0b937fd5250..5d57362a64f 100644 --- a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift +++ b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift @@ -3,12 +3,12 @@ #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT import UIKit private typealias CrossPlatformApplication = UIApplication -#elseif (os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT +#elseif os(macOS) import AppKit private typealias CrossPlatformApplication = NSApplication #endif -#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) +#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS) protocol NotificationCenterProvider { var notificationCenterWrapper: SentryNSNotificationCenterWrapper { get } diff --git a/Sources/Swift/Integrations/Session/SessionTracker.swift b/Sources/Swift/Integrations/Session/SessionTracker.swift index 87ebce7145a..12e3e24cf34 100644 --- a/Sources/Swift/Integrations/Session/SessionTracker.swift +++ b/Sources/Swift/Integrations/Session/SessionTracker.swift @@ -3,7 +3,7 @@ #if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT import UIKit typealias Application = UIApplication -#elseif (os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT +#elseif os(macOS) import AppKit typealias Application = NSApplication #endif @@ -50,7 +50,7 @@ typealias Application = NSApplication // WillTerminate is called no matter if started from the background or launched into the // foreground. - #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) + #if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS) // Call before subscribing to the notifications to avoid that didBecomeActive gets called before // ending the cached session. @@ -84,7 +84,7 @@ typealias Application = NSApplication } @objc public func removeObservers() { -#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) +#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS) // Remove the observers with the most specific detail possible, see // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1413994-removeobserver notificationCenter.removeObserver(self, name: Application.didBecomeActiveNotification, object: nil) diff --git a/Sources/Swift/SentryDependencyContainer.swift b/Sources/Swift/SentryDependencyContainer.swift index 93b8ab291ab..509afda481e 100644 --- a/Sources/Swift/SentryDependencyContainer.swift +++ b/Sources/Swift/SentryDependencyContainer.swift @@ -257,6 +257,6 @@ extension SentryFileManager: SentryFileManagerProtocol { } extension SentryDependencyContainer: ScreenshotSourceProvider { } #endif -#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || ((os(macOS) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT) +#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS) extension SentryDependencyContainer: NotificationCenterProvider { } #endif From 5b106431d24edfd6a0654af058a08ab433a60d39 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 9 Dec 2025 11:19:02 +0100 Subject: [PATCH 22/25] =?UTF-8?q?Let=20the=20batcher=20create=20it?= =?UTF-8?q?=E2=80=99s=20own=20serial=20default=20queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Sentry/SentryClient.m | 12 +---------- .../_SentryDispatchQueueWrapperInternal.m | 16 ++++++++++----- .../_SentryDispatchQueueWrapperInternal.h | 2 ++ .../Helper/SentryDispatchQueueWrapper.swift | 4 ++++ Sources/Swift/Tools/SentryLogBatcher.swift | 9 ++++----- .../SentryDispatchQueueWrapperTests.m | 20 +++++++++++++++++++ Tests/SentryTests/SentryClientTests.swift | 9 +++++++++ 7 files changed, 51 insertions(+), 21 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 7b769c29245..7128b606b59 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -116,17 +116,7 @@ - (instancetype)initWithOptions:(SentryOptions *)options self.timezone = timezone; self.attachmentProcessors = [[NSMutableArray alloc] init]; - // Uses DEFAULT priority (not LOW) because captureLogs() is called synchronously during - // app lifecycle events (willResignActive, willTerminate) and needs to complete quickly. - dispatch_queue_attr_t attributes - = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0); - SentryDispatchQueueWrapper *logBatcherQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.log-batcher" - attributes:attributes]; - - self.logBatcher = [[SentryLogBatcher alloc] initWithOptions:options - dispatchQueue:logBatcherQueue - delegate:self]; + self.logBatcher = [[SentryLogBatcher alloc] initWithOptions:options delegate:self]; // The SDK stores the installationID in a file. The first call requires file IO. To avoid // executing this on the main thread, we cache the installationID async here. diff --git a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m index 8ca3a9da22c..c0fb0ba1a5b 100644 --- a/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m +++ b/Sources/Sentry/_SentryDispatchQueueWrapperInternal.m @@ -7,11 +7,17 @@ @implementation _SentryDispatchQueueWrapperInternal - (instancetype)init { - // DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL is requires iOS 10. Since we are targeting - // iOS 9 we need to manually add the autoreleasepool. - dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - self = [self initWithName:"io.sentry.default" attributes:attributes]; + self = [self initWithName:"io.sentry.default"]; + return self; +} + +- (instancetype)initWithName:(const char *)name +{ + if (self = [super init]) { + dispatch_queue_attr_t attributes + = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0); + _queue = dispatch_queue_create(name, attributes); + } return self; } diff --git a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h index 060ac0e4a1c..7460e14e73d 100644 --- a/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h +++ b/Sources/Sentry/include/_SentryDispatchQueueWrapperInternal.h @@ -12,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN @property (strong, nonatomic) dispatch_queue_t queue; +- (instancetype)initWithName:(const char *)name; + - (instancetype)initWithName:(const char *)name attributes:(nullable dispatch_queue_attr_t)attributes; diff --git a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift index 1a1e25c5742..9a6966716de 100644 --- a/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift +++ b/Sources/Swift/Helper/SentryDispatchQueueWrapper.swift @@ -10,6 +10,10 @@ public override init() { internalWrapper = _SentryDispatchQueueWrapperInternal() } + + public init(name: UnsafePointer) { + internalWrapper = _SentryDispatchQueueWrapperInternal(name: name) + } public init(name: UnsafePointer, relativePriority: Int32) { internalWrapper = _SentryDispatchQueueWrapperInternal(name: name, relativePriority: relativePriority) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index ca64d6d7c7d..fef794a8c5b 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -26,20 +26,19 @@ import Foundation private weak var delegate: SentryLogBatcherDelegate? /// Convenience initializer with default flush timeout, max log count (100), and buffer size. + /// Creates its own serial dispatch queue with DEFAULT QoS for thread-safe access to mutable state. /// - Parameters: /// - options: The Sentry configuration options - /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state /// - delegate: The delegate to handle captured log batches /// - /// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety. - /// Passing a concurrent queue will result in undefined behavior and potential data races. - /// + /// - Note: Uses DEFAULT priority (not LOW) because captureLogs() is called synchronously during + /// app lifecycle events (willResignActive, willTerminate) and needs to complete quickly. /// - Note: Setting `maxLogCount` to 100. While Replay hard limit is 1000, we keep this lower, as it's hard to lower once released. @_spi(Private) public convenience init( options: Options, - dispatchQueue: SentryDispatchQueueWrapper, delegate: SentryLogBatcherDelegate ) { + let dispatchQueue = SentryDispatchQueueWrapper(name: "io.sentry.log-batcher") self.init( options: options, flushTimeout: 5, diff --git a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m index 0a5a6e33efb..94bf24ab9ec 100644 --- a/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m +++ b/Tests/SentryTests/Networking/SentryDispatchQueueWrapperTests.m @@ -77,6 +77,26 @@ - (void)testInitWithNameAndAttributes_customAttributes_shouldCreateQueueWithGive XCTAssertEqual(actualQoSClass, QOS_CLASS_UNSPECIFIED); XCTAssertEqual(actualRelativePriority, 0); } + +- (void)testInitWithName_shouldCreateQueueWithDefaultQoS +{ + // -- Arrange -- + const char *queueName = "sentry-dispatch-factory.test-default-qos"; + + // -- Act -- + SentryDispatchQueueWrapper *wrappedQueue = + [[SentryDispatchQueueWrapper alloc] initWithName:queueName]; + + // -- Assert -- + const char *actualName = dispatch_queue_get_label(wrappedQueue.queue); + XCTAssertEqual(strcmp(actualName, queueName), 0); + + int actualRelativePriority; + dispatch_qos_class_t actualQoSClass + = dispatch_queue_get_qos_class(wrappedQueue.queue, &actualRelativePriority); + XCTAssertEqual(actualQoSClass, QOS_CLASS_DEFAULT); + XCTAssertEqual(actualRelativePriority, 0); +} @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 36445069514..38ef6e9d4d0 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -2398,6 +2398,9 @@ class SentryClientTests: XCTestCase { let testDelegate = TestLogBatcherDelegateForClient() let testBatcher = TestLogBatcherForClient( options: sut.options, + flushTimeout: 5, + maxLogCount: 100, + maxBufferSizeBytes: 1_024 * 1_024, dispatchQueue: TestSentryDispatchQueueWrapper(), delegate: testDelegate ) @@ -2426,6 +2429,9 @@ class SentryClientTests: XCTestCase { let testDelegate = TestLogBatcherDelegateForClient() let testBatcher = TestLogBatcherForClient( options: sut.options, + flushTimeout: 5, + maxLogCount: 100, + maxBufferSizeBytes: 1_024 * 1_024, dispatchQueue: TestSentryDispatchQueueWrapper(), delegate: testDelegate ) @@ -2444,6 +2450,9 @@ class SentryClientTests: XCTestCase { let testDelegate = TestLogBatcherDelegateForClient() let testBatcher = TestLogBatcherForClient( options: sut.options, + flushTimeout: 5, + maxLogCount: 100, + maxBufferSizeBytes: 1_024 * 1_024, dispatchQueue: TestSentryDispatchQueueWrapper(), delegate: testDelegate ) From 43496535b4b6070e72472b9b5845a8a664d4959b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 9 Dec 2025 11:22:40 +0100 Subject: [PATCH 23/25] add sdk debug message --- Sources/Swift/Integrations/Log/FlushLogsIntegration.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift index 5d57362a64f..b7861d92a95 100644 --- a/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift +++ b/Sources/Swift/Integrations/Log/FlushLogsIntegration.swift @@ -62,6 +62,7 @@ final class FlushLogsIntegration: NSOb @objc private func willResignActive() { guard let client = SentrySDKInternal.currentHub().getClient() else { + SentrySDKLog.debug("No need to flush logs on `willResignActive` because there is no client.") return } client.captureLogs() @@ -69,6 +70,7 @@ final class FlushLogsIntegration: NSOb @objc private func willTerminate() { guard let client = SentrySDKInternal.currentHub().getClient() else { + SentrySDKLog.debug("No need to flush logs on `willTerminate` because there is no client.") return } client.captureLogs() From 97b6ff7a12c96ce390f43e289e737150b18e23ad Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 9 Dec 2025 11:25:18 +0100 Subject: [PATCH 24/25] remove fixture --- .../Log/FlushLogsIntegrationTests.swift | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift index fb21b261e71..d8d4b572b67 100644 --- a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -5,37 +5,27 @@ import XCTest #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) final class FlushLogsIntegrationTests: XCTestCase { - private class Fixture { - let options: Options - let client: TestClient - let hub: SentryHubInternal - let dependencies: SentryDependencyContainer - let notificationCenterWrapper: TestNSNotificationCenterWrapper - - init() throws { - options = Options() - options.dsn = TestConstants.dsnForTestCase(type: FlushLogsIntegrationTests.self) - options.enableLogs = true - - client = TestClient(options: options)! - hub = TestHub(client: client, andScope: nil) - dependencies = SentryDependencyContainer.sharedInstance() - notificationCenterWrapper = TestNSNotificationCenterWrapper() - dependencies.notificationCenterWrapper = notificationCenterWrapper - } - - func getSut() -> FlushLogsIntegration? { - return FlushLogsIntegration(with: options, dependencies: dependencies) - } - } - - private var fixture: Fixture! + private var options: Options! + private var client: TestClient! + private var hub: SentryHubInternal! + private var dependencies: SentryDependencyContainer! + private var notificationCenterWrapper: TestNSNotificationCenterWrapper! private var sut: FlushLogsIntegration? override func setUpWithError() throws { try super.setUpWithError() - fixture = try Fixture() - SentrySDKInternal.setCurrentHub(fixture.hub) + + options = Options() + options.dsn = TestConstants.dsnForTestCase(type: FlushLogsIntegrationTests.self) + options.enableLogs = true + + client = TestClient(options: options)! + hub = TestHub(client: client, andScope: nil) + dependencies = SentryDependencyContainer.sharedInstance() + notificationCenterWrapper = TestNSNotificationCenterWrapper() + dependencies.notificationCenterWrapper = notificationCenterWrapper + + SentrySDKInternal.setCurrentHub(hub) } override func tearDown() { @@ -45,61 +35,61 @@ final class FlushLogsIntegrationTests: XCTestCase { } func testInstall_Success() { - sut = fixture.getSut() + sut = FlushLogsIntegration(with: options, dependencies: dependencies) XCTAssertNotNil(sut) } func testInstall_FailsWhenLogsDisabled() { - fixture.options.enableLogs = false - sut = fixture.getSut() + options.enableLogs = false + sut = FlushLogsIntegration(with: options, dependencies: dependencies) XCTAssertNil(sut) } func testName_ReturnsCorrectName() { - sut = fixture.getSut() + sut = FlushLogsIntegration(with: options, dependencies: dependencies) XCTAssertEqual(FlushLogsIntegration.name, "FlushLogsIntegration") } func testWillResignActive_FlushesLogs() { - sut = fixture.getSut() + sut = FlushLogsIntegration(with: options, dependencies: dependencies) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - XCTAssertEqual(fixture.client.captureLogsInvocations.count, 1) + XCTAssertEqual(client.captureLogsInvocations.count, 1) } func testWillTerminate_FlushesLogs() { - sut = fixture.getSut() + sut = FlushLogsIntegration(with: options, dependencies: dependencies) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - XCTAssertEqual(fixture.client.captureLogsInvocations.count, 1) + XCTAssertEqual(client.captureLogsInvocations.count, 1) } func testUninstall_RemovesObservers() { - guard let sut = fixture.getSut()else { + guard let sut = FlushLogsIntegration(with: options, dependencies: dependencies) else { XCTFail("Integration should be initialized") return } sut.uninstall() - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) // Should not flush logs after uninstall - XCTAssertEqual(fixture.client.captureLogsInvocations.count, 0) + XCTAssertEqual(client.captureLogsInvocations.count, 0) } func testMultipleNotifications_FlushesLogsMultipleTimes() { - sut = fixture.getSut() + sut = FlushLogsIntegration(with: options, dependencies: dependencies) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) - fixture.notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willTerminateNotification)) + notificationCenterWrapper.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - XCTAssertEqual(fixture.client.captureLogsInvocations.count, 3) + XCTAssertEqual(client.captureLogsInvocations.count, 3) } } #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) From dd6fb88a7fb905d2a898f1375b22d33ddf8c4340 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 9 Dec 2025 11:27:51 +0100 Subject: [PATCH 25/25] dont call clearTestState --- .../Integrations/Log/FlushLogsIntegrationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift index d8d4b572b67..fcbd7729825 100644 --- a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift @@ -30,12 +30,12 @@ final class FlushLogsIntegrationTests: XCTestCase { override func tearDown() { super.tearDown() - clearTestState() + SentrySDKInternal.setCurrentHub(nil) sut = nil } func testInstall_Success() { - sut = FlushLogsIntegration(with: options, dependencies: dependencies) + let sut = FlushLogsIntegration(with: options, dependencies: dependencies) XCTAssertNotNil(sut) }