Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
be66378
Flush logs when app terminates or resigns active
denrase Nov 25, 2025
fd55fdf
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Nov 25, 2025
115a625
fix deadlock issue
denrase Nov 25, 2025
71f478e
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Nov 26, 2025
162273e
rename group name
denrase Nov 26, 2025
3f4813d
move into existing confitional block
denrase Nov 26, 2025
aa1fbc9
Use dispatch_queue_set_specific/dispatch_get_specific to get corrrect…
denrase Nov 26, 2025
6cdd1b0
cleanup queu specific key
denrase Nov 26, 2025
8e36044
check client for nil
denrase Nov 26, 2025
d502009
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Dec 1, 2025
d22d16e
Listen to notifications directly in integration
denrase Dec 1, 2025
539d652
move integration to swift class
denrase Dec 1, 2025
e214d5e
use dedicated queue for log batcher
denrase Dec 1, 2025
b493f18
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Dec 1, 2025
13de622
update
denrase Dec 1, 2025
e270fa1
cleanup
denrase Dec 1, 2025
19df878
cleanup
denrase Dec 1, 2025
0f74c74
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Dec 1, 2025
6daca9e
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Dec 3, 2025
e3f2df8
update cl
denrase Dec 3, 2025
dd00065
fix build issue :facepalm
denrase Dec 3, 2025
f2e5b8b
update changelog
denrase Dec 3, 2025
f9a7ed8
Merge branch 'main' into feat/flush-logs-on-app-state-change
philprime Dec 3, 2025
d828f69
remove kIntegrationOptionEnableLogs
denrase Dec 3, 2025
3b22387
use dsnForTestCase
denrase Dec 3, 2025
0682b5b
update test setup
denrase Dec 3, 2025
fda13c2
Call with QOS_CLASS_DEFAULT instead of DISPATCH_QUEUE_PRIORITY_DEFAULT
denrase Dec 3, 2025
2edc82f
Merge branch 'main' into feat/flush-logs-on-app-state-change
denrase Dec 4, 2025
7a57e7a
fix incorrect import of NSApplication in macCatalyst environments
denrase Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Add attributes data to `SentryScope` (#6830)
- Add `SentryScope` attributes into log messages (#6834)

### Improvements

- Flush Logs on `WillTerminate` or `WillResignActive` Notifications (#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/).
Expand Down
36 changes: 34 additions & 2 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2065,10 +2067,12 @@
92235CAB2E15369900865983 /* SentryLogBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcher.swift; sourceTree = "<group>"; };
92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = "<group>"; };
92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = "<group>"; };
925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlushLogsIntegration.swift; sourceTree = "<group>"; };
9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = "<group>"; };
9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = "<group>"; };
92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = "<group>"; };
927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = "<group>"; };
927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlushLogsIntegrationTests.swift; sourceTree = "<group>"; };
928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = "<group>"; };
9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = "<group>"; };
9286059629A5098900F96038 /* SentryGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryGeo.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3508,6 +3512,7 @@
7B944FA924697E9700A10721 /* Integrations */ = {
isa = PBXGroup;
children = (
927D21F42ED5DE7800916D31 /* Log */,
843FB3422D156B9900558F18 /* Feedback */,
7BF6505D292B77D100BBA5A8 /* MetricKit */,
D808FB85281AB2EF009A2A33 /* UIEvents */,
Expand Down Expand Up @@ -4185,6 +4190,31 @@
name = Transaction;
sourceTree = "<group>";
};
9246A2352ED5CFDC002FA318 /* AppState */ = {
isa = PBXGroup;
children = (
FA4C32972DF7513F001D7B01 /* SentryAppState.swift */,
FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */,
);
path = AppState;
sourceTree = "<group>";
};
925189AB2EDDA6A300557BD1 /* Log */ = {
isa = PBXGroup;
children = (
925189AA2EDDA6A300557BD1 /* FlushLogsIntegration.swift */,
);
path = Log;
sourceTree = "<group>";
};
927D21F42ED5DE7800916D31 /* Log */ = {
isa = PBXGroup;
children = (
927D21FA2ED5DE7F00916D31 /* FlushLogsIntegrationTests.swift */,
);
path = Log;
sourceTree = "<group>";
};
D4009EA02D77196F0007AF30 /* ViewCapture */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4453,9 +4483,9 @@
D800942328F82E8D005D3943 /* Swift */ = {
isa = PBXGroup;
children = (
9246A2352ED5CFDC002FA318 /* AppState */,
FAAB95CC2EA18B260030A2DB /* SentryDependencyContainer.swift */,
FAAB95B92EA1633E0030A2DB /* State */,
FA560F5A2E8C876A00F2AF7F /* SentryAppStateManager.swift */,
F429D37E2E8532A300DBF387 /* Networking */,
F4FE9E062E6248BB0014FED5 /* SentryCrash */,
FABB48B22E59310D0071397E /* Transaction */,
Expand All @@ -4468,7 +4498,6 @@
D856272A2A374A6800FB8062 /* Tools */,
D8B665BB2B95F5A100BD0E7B /* module.modulemap */,
FA4C32962DF7513F001D7B00 /* SentryExperimentalOptions.swift */,
FA4C32972DF7513F001D7B01 /* SentryAppState.swift */,
FA6251FE2EB52DD700BFC967 /* SentryHub.swift */,
FA6252052EB5489B00BFC967 /* SentryClient.swift */,
FA27EC152EB9236000F2ECF7 /* Options.swift */,
Expand Down Expand Up @@ -4828,6 +4857,7 @@
D8CAC02D2BA0663E00E38F34 /* Integrations */ = {
isa = PBXGroup;
children = (
925189AB2EDDA6A300557BD1 /* Log */,
FAB0073C2E9F47DE001C806A /* Session */,
FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */,
FAD882C12EDAADF90055AA44 /* SwiftAsyncIntegration.swift */,
Expand Down Expand Up @@ -5769,6 +5799,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 */,
Expand Down Expand Up @@ -6313,6 +6344,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 */,
Expand Down
5 changes: 5 additions & 0 deletions SentryTestUtils/Sources/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,9 @@ public class TestClient: SentryClientInternal {
captureLogInvocations.record((castLog, scope))
}
}

public var captureLogsInvocations = Invocations<Void>()
public override func captureLogs() {
captureLogsInvocations.record(())
}
}
21 changes: 17 additions & 4 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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];
Comment on lines +118 to +128
Copy link
Member

Choose a reason for hiding this comment

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

m: Thanks for adding an extra dispatch queue. What bugs me a bit is that the SentryLogBatcher specifically needs the dispatch queue above to work correctly, so the init of this specific DispatchQueueWrapper should be in the SentryLogBatcher.swift file if possible, IMO. What about removing the SentryDispatchQueueWrapper param from the convenience init and let the convenience init create the SentryDispatchQueueWrapper. We anyways use the other init method for tests.


// 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.
Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ NS_ASSUME_NONNULL_BEGIN

- (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope;

- (void)captureLogs;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

m: Is this change still intended?

import AppKit
private typealias CrossPlatformApplication = NSApplication
#endif
Expand Down
13 changes: 9 additions & 4 deletions Sources/Swift/Core/Integrations/Integrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ private struct AnyIntegration {
@_spi(Private) @objc public final class SentrySwiftIntegrationInstaller: NSObject {
@objc public class func install(with options: Options) {
let dependencies = SentryDependencyContainer.sharedInstance()
let commonIntegrations: [AnyIntegration] = [.init(SwiftAsyncIntegration.self)]

var integrations: [AnyIntegration] = [.init(SwiftAsyncIntegration.self)]

#if os(iOS) && !SENTRY_NO_UIKIT
let integrations: [AnyIntegration] = commonIntegrations + [.init(UserFeedbackIntegration<SentryDependencyContainer>.self)]
#else
let integrations: [AnyIntegration] = commonIntegrations
integrations.append(.init(UserFeedbackIntegration<SentryDependencyContainer>.self))
#endif

#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS)
integrations.append(.init(FlushLogsIntegration<SentryDependencyContainer>.self))
#endif

integrations.forEach { anyIntegration in
guard let integration = anyIntegration.install(options, dependencies) else { return }

Expand Down
82 changes: 82 additions & 0 deletions Sources/Swift/Integrations/Log/FlushLogsIntegration.swift
Original file line number Diff line number Diff line change
@@ -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)
import AppKit
private typealias CrossPlatformApplication = NSApplication
#endif

#if ((os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT) || os(macOS)

protocol NotificationCenterProvider {
var notificationCenterWrapper: SentryNSNotificationCenterWrapper { get }
}

final class FlushLogsIntegration<Dependencies: NotificationCenterProvider>: 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
Copy link
Member

Choose a reason for hiding this comment

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

l: Maybe worth adding a log message that the client is nil so we don't have to call captureLogs

}
client.captureLogs()
}

@objc private func willTerminate() {
guard let client = SentrySDKInternal.currentHub().getClient() else {
return
}
client.captureLogs()
}

static var name: String {
"FlushLogsIntegration"
}
}

#endif
6 changes: 3 additions & 3 deletions Sources/Swift/Integrations/Session/SessionTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

m: Are these changes here intended?

import AppKit
typealias Application = NSApplication
#endif
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Sources/Swift/SentryDependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
extension SentryDependencyContainer: NotificationCenterProvider { }
#endif
5 changes: 3 additions & 2 deletions Tests/SentryTests/Helper/SentryAppStateManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.dsnForTestCase(type: SentryAppStateManagerTests.self)

private class Fixture {

Expand Down Expand Up @@ -51,6 +51,7 @@ class SentryAppStateManagerTests: XCTestCase {

override func tearDown() {
super.tearDown()
sut.stop(withForce: true)
fixture.fileManager.deleteAppState()
clearTestState()
}
Expand Down
Loading
Loading