Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 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
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 @@ -2066,10 +2068,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 @@ -3509,6 +3513,7 @@
7B944FA924697E9700A10721 /* Integrations */ = {
isa = PBXGroup;
children = (
927D21F42ED5DE7800916D31 /* Log */,
843FB3422D156B9900558F18 /* Feedback */,
7BF6505D292B77D100BBA5A8 /* MetricKit */,
D808FB85281AB2EF009A2A33 /* UIEvents */,
Expand Down Expand Up @@ -4187,6 +4192,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 @@ -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 */,
Expand All @@ -4470,7 +4500,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 @@ -4831,6 +4860,7 @@
D8CAC02D2BA0663E00E38F34 /* Integrations */ = {
isa = PBXGroup;
children = (
925189AB2EDDA6A300557BD1 /* Log */,
FAB0073C2E9F47DE001C806A /* Session */,
FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */,
D49064862DFAE1B700555785 /* Screenshot */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
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 flushLogsInvocations = Invocations<Void>()
public override func flushLogs() {
flushLogsInvocations.record(())
}
}
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryBaseIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
#endif // SENTRY_HAS_UIKIT
}

if ((integrationOptions & kIntegrationOptionEnableLogs) && !options.enableLogs) {
[self logWithOptionName:@"enableLogs"];
return NO;
}

return YES;
}

Expand Down
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(
Copy link
Member

Choose a reason for hiding this comment

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

m: Consider moving this to SentryDispatchFactory.m available via SentryDependencyContainer.sharedInstance.dispatchFactory so the creating of queues is consolidated to one position (as it should be everywhere)

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.
Expand Down Expand Up @@ -1105,6 +1113,11 @@ - (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope
}
}

- (void)flushLogs
{
[self.logBatcher captureLogs];
}

- (void)captureLogsData:(NSData *)data with:(NSNumber *)itemCount
{
SentryEnvelopeItem *envelopeItem =
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/_SentryDispatchQueueWrapperInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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;
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryBaseIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) {
kIntegrationOptionEnableMetricKit = 1 << 17,
kIntegrationOptionEnableReplay = 1 << 18,
kIntegrationOptionStartFramesTracker = 1 << 19,
kIntegrationOptionEnableLogs = 1 << 20,
};

@class SentryOptions;
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)flushLogs;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -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 let helper: SentryDefaultAppStateManager
private var helper: SentryDefaultAppStateManager
#endif

init(releaseName: String?, crashWrapper: SentryCrashWrapper, fileManager: SentryFileManager?, sysctlWrapper: SentrySysctl) {
Expand Down Expand Up @@ -41,6 +41,7 @@ import UIKit
}
}
_updateAppState = updateAppState

helper = SentryDefaultAppStateManager(storeCurrent: {
fileManager?.store(buildCurrentAppState())
}, updateTerminated: {
Expand Down
11 changes: 8 additions & 3 deletions Sources/Swift/Core/Integrations/Integrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SentryDependencyContainer>.self)]
#else
let integrations: [AnyIntegration] = []
integrations.append(.init(UserFeedbackIntegration<SentryDependencyContainer>.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<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) || 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<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.flushLogs()
}

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

static var name: String {
"FlushLogsIntegration"
}
}

#endif
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) || targetEnvironment(macCatalyst)) && !SENTRY_NO_UIKIT)
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.dsnAsString(username: "SentryAppStateManagerTests")

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