diff --git a/CHANGELOG.md b/CHANGELOG.md index 338dbe88e9..82adf069af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,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/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 46c011026f..f58372f82b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -705,10 +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 */; }; + 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 /* 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 */; }; @@ -2066,10 +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 = ""; }; + 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 /* 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 = ""; }; @@ -3509,6 +3513,7 @@ 7B944FA924697E9700A10721 /* Integrations */ = { isa = PBXGroup; children = ( + 927D21F42ED5DE7800916D31 /* Log */, 843FB3422D156B9900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, D808FB85281AB2EF009A2A33 /* UIEvents */, @@ -4187,6 +4192,31 @@ name = Transaction; sourceTree = ""; }; + 9246A2352ED5CFDC002FA318 /* AppState */ = { + isa = PBXGroup; + children = ( + FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, + FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, + ); + path = AppState; + sourceTree = ""; + }; + 925189AB2EDDA6A300557BD1 /* Log */ = { + isa = PBXGroup; + children = ( + 925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */, + ); + path = Log; + sourceTree = ""; + }; + 927D21F42ED5DE7800916D31 /* Log */ = { + isa = PBXGroup; + children = ( + 927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */, + ); + path = Log; + sourceTree = ""; + }; D4009EA02D77196F0007AF30 /* ViewCapture */ = { isa = PBXGroup; children = ( @@ -4455,9 +4485,9 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + 9246A2352ED5CFDC002FA318 /* AppState */, FAAB95CC2EA18B260030A2DB /* SentryDependencyContainer.swift */, FAAB95B92EA1633E0030A2DB /* State */, - FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */, F429D37E2E8532A300DBF387 /* Networking */, F4FE9E062E6248BB0014FED5 /* SentryCrash */, FABB48B22E59310D0071397E /* Transaction */, @@ -4470,7 +4500,6 @@ D856272A2A374A6800FB8062 /* Tools */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, FA4C32962DF7513F001D7B00 /* SentryExperimentalOptions.swift */, - FA4C32972DF7513F001D7B01 /* SentryAppState.swift */, FA6251FE2EB52DD700BFC967 /* SentryHub.swift */, FA6252052EB5489B00BFC967 /* SentryClient.swift */, FA27EC152EB9236000F2ECF7 /* Options.swift */, @@ -4831,6 +4860,7 @@ D8CAC02D2BA0663E00E38F34 /* Integrations */ = { isa = PBXGroup; children = ( + 925189AB2EDDA6A300557BD1 /* Log */, FAB0073C2E9F47DE001C806A /* Session */, FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */, D49064862DFAE1B700555785 /* Screenshot */, @@ -5772,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 */, @@ -6315,6 +6346,7 @@ D88817DD26D72BA500BF2251 /* SentryTraceContextTests.swift in Sources */, D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */, 7B984A9F28E572AF001F4BEE /* CrashReport.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/SentryTestUtils/Sources/TestClient.swift b/SentryTestUtils/Sources/TestClient.swift index 57edc820ab..86b50cd838 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 captureLogsInvocations = Invocations() + public override func captureLogs() { + captureLogsInvocations.record(()) + } } diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 1f79d446aa..0df7aab75e 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 a578cf25ff..2334d02a83 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. @@ -1105,6 +1113,11 @@ - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope } } +- (void)captureLogs +{ + [self.logBatcher captureLogs]; +} + - (void)captureLogsData:(NSData *)data with:(NSNumber *)itemCount { SentryEnvelopeItem *envelopeItem = diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h index 3f6ad613be..f8256fc15b 100644 --- a/Sources/Sentry/include/SentryBaseIntegration.h +++ b/Sources/Sentry/include/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 a2676f57b4..9792434999 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)captureLogs; + @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/SentryAppStateManager.swift b/Sources/Swift/AppState/SentryAppStateManager.swift similarity index 100% rename from Sources/Swift/SentryAppStateManager.swift rename to Sources/Swift/AppState/SentryAppStateManager.swift diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index b265df7ce6..c7ae482904 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 0000000000..0b937fd525 --- /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.captureLogs() + } + + @objc private func willTerminate() { + guard let client = SentrySDKInternal.currentHub().getClient() else { + return + } + client.captureLogs() + } + + static var name: String { + "FlushLogsIntegration" + } +} + +#endif diff --git a/Sources/Swift/SentryDependencyContainer.swift b/Sources/Swift/SentryDependencyContainer.swift index 8f8b441b65..93b8ab291a 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/Helper/SentryAppStateManagerTests.swift b/Tests/SentryTests/Helper/SentryAppStateManagerTests.swift index 8cecc76b4a..5d95d111a8 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() } diff --git a/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift b/Tests/SentryTests/Integrations/Log/FlushLogsIntegrationTests.swift new file mode 100644 index 0000000000..101969e0ca --- /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 FlushLogsIntegrationTests: 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 = FlushLogsIntegrationTests.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.captureLogsInvocations.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.captureLogsInvocations.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.captureLogsInvocations.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.captureLogsInvocations.count, 3) + } +} +#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 13a7e9f67a..35b6918bb2 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -2423,7 +2423,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, @@ -2432,13 +2431,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 testCaptureLogsCallsLogBatcherCaptureLogs() { + 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.captureLogs() + XCTAssertEqual(testBatcher.captureLogsInvocations.count, 1) }